| "use client"; |
|
|
| import * as Y from "yjs"; |
|
|
| export interface QueuedUpdate { |
| id: string; |
| documentId: string; |
| update: Uint8Array; |
| timestamp: number; |
| synced: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| export class OfflineQueue { |
| private queue: QueuedUpdate[] = []; |
| private documentId: string; |
| private storageKey: string; |
|
|
| constructor(documentId: string) { |
| this.documentId = documentId; |
| this.storageKey = `offline-queue-${documentId}`; |
| this.loadFromStorage(); |
| } |
|
|
| private loadFromStorage(): void { |
| try { |
| const stored = localStorage.getItem(this.storageKey); |
| if (stored) { |
| const parsed = JSON.parse(stored); |
| this.queue = parsed.map((item: { id: string; documentId: string; update: number[]; timestamp: number; synced: boolean }) => ({ |
| ...item, |
| update: new Uint8Array(item.update), |
| })); |
| } |
| } catch { |
| this.queue = []; |
| } |
| } |
|
|
| private saveToStorage(): void { |
| try { |
| const serialized = this.queue.map((item) => ({ |
| ...item, |
| update: Array.from(item.update), |
| })); |
| localStorage.setItem(this.storageKey, JSON.stringify(serialized)); |
| } catch { |
| |
| } |
| } |
|
|
| |
| |
| |
| enqueue(update: Uint8Array): string { |
| const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; |
| const queuedUpdate: QueuedUpdate = { |
| id, |
| documentId: this.documentId, |
| update, |
| timestamp: Date.now(), |
| synced: false, |
| }; |
| this.queue.push(queuedUpdate); |
| this.saveToStorage(); |
| return id; |
| } |
|
|
| |
| |
| |
| getUnsynced(): QueuedUpdate[] { |
| return this.queue.filter((item) => !item.synced); |
| } |
|
|
| |
| |
| |
| markSynced(ids: string[]): void { |
| const idSet = new Set(ids); |
| this.queue = this.queue.map((item) => |
| idSet.has(item.id) ? { ...item, synced: true } : item |
| ); |
| |
| const synced = this.queue.filter((item) => item.synced); |
| const unsynced = this.queue.filter((item) => !item.synced); |
| this.queue = [...synced.slice(-100), ...unsynced]; |
| this.saveToStorage(); |
| } |
|
|
| |
| |
| |
| clearSynced(): void { |
| this.queue = this.queue.filter((item) => !item.synced); |
| this.saveToStorage(); |
| } |
|
|
| |
| |
| |
| size(): number { |
| return this.getUnsynced().length; |
| } |
|
|
| |
| |
| |
| getMergedUpdate(): Uint8Array | null { |
| const unsynced = this.getUnsynced(); |
| if (unsynced.length === 0) return null; |
| return Y.mergeUpdates(unsynced.map((u) => u.update)); |
| } |
|
|
| |
| |
| |
| clear(): void { |
| this.queue = []; |
| this.saveToStorage(); |
| } |
| } |
|
|
| |
| |
| |
| export function encodeStateVector(doc: Y.Doc): Uint8Array { |
| return Y.encodeStateVector(doc); |
| } |
|
|
| export function encodeStateAsUpdate(doc: Y.Doc, targetStateVector?: Uint8Array): Uint8Array { |
| return Y.encodeStateAsUpdate(doc, targetStateVector); |
| } |
|
|
| |
| |
| |
| export function getMissingUpdates( |
| localDoc: Y.Doc, |
| remoteStateVector: Uint8Array |
| ): Uint8Array { |
| |
| return Y.encodeStateAsUpdate(localDoc, remoteStateVector); |
| } |
|
|
| |
| |
| |
| export function compareStates( |
| localStateVector: Uint8Array, |
| remoteStateVector: Uint8Array |
| ): "ahead" | "behind" | "diverged" | "synced" { |
| const localMap = Y.decodeStateVector(localStateVector); |
| const remoteMap = Y.decodeStateVector(remoteStateVector); |
|
|
| let localAhead = false; |
| let remoteBehind = false; |
|
|
| |
| localMap.forEach((localClock, clientId) => { |
| const remoteClock = remoteMap.get(clientId) || 0; |
| if (localClock > remoteClock) localAhead = true; |
| if (localClock < remoteClock) remoteBehind = true; |
| }); |
|
|
| |
| remoteMap.forEach((remoteClock, clientId) => { |
| if (!localMap.has(clientId) && remoteClock > 0) { |
| remoteBehind = true; |
| } |
| }); |
|
|
| if (localAhead && remoteBehind) return "diverged"; |
| if (localAhead) return "ahead"; |
| if (remoteBehind) return "behind"; |
| return "synced"; |
| } |
|
|