feat: implement live-sync for tasks, workspace note sharing, and document locking system
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 53s

This commit is contained in:
sandrodach committed 2026-10-25 02:23:46 +02:00
1 parent dccf1a91e3
commit 84e0b6a4bd
10 files changed
+1773 -110

No files matched your search

+150 -2
View File
@@ -4,6 +4,8 @@ import {
DashboardStats,
Document,
DocumentBlock,
DocShare,
DocLockInfo,
ListItem,
Organization,
PriorityLevel,
@@ -16,6 +18,7 @@ import {
SubTask,
User,
UserInvite,
WSMessage,
} from './types';
const TOKEN_STORAGE_KEY = 'mindsift_jwt_token';
@@ -428,6 +431,69 @@ export const api = {
await request(`/api/v1/documents/${id}`, { method: 'DELETE' });
},
// --- DOCUMENT SHARING ---
async getDocShares(docId: string): Promise<DocShare[]> {
const res = await request<DocShare[]>(`/api/v1/documents/${docId}/shares`);
return ensureArray(res);
},
async createDocShare(
docId: string,
payload: { userId?: string; email?: string; permission: 'read' | 'write' }
): Promise<DocShare> {
return request<DocShare>(`/api/v1/documents/${docId}/shares`, {
method: 'POST',
body: JSON.stringify(payload),
});
},
async deleteDocShare(docId: string, userId: string): Promise<void> {
await request(`/api/v1/documents/${docId}/shares/${userId}`, { method: 'DELETE' });
},
async getCollaborators(): Promise<User[]> {
const res = await request<User[]>('/api/v1/collaborators');
return ensureArray(res);
},
// --- DOCUMENT LOCKING ---
async getDocLock(docId: string): Promise<{ locked: boolean; lock?: DocLockInfo }> {
return request<{ locked: boolean; lock?: DocLockInfo }>(`/api/v1/documents/${docId}/lock`);
},
async lockDocument(
docId: string
): Promise<{ locked: boolean; acquired: boolean; lock?: DocLockInfo; message?: string }> {
try {
return await request<{ locked: boolean; acquired: boolean; lock?: DocLockInfo; message?: string }>(
`/api/v1/documents/${docId}/lock`,
{ method: 'POST' }
);
} catch (e: any) {
return { locked: true, acquired: false, message: e.message };
}
},
async heartbeatDocLock(docId: string): Promise<{ extended: boolean }> {
try {
return await request<{ extended: boolean }>(`/api/v1/documents/${docId}/lock/heartbeat`, {
method: 'POST',
});
} catch {
return { extended: false };
}
},
async unlockDocument(docId: string): Promise<{ unlocked: boolean }> {
try {
return await request<{ unlocked: boolean }>(`/api/v1/documents/${docId}/lock`, {
method: 'DELETE',
});
} catch {
return { unlocked: false };
}
},
// --- SPACES & REALTIME LISTS ---
async getSpaces(): Promise<Space[]> {
const res = await request<Space[]>('/api/v1/spaces');
@@ -700,8 +766,90 @@ export function getOfflineQueue(): Thought[] {
}
}
export function buildWebSocketURL(spaceId: string): string {
let activeGlobalWS: WebSocket | null = null;
let wsReconnectTimer: any = null;
let wsListeners: ((msg: WSMessage) => void)[] = [];
let manualDisconnect = false;
export function buildWebSocketURL(spaceId?: string): string {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const token = getToken() || '';
return `${protocol}//${window.location.host}/api/v1/ws/spaces/${encodeURIComponent(spaceId)}?token=${encodeURIComponent(token)}`;
if (spaceId) {
return `${protocol}//${window.location.host}/api/v1/ws/spaces/${encodeURIComponent(spaceId)}?token=${encodeURIComponent(token)}`;
}
return `${protocol}//${window.location.host}/api/v1/ws?token=${encodeURIComponent(token)}`;
}
export function connectWebSocket(): void {
const token = getToken();
if (!token) return;
manualDisconnect = false;
if (activeGlobalWS && (activeGlobalWS.readyState === WebSocket.OPEN || activeGlobalWS.readyState === WebSocket.CONNECTING)) {
return;
}
try {
const url = buildWebSocketURL();
const ws = new WebSocket(url);
activeGlobalWS = ws;
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data) as WSMessage;
wsListeners.forEach((fn) => {
try {
fn(msg);
} catch (e) {
console.error('WS listener error', e);
}
});
window.dispatchEvent(new CustomEvent('mindsift:ws', { detail: msg }));
} catch (err) {
console.error('Failed to parse WS message', err);
}
};
ws.onclose = () => {
activeGlobalWS = null;
if (!manualDisconnect && getToken()) {
clearTimeout(wsReconnectTimer);
wsReconnectTimer = setTimeout(() => {
connectWebSocket();
}, 3000);
}
};
ws.onerror = () => {
try {
ws.close();
} catch {}
};
} catch (e) {
console.error('Failed to connect WebSocket', e);
}
}
export function disconnectWebSocket(): void {
manualDisconnect = true;
clearTimeout(wsReconnectTimer);
if (activeGlobalWS) {
try {
activeGlobalWS.close();
} catch {}
activeGlobalWS = null;
}
}
export function sendWebSocketMessage(msg: any): void {
if (activeGlobalWS && activeGlobalWS.readyState === WebSocket.OPEN) {
activeGlobalWS.send(typeof msg === 'string' ? msg : JSON.stringify(msg));
}
}
export function onWebSocketMessage(callback: (msg: WSMessage) => void): () => void {
wsListeners.push(callback);
return () => {
wsListeners = wsListeners.filter((fn) => fn !== callback);
};
}
+26 -2
View File
@@ -128,6 +128,24 @@ export interface DocumentBlock {
collapsed?: boolean;
}
export interface DocShare {
id: string;
docId: string;
userId: string;
userEmail?: string;
displayName?: string;
permission: 'read' | 'write';
createdBy: string;
createdAt: string;
}
export interface DocLockInfo {
docId: string;
userId: string;
userName: string;
expiresAt: string;
}
export interface Document {
id: string;
userId: string;
@@ -142,6 +160,12 @@ export interface Document {
isArchived: boolean;
createdAt: string;
updatedAt: string;
// Collaboration & Permissions
permission?: 'owner' | 'write' | 'read';
isOwner?: boolean;
ownerName?: string;
sharedWith?: DocShare[];
}
export interface SpaceMember {
@@ -212,8 +236,8 @@ export interface DashboardStats {
}
export interface WSMessage<T = any> {
event: 'ITEM_CREATED' | 'ITEM_UPDATED' | 'ITEM_TOGGLED' | 'ITEM_DELETED' | 'LIST_CLEARED' | 'LIST_CREATED' | 'LIST_DELETED' | 'USER_PRESENCE';
space_id: string;
event: string;
space_id?: string;
list_id?: string;
payload: T;
timestamp: string;