@prabhask5/stellar-engine 1.1.17 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -1
- package/dist/bin/install-pwa.js +234 -317
- package/dist/bin/install-pwa.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -2
- package/dist/config.js.map +1 -1
- package/dist/crdt/awareness.d.ts +128 -0
- package/dist/crdt/awareness.d.ts.map +1 -0
- package/dist/crdt/awareness.js +284 -0
- package/dist/crdt/awareness.js.map +1 -0
- package/dist/crdt/channel.d.ts +165 -0
- package/dist/crdt/channel.d.ts.map +1 -0
- package/dist/crdt/channel.js +522 -0
- package/dist/crdt/channel.js.map +1 -0
- package/dist/crdt/config.d.ts +58 -0
- package/dist/crdt/config.d.ts.map +1 -0
- package/dist/crdt/config.js +123 -0
- package/dist/crdt/config.js.map +1 -0
- package/dist/crdt/helpers.d.ts +104 -0
- package/dist/crdt/helpers.d.ts.map +1 -0
- package/dist/crdt/helpers.js +116 -0
- package/dist/crdt/helpers.js.map +1 -0
- package/dist/crdt/offline.d.ts +58 -0
- package/dist/crdt/offline.d.ts.map +1 -0
- package/dist/crdt/offline.js +130 -0
- package/dist/crdt/offline.js.map +1 -0
- package/dist/crdt/persistence.d.ts +65 -0
- package/dist/crdt/persistence.d.ts.map +1 -0
- package/dist/crdt/persistence.js +171 -0
- package/dist/crdt/persistence.js.map +1 -0
- package/dist/crdt/provider.d.ts +109 -0
- package/dist/crdt/provider.d.ts.map +1 -0
- package/dist/crdt/provider.js +543 -0
- package/dist/crdt/provider.js.map +1 -0
- package/dist/crdt/store.d.ts +111 -0
- package/dist/crdt/store.d.ts.map +1 -0
- package/dist/crdt/store.js +158 -0
- package/dist/crdt/store.js.map +1 -0
- package/dist/crdt/types.d.ts +281 -0
- package/dist/crdt/types.d.ts.map +1 -0
- package/dist/crdt/types.js +26 -0
- package/dist/crdt/types.js.map +1 -0
- package/dist/database.d.ts +1 -1
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +28 -7
- package/dist/database.js.map +1 -1
- package/dist/diagnostics.d.ts +75 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +114 -2
- package/dist/diagnostics.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +21 -1
- package/dist/engine.js.map +1 -1
- package/dist/entries/crdt.d.ts +32 -0
- package/dist/entries/crdt.d.ts.map +1 -0
- package/dist/entries/crdt.js +52 -0
- package/dist/entries/crdt.js.map +1 -0
- package/dist/entries/types.d.ts +1 -0
- package/dist/entries/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
- package/dist/operations.d.ts +0 -73
- package/dist/operations.d.ts.map +0 -1
- package/dist/operations.js +0 -227
- package/dist/operations.js.map +0 -1
- package/dist/reconnectHandler.d.ts +0 -16
- package/dist/reconnectHandler.d.ts.map +0 -1
- package/dist/reconnectHandler.js +0 -21
- package/dist/reconnectHandler.js.map +0 -1
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CRDT Document Provider — Per-Document Lifecycle Manager
|
|
3
|
+
*
|
|
4
|
+
* The `CRDTProvider` is the central orchestrator for a single collaborative
|
|
5
|
+
* document. It manages:
|
|
6
|
+
* - Yjs `Y.Doc` creation and state loading (from IndexedDB or Supabase)
|
|
7
|
+
* - Wiring `doc.on('update')` to persistence, broadcast, and crash recovery
|
|
8
|
+
* - Supabase Broadcast channel for real-time update distribution
|
|
9
|
+
* - Periodic Supabase persistence timer
|
|
10
|
+
* - Local IndexedDB full-state saves (debounced)
|
|
11
|
+
* - Document lifecycle (open → edit → close → destroy)
|
|
12
|
+
*
|
|
13
|
+
* Module-level `Map<string, CRDTProvider>` tracks all active providers.
|
|
14
|
+
* Factory functions {@link openDocument} / {@link closeDocument} manage the lifecycle.
|
|
15
|
+
*
|
|
16
|
+
* @see {@link ./channel.ts} for Broadcast channel management
|
|
17
|
+
* @see {@link ./store.ts} for IndexedDB persistence
|
|
18
|
+
* @see {@link ./persistence.ts} for Supabase persistence
|
|
19
|
+
* @see {@link ./awareness.ts} for cursor/presence management
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* import { openDocument, closeDocument } from '@prabhask5/stellar-engine/crdt';
|
|
23
|
+
*
|
|
24
|
+
* const provider = await openDocument('doc-1', 'page-1', { offlineEnabled: true });
|
|
25
|
+
* // provider.doc is a Y.Doc — use with your editor
|
|
26
|
+
* // ...
|
|
27
|
+
* await closeDocument('doc-1');
|
|
28
|
+
*/
|
|
29
|
+
import * as Y from 'yjs';
|
|
30
|
+
import { debugLog, debugWarn } from '../debug';
|
|
31
|
+
import { isOnline } from '../stores/network';
|
|
32
|
+
import { getCRDTConfig } from './config';
|
|
33
|
+
import { CRDTChannel } from './channel';
|
|
34
|
+
import { loadDocumentState, saveDocumentState, appendPendingUpdate, loadPendingUpdates, clearPendingUpdates } from './store';
|
|
35
|
+
import { fetchRemoteState, persistDocument } from './persistence';
|
|
36
|
+
import { joinPresence, leavePresence } from './awareness';
|
|
37
|
+
// =============================================================================
|
|
38
|
+
// Active Provider Registry
|
|
39
|
+
// =============================================================================
|
|
40
|
+
/**
|
|
41
|
+
* Tracks all currently active CRDT providers, keyed by `documentId`.
|
|
42
|
+
*
|
|
43
|
+
* This ensures:
|
|
44
|
+
* 1. `openDocument()` is idempotent — returns existing provider if already open
|
|
45
|
+
* 2. `closeAllDocuments()` can tear down everything on sign-out
|
|
46
|
+
* 3. Reconnection logic can iterate active providers
|
|
47
|
+
*/
|
|
48
|
+
const activeProviders = new Map();
|
|
49
|
+
// =============================================================================
|
|
50
|
+
// CRDTProvider Implementation (internal)
|
|
51
|
+
// =============================================================================
|
|
52
|
+
/**
|
|
53
|
+
* Internal implementation of the CRDT provider.
|
|
54
|
+
*
|
|
55
|
+
* Manages the full lifecycle of a collaborative document: loading initial
|
|
56
|
+
* state, wiring update handlers, managing the Broadcast channel, and
|
|
57
|
+
* coordinating persistence.
|
|
58
|
+
*
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
class CRDTProviderImpl {
|
|
62
|
+
constructor(documentId, pageId, offlineEnabled) {
|
|
63
|
+
this._connectionState = 'disconnected';
|
|
64
|
+
this._isDirty = false;
|
|
65
|
+
/** Broadcast channel for this document. */
|
|
66
|
+
this.channel = null;
|
|
67
|
+
/** Timer for periodic Supabase persistence. */
|
|
68
|
+
this.persistTimer = null;
|
|
69
|
+
/** Timer for debounced local IndexedDB full-state saves. */
|
|
70
|
+
this.localSaveTimer = null;
|
|
71
|
+
/** Yjs update handler reference (for cleanup). */
|
|
72
|
+
this.updateHandler = null;
|
|
73
|
+
/** Whether this provider has been destroyed. */
|
|
74
|
+
this.destroyed = false;
|
|
75
|
+
/** Last state vector at the time of last Supabase persist (for dirty detection). */
|
|
76
|
+
this.lastPersistedStateVector = null;
|
|
77
|
+
/** Guard against concurrent persist operations. */
|
|
78
|
+
this.persistInProgress = false;
|
|
79
|
+
/** Whether the current online state is true. */
|
|
80
|
+
this._isOnline = true;
|
|
81
|
+
/** Store subscription cleanup function. */
|
|
82
|
+
this.onlineUnsubscribe = null;
|
|
83
|
+
this.documentId = documentId;
|
|
84
|
+
this.pageId = pageId;
|
|
85
|
+
this.offlineEnabled = offlineEnabled;
|
|
86
|
+
this.doc = new Y.Doc();
|
|
87
|
+
}
|
|
88
|
+
get connectionState() {
|
|
89
|
+
return this._connectionState;
|
|
90
|
+
}
|
|
91
|
+
get isDirty() {
|
|
92
|
+
return this._isDirty;
|
|
93
|
+
}
|
|
94
|
+
// ===========================================================================
|
|
95
|
+
// Initialization
|
|
96
|
+
// ===========================================================================
|
|
97
|
+
/**
|
|
98
|
+
* Initialize the provider: load state, wire handlers, join channel.
|
|
99
|
+
*
|
|
100
|
+
* Called by {@link openDocument} after construction.
|
|
101
|
+
*/
|
|
102
|
+
async init(options) {
|
|
103
|
+
/* Subscribe to online status. */
|
|
104
|
+
this.onlineUnsubscribe = isOnline.subscribe((online) => {
|
|
105
|
+
const wasOffline = !this._isOnline;
|
|
106
|
+
this._isOnline = online;
|
|
107
|
+
/* Reconnect on online transition. */
|
|
108
|
+
if (online && wasOffline && !this.destroyed) {
|
|
109
|
+
this.handleReconnect();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
/* Step 1: Load initial state. */
|
|
113
|
+
await this.loadInitialState();
|
|
114
|
+
/* Step 2: Wire the update handler. */
|
|
115
|
+
this.wireUpdateHandler();
|
|
116
|
+
/* Step 3: Join the Broadcast channel (if online). */
|
|
117
|
+
if (this._isOnline) {
|
|
118
|
+
await this.joinChannel();
|
|
119
|
+
/* Step 4: Run sync protocol. */
|
|
120
|
+
if (this.channel) {
|
|
121
|
+
const peersResponded = await this.channel.waitForSync();
|
|
122
|
+
if (!peersResponded) {
|
|
123
|
+
/* No peers online — fetch latest state from Supabase if available. */
|
|
124
|
+
await this.fetchAndMergeRemoteState();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/* Step 5: Join presence if initial presence was provided. */
|
|
129
|
+
if (options.initialPresence) {
|
|
130
|
+
joinPresence(this.documentId, this.channel?.connectionState === 'connected', {
|
|
131
|
+
name: options.initialPresence.name,
|
|
132
|
+
avatarUrl: options.initialPresence.avatarUrl
|
|
133
|
+
});
|
|
134
|
+
/* Set presence info on the channel so Supabase Presence tracks this user. */
|
|
135
|
+
this.channel?.setPresenceInfo({
|
|
136
|
+
name: options.initialPresence.name,
|
|
137
|
+
avatarUrl: options.initialPresence.avatarUrl
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/* Step 6: Start periodic Supabase persist timer. */
|
|
141
|
+
this.startPersistTimer();
|
|
142
|
+
/* Record initial state vector for dirty detection. */
|
|
143
|
+
this.lastPersistedStateVector = Y.encodeStateVector(this.doc);
|
|
144
|
+
debugLog(`[CRDT] Opening document ${this.documentId} (pageId=${this.pageId}, offlineEnabled=${this.offlineEnabled})`);
|
|
145
|
+
}
|
|
146
|
+
// ===========================================================================
|
|
147
|
+
// State Loading
|
|
148
|
+
// ===========================================================================
|
|
149
|
+
/**
|
|
150
|
+
* Load the initial document state from IndexedDB or Supabase.
|
|
151
|
+
*
|
|
152
|
+
* Priority:
|
|
153
|
+
* 1. IndexedDB (if offline-enabled and has stored state)
|
|
154
|
+
* 2. Supabase (if online and no local state)
|
|
155
|
+
* 3. Empty doc (if offline and no local state)
|
|
156
|
+
*/
|
|
157
|
+
async loadInitialState() {
|
|
158
|
+
/* Try IndexedDB first. */
|
|
159
|
+
const localRecord = await loadDocumentState(this.documentId);
|
|
160
|
+
if (localRecord) {
|
|
161
|
+
/* Apply stored full state. */
|
|
162
|
+
Y.applyUpdate(this.doc, localRecord.state);
|
|
163
|
+
/* Replay any pending updates that weren't captured in the last full save. */
|
|
164
|
+
const pendingUpdates = await loadPendingUpdates(this.documentId);
|
|
165
|
+
if (pendingUpdates.length > 0) {
|
|
166
|
+
for (const pending of pendingUpdates) {
|
|
167
|
+
Y.applyUpdate(this.doc, pending.update);
|
|
168
|
+
}
|
|
169
|
+
debugLog(`[CRDT] Document ${this.documentId} loaded from IndexedDB (${localRecord.stateSize} bytes, ${pendingUpdates.length} pending updates)`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
debugLog(`[CRDT] Document ${this.documentId} loaded from IndexedDB (${localRecord.stateSize} bytes, 0 pending updates)`);
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
/* No local state — try Supabase if online. */
|
|
177
|
+
if (this._isOnline) {
|
|
178
|
+
await this.fetchAndMergeRemoteState();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
/* Offline with no local state — start with empty doc. */
|
|
182
|
+
debugLog(`[CRDT] Document ${this.documentId} started with empty state (offline, no local cache)`);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Fetch the latest document state from Supabase and merge into local doc.
|
|
186
|
+
*/
|
|
187
|
+
async fetchAndMergeRemoteState() {
|
|
188
|
+
try {
|
|
189
|
+
const remoteState = await fetchRemoteState(this.pageId);
|
|
190
|
+
if (remoteState) {
|
|
191
|
+
Y.applyUpdate(this.doc, remoteState);
|
|
192
|
+
debugLog(`[CRDT] Document ${this.documentId} loaded from Supabase (${remoteState.byteLength} bytes)`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
debugWarn(`[CRDT] Document ${this.documentId}: failed to fetch remote state:`, e);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ===========================================================================
|
|
200
|
+
// Update Handler
|
|
201
|
+
// ===========================================================================
|
|
202
|
+
/**
|
|
203
|
+
* Wire the `doc.on('update')` handler that drives all downstream effects.
|
|
204
|
+
*
|
|
205
|
+
* On each Yjs update:
|
|
206
|
+
* 1. Queue incremental update to IndexedDB (crash safety)
|
|
207
|
+
* 2. Broadcast to remote peers (debounced 100ms)
|
|
208
|
+
* 3. Broadcast to same-device tabs (immediate)
|
|
209
|
+
* 4. Schedule debounced full-state save to IndexedDB (5s)
|
|
210
|
+
*/
|
|
211
|
+
wireUpdateHandler() {
|
|
212
|
+
this.updateHandler = (update, origin) => {
|
|
213
|
+
/* Skip updates that originated from remote peers (already persisted by them). */
|
|
214
|
+
if (origin === 'remote')
|
|
215
|
+
return;
|
|
216
|
+
this._isDirty = true;
|
|
217
|
+
/* 1. Crash-safe: append incremental update to IndexedDB. */
|
|
218
|
+
if (this.offlineEnabled) {
|
|
219
|
+
appendPendingUpdate(this.documentId, update).catch(() => {
|
|
220
|
+
/* Non-critical — full state save will capture it. */
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/* 2. Broadcast to remote peers (debounced). */
|
|
224
|
+
this.channel?.broadcastUpdate(update);
|
|
225
|
+
/* 3. Schedule debounced local full-state save. */
|
|
226
|
+
if (this.offlineEnabled) {
|
|
227
|
+
this.scheduleLocalSave();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
this.doc.on('update', this.updateHandler);
|
|
231
|
+
}
|
|
232
|
+
// ===========================================================================
|
|
233
|
+
// Local Persistence (IndexedDB)
|
|
234
|
+
// ===========================================================================
|
|
235
|
+
/**
|
|
236
|
+
* Schedule a debounced full-state save to IndexedDB.
|
|
237
|
+
*
|
|
238
|
+
* Resets the timer on each call so rapid edits don't cause excessive writes.
|
|
239
|
+
*/
|
|
240
|
+
scheduleLocalSave() {
|
|
241
|
+
if (this.localSaveTimer) {
|
|
242
|
+
clearTimeout(this.localSaveTimer);
|
|
243
|
+
}
|
|
244
|
+
const config = getCRDTConfig();
|
|
245
|
+
this.localSaveTimer = setTimeout(() => {
|
|
246
|
+
this.localSaveTimer = null;
|
|
247
|
+
this.saveToIndexedDB();
|
|
248
|
+
}, config.localSaveDebounceMs);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Save the current full document state to IndexedDB.
|
|
252
|
+
*
|
|
253
|
+
* Also clears pending updates since they're now captured in the full state.
|
|
254
|
+
*/
|
|
255
|
+
async saveToIndexedDB() {
|
|
256
|
+
if (this.destroyed)
|
|
257
|
+
return;
|
|
258
|
+
const state = Y.encodeStateAsUpdate(this.doc);
|
|
259
|
+
const stateVector = Y.encodeStateVector(this.doc);
|
|
260
|
+
const record = {
|
|
261
|
+
documentId: this.documentId,
|
|
262
|
+
pageId: this.pageId,
|
|
263
|
+
state,
|
|
264
|
+
stateVector,
|
|
265
|
+
offlineEnabled: this.offlineEnabled ? 1 : 0,
|
|
266
|
+
localUpdatedAt: new Date().toISOString(),
|
|
267
|
+
lastPersistedAt: null, // Will be set by Supabase persist
|
|
268
|
+
stateSize: state.byteLength
|
|
269
|
+
};
|
|
270
|
+
/* Preserve existing lastPersistedAt if we're just doing a local save. */
|
|
271
|
+
const existing = await loadDocumentState(this.documentId);
|
|
272
|
+
if (existing?.lastPersistedAt) {
|
|
273
|
+
record.lastPersistedAt = existing.lastPersistedAt;
|
|
274
|
+
}
|
|
275
|
+
await saveDocumentState(record);
|
|
276
|
+
await clearPendingUpdates(this.documentId);
|
|
277
|
+
}
|
|
278
|
+
// ===========================================================================
|
|
279
|
+
// Supabase Persistence Timer
|
|
280
|
+
// ===========================================================================
|
|
281
|
+
/**
|
|
282
|
+
* Start the periodic timer that persists dirty documents to Supabase.
|
|
283
|
+
*/
|
|
284
|
+
startPersistTimer() {
|
|
285
|
+
const config = getCRDTConfig();
|
|
286
|
+
this.persistTimer = setInterval(() => {
|
|
287
|
+
this.tryPersistToSupabase();
|
|
288
|
+
}, config.persistIntervalMs);
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Attempt to persist the document to Supabase if it's dirty and online.
|
|
292
|
+
*/
|
|
293
|
+
async tryPersistToSupabase() {
|
|
294
|
+
if (this.destroyed || !this._isOnline || !this._isDirty)
|
|
295
|
+
return;
|
|
296
|
+
/* Check if state has actually changed since last persist. */
|
|
297
|
+
const currentStateVector = Y.encodeStateVector(this.doc);
|
|
298
|
+
if (this.lastPersistedStateVector &&
|
|
299
|
+
arraysEqual(currentStateVector, this.lastPersistedStateVector)) {
|
|
300
|
+
debugLog(`[CRDT] Document ${this.documentId}: Supabase persist skipped (not dirty)`);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
/* Guard against concurrent persists. */
|
|
304
|
+
if (this.persistInProgress) {
|
|
305
|
+
debugLog(`[CRDT] Document ${this.documentId}: persist already in progress, skipping`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
this.persistInProgress = true;
|
|
309
|
+
try {
|
|
310
|
+
await persistDocument(this.documentId, this.doc);
|
|
311
|
+
this.lastPersistedStateVector = currentStateVector;
|
|
312
|
+
this._isDirty = false;
|
|
313
|
+
/* Update local record's lastPersistedAt. */
|
|
314
|
+
if (this.offlineEnabled) {
|
|
315
|
+
const existing = await loadDocumentState(this.documentId);
|
|
316
|
+
if (existing) {
|
|
317
|
+
existing.lastPersistedAt = new Date().toISOString();
|
|
318
|
+
await saveDocumentState(existing);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
debugWarn(`[CRDT] Document ${this.documentId}: Supabase persist failed:`, e);
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
this.persistInProgress = false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// ===========================================================================
|
|
330
|
+
// Channel Management
|
|
331
|
+
// ===========================================================================
|
|
332
|
+
/**
|
|
333
|
+
* Join the Supabase Broadcast channel for this document.
|
|
334
|
+
*/
|
|
335
|
+
async joinChannel() {
|
|
336
|
+
this.channel = new CRDTChannel(this.documentId, this.doc, (state) => {
|
|
337
|
+
this._connectionState = state;
|
|
338
|
+
});
|
|
339
|
+
await this.channel.join();
|
|
340
|
+
}
|
|
341
|
+
// ===========================================================================
|
|
342
|
+
// Reconnection
|
|
343
|
+
// ===========================================================================
|
|
344
|
+
/**
|
|
345
|
+
* Handle offline → online transition.
|
|
346
|
+
*
|
|
347
|
+
* Merges pending updates, rejoins the Broadcast channel, runs sync protocol,
|
|
348
|
+
* and persists the merged state to Supabase.
|
|
349
|
+
*/
|
|
350
|
+
async handleReconnect() {
|
|
351
|
+
debugLog(`[CRDT] Document ${this.documentId}: reconnecting after coming online`);
|
|
352
|
+
/* Merge any pending updates accumulated while offline. */
|
|
353
|
+
const pendingUpdates = await loadPendingUpdates(this.documentId);
|
|
354
|
+
if (pendingUpdates.length > 0) {
|
|
355
|
+
debugLog(`[CRDT] Document ${this.documentId}: merging ${pendingUpdates.length} pending updates after reconnect`);
|
|
356
|
+
for (const pending of pendingUpdates) {
|
|
357
|
+
Y.applyUpdate(this.doc, pending.update);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/* Rejoin Broadcast channel. */
|
|
361
|
+
if (this.channel) {
|
|
362
|
+
await this.channel.leave();
|
|
363
|
+
}
|
|
364
|
+
await this.joinChannel();
|
|
365
|
+
/* Run sync protocol. */
|
|
366
|
+
if (this.channel) {
|
|
367
|
+
const peersResponded = await this.channel.waitForSync();
|
|
368
|
+
if (!peersResponded) {
|
|
369
|
+
await this.fetchAndMergeRemoteState();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/* Broadcast our pending updates to peers. */
|
|
373
|
+
const state = Y.encodeStateAsUpdate(this.doc);
|
|
374
|
+
this.channel?.broadcastUpdate(state);
|
|
375
|
+
/* Immediately persist merged state to Supabase. */
|
|
376
|
+
this._isDirty = true;
|
|
377
|
+
await this.tryPersistToSupabase();
|
|
378
|
+
/* Clear pending updates (now captured in full state). */
|
|
379
|
+
await clearPendingUpdates(this.documentId);
|
|
380
|
+
debugLog(`[CRDT] Document ${this.documentId}: reconnection sync complete, state size ${state.byteLength} bytes`);
|
|
381
|
+
}
|
|
382
|
+
// ===========================================================================
|
|
383
|
+
// Destruction
|
|
384
|
+
// ===========================================================================
|
|
385
|
+
/**
|
|
386
|
+
* Destroy this provider: save final state, leave channel, clean up.
|
|
387
|
+
*/
|
|
388
|
+
async destroy() {
|
|
389
|
+
if (this.destroyed)
|
|
390
|
+
return;
|
|
391
|
+
this.destroyed = true;
|
|
392
|
+
debugLog(`[CRDT] Closing document ${this.documentId} (dirty=${this._isDirty}, online=${this._isOnline})`);
|
|
393
|
+
/* Stop timers. */
|
|
394
|
+
if (this.persistTimer) {
|
|
395
|
+
clearInterval(this.persistTimer);
|
|
396
|
+
this.persistTimer = null;
|
|
397
|
+
}
|
|
398
|
+
if (this.localSaveTimer) {
|
|
399
|
+
clearTimeout(this.localSaveTimer);
|
|
400
|
+
this.localSaveTimer = null;
|
|
401
|
+
}
|
|
402
|
+
/* Unwire update handler. */
|
|
403
|
+
if (this.updateHandler) {
|
|
404
|
+
this.doc.off('update', this.updateHandler);
|
|
405
|
+
this.updateHandler = null;
|
|
406
|
+
}
|
|
407
|
+
/* Unsubscribe from online store. */
|
|
408
|
+
if (this.onlineUnsubscribe) {
|
|
409
|
+
this.onlineUnsubscribe();
|
|
410
|
+
this.onlineUnsubscribe = null;
|
|
411
|
+
}
|
|
412
|
+
/* Save final state to IndexedDB. */
|
|
413
|
+
if (this.offlineEnabled) {
|
|
414
|
+
await this.saveToIndexedDB();
|
|
415
|
+
}
|
|
416
|
+
/* Persist to Supabase if online and dirty. */
|
|
417
|
+
if (this._isOnline && this._isDirty) {
|
|
418
|
+
await this.tryPersistToSupabase();
|
|
419
|
+
}
|
|
420
|
+
/* Leave presence. */
|
|
421
|
+
leavePresence(this.documentId);
|
|
422
|
+
/* Leave Broadcast channel. */
|
|
423
|
+
if (this.channel) {
|
|
424
|
+
await this.channel.leave();
|
|
425
|
+
this.channel = null;
|
|
426
|
+
}
|
|
427
|
+
/* Destroy Y.Doc. */
|
|
428
|
+
this.doc.destroy();
|
|
429
|
+
/* Remove from active providers. */
|
|
430
|
+
activeProviders.delete(this.documentId);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
// =============================================================================
|
|
434
|
+
// Utility
|
|
435
|
+
// =============================================================================
|
|
436
|
+
/** Compare two Uint8Arrays for equality. */
|
|
437
|
+
function arraysEqual(a, b) {
|
|
438
|
+
if (a.length !== b.length)
|
|
439
|
+
return false;
|
|
440
|
+
for (let i = 0; i < a.length; i++) {
|
|
441
|
+
if (a[i] !== b[i])
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
// =============================================================================
|
|
447
|
+
// Public Factory Functions
|
|
448
|
+
// =============================================================================
|
|
449
|
+
/**
|
|
450
|
+
* Open a collaborative CRDT document.
|
|
451
|
+
*
|
|
452
|
+
* Creates a new `CRDTProvider` for the document, loads its initial state
|
|
453
|
+
* (from IndexedDB or Supabase), wires update handlers, joins the Broadcast
|
|
454
|
+
* channel, and starts the periodic persist timer.
|
|
455
|
+
*
|
|
456
|
+
* **Idempotent:** If the document is already open, returns the existing provider.
|
|
457
|
+
*
|
|
458
|
+
* @param documentId - Unique identifier for the document.
|
|
459
|
+
* @param pageId - The page/entity this document belongs to.
|
|
460
|
+
* @param options - Optional configuration (offline mode, initial presence).
|
|
461
|
+
* @returns The active `CRDTProvider` for this document.
|
|
462
|
+
*
|
|
463
|
+
* @throws {Error} If CRDT is not configured in `initEngine()`.
|
|
464
|
+
*
|
|
465
|
+
* @example
|
|
466
|
+
* const provider = await openDocument('doc-1', 'page-1', {
|
|
467
|
+
* offlineEnabled: true,
|
|
468
|
+
* initialPresence: { name: 'Alice' },
|
|
469
|
+
* });
|
|
470
|
+
* const text = provider.doc.getText('content');
|
|
471
|
+
*/
|
|
472
|
+
export async function openDocument(documentId, pageId, options = {}) {
|
|
473
|
+
/* Ensure CRDT is configured (getCRDTConfig throws if not). */
|
|
474
|
+
getCRDTConfig();
|
|
475
|
+
/* Return existing provider if already open (idempotent). */
|
|
476
|
+
const existing = activeProviders.get(documentId);
|
|
477
|
+
if (existing)
|
|
478
|
+
return existing;
|
|
479
|
+
/* Create and initialize a new provider. */
|
|
480
|
+
const provider = new CRDTProviderImpl(documentId, pageId, options.offlineEnabled ?? false);
|
|
481
|
+
activeProviders.set(documentId, provider);
|
|
482
|
+
try {
|
|
483
|
+
await provider.init(options);
|
|
484
|
+
}
|
|
485
|
+
catch (e) {
|
|
486
|
+
/* Clean up on initialization failure. */
|
|
487
|
+
activeProviders.delete(documentId);
|
|
488
|
+
provider.doc.destroy();
|
|
489
|
+
throw e;
|
|
490
|
+
}
|
|
491
|
+
return provider;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Close a specific CRDT document.
|
|
495
|
+
*
|
|
496
|
+
* Saves final state, persists to Supabase if dirty, leaves the Broadcast
|
|
497
|
+
* channel, and destroys the Y.Doc.
|
|
498
|
+
*
|
|
499
|
+
* @param documentId - The document to close.
|
|
500
|
+
*/
|
|
501
|
+
export async function closeDocument(documentId) {
|
|
502
|
+
const provider = activeProviders.get(documentId);
|
|
503
|
+
if (!provider)
|
|
504
|
+
return;
|
|
505
|
+
await provider.destroy();
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Close all active CRDT documents.
|
|
509
|
+
*
|
|
510
|
+
* Called during sign-out to ensure all documents are properly saved and
|
|
511
|
+
* all channels are cleaned up. Each document is closed in parallel.
|
|
512
|
+
*/
|
|
513
|
+
export async function closeAllDocuments() {
|
|
514
|
+
const count = activeProviders.size;
|
|
515
|
+
if (count === 0)
|
|
516
|
+
return;
|
|
517
|
+
debugLog(`[CRDT] All documents closed (count=${count})`);
|
|
518
|
+
const promises = Array.from(activeProviders.values()).map((p) => p.destroy());
|
|
519
|
+
await Promise.allSettled(promises);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get the active provider for a document, if open.
|
|
523
|
+
*
|
|
524
|
+
* @param documentId - The document to look up.
|
|
525
|
+
* @returns The active provider, or `undefined` if not open.
|
|
526
|
+
* @internal
|
|
527
|
+
*/
|
|
528
|
+
export function getActiveProvider(documentId) {
|
|
529
|
+
return activeProviders.get(documentId);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Get all active provider entries for iteration.
|
|
533
|
+
*
|
|
534
|
+
* Used by {@link persistence.ts#persistAllDirty} to iterate and persist
|
|
535
|
+
* all dirty documents. Returns `[documentId, provider]` pairs.
|
|
536
|
+
*
|
|
537
|
+
* @returns Iterator of `[documentId, CRDTProvider]` entries.
|
|
538
|
+
* @internal
|
|
539
|
+
*/
|
|
540
|
+
export function getActiveProviderEntries() {
|
|
541
|
+
return activeProviders.entries();
|
|
542
|
+
}
|
|
543
|
+
//# sourceMappingURL=provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/crdt/provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CRDT IndexedDB Persistence Layer
|
|
3
|
+
*
|
|
4
|
+
* Provides CRUD operations for the two CRDT-specific IndexedDB tables:
|
|
5
|
+
* - `crdtDocuments` — Full Yjs document state snapshots
|
|
6
|
+
* - `crdtPendingUpdates` — Incremental Yjs update deltas for crash recovery
|
|
7
|
+
*
|
|
8
|
+
* These tables are conditionally created by {@link ../database.ts#buildDexie}
|
|
9
|
+
* only when `crdt` config is provided to `initEngine()`.
|
|
10
|
+
*
|
|
11
|
+
* This module also exposes offline management query functions:
|
|
12
|
+
* - {@link isOfflineEnabled} — check if a document is stored for offline
|
|
13
|
+
* - {@link getOfflineDocuments} — list all offline-enabled documents
|
|
14
|
+
* - {@link getOfflineDocumentCount} — count for limit enforcement
|
|
15
|
+
*
|
|
16
|
+
* All functions access Dexie via the engine-managed instance from
|
|
17
|
+
* {@link ../database.ts#getDb}. Binary Yjs state is stored directly as
|
|
18
|
+
* `Uint8Array` — Dexie/IndexedDB handles binary data natively.
|
|
19
|
+
*
|
|
20
|
+
* @see {@link ./types.ts} for record shapes (CRDTDocumentRecord, CRDTPendingUpdate)
|
|
21
|
+
* @see {@link ./provider.ts} for the orchestrator that calls these functions
|
|
22
|
+
* @see {@link ../database.ts} for conditional CRDT table creation
|
|
23
|
+
*/
|
|
24
|
+
import type { CRDTDocumentRecord, CRDTPendingUpdate } from './types';
|
|
25
|
+
/**
|
|
26
|
+
* Load a CRDT document record from IndexedDB.
|
|
27
|
+
*
|
|
28
|
+
* @param documentId - The unique document identifier.
|
|
29
|
+
* @returns The document record, or `undefined` if not found.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadDocumentState(documentId: string): Promise<CRDTDocumentRecord | undefined>;
|
|
32
|
+
/**
|
|
33
|
+
* Save a full CRDT document state snapshot to IndexedDB.
|
|
34
|
+
*
|
|
35
|
+
* Uses Dexie's `put()` for upsert semantics — creates a new record if the
|
|
36
|
+
* document doesn't exist, or overwrites the existing one.
|
|
37
|
+
*
|
|
38
|
+
* @param record - The full document record to persist.
|
|
39
|
+
*/
|
|
40
|
+
export declare function saveDocumentState(record: CRDTDocumentRecord): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Delete a CRDT document record from IndexedDB.
|
|
43
|
+
*
|
|
44
|
+
* Also clears all associated pending updates for the document.
|
|
45
|
+
*
|
|
46
|
+
* @param documentId - The document to delete.
|
|
47
|
+
*/
|
|
48
|
+
export declare function deleteDocumentState(documentId: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Load a CRDT document record by page ID.
|
|
51
|
+
*
|
|
52
|
+
* Pages may have at most one CRDT document. Returns the first match.
|
|
53
|
+
*
|
|
54
|
+
* @param pageId - The page/entity ID to look up.
|
|
55
|
+
* @returns The document record, or `undefined` if not found.
|
|
56
|
+
*/
|
|
57
|
+
export declare function loadDocumentByPageId(pageId: string): Promise<CRDTDocumentRecord | undefined>;
|
|
58
|
+
/**
|
|
59
|
+
* Append an incremental Yjs update to the pending updates table.
|
|
60
|
+
*
|
|
61
|
+
* Called on every `doc.on('update')` event for crash safety. If the browser
|
|
62
|
+
* crashes between full-state saves (every 5s), these deltas can be replayed
|
|
63
|
+
* to recover the document.
|
|
64
|
+
*
|
|
65
|
+
* @param documentId - The document this update belongs to.
|
|
66
|
+
* @param update - The incremental Yjs update delta.
|
|
67
|
+
*/
|
|
68
|
+
export declare function appendPendingUpdate(documentId: string, update: Uint8Array): Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Load all pending updates for a specific document, ordered by ID (insertion order).
|
|
71
|
+
*
|
|
72
|
+
* Used during document opening to replay any updates that weren't captured
|
|
73
|
+
* in the last full-state save.
|
|
74
|
+
*
|
|
75
|
+
* @param documentId - The document to load updates for.
|
|
76
|
+
* @returns Array of pending update records, oldest first.
|
|
77
|
+
*/
|
|
78
|
+
export declare function loadPendingUpdates(documentId: string): Promise<CRDTPendingUpdate[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Clear all pending updates for a document.
|
|
81
|
+
*
|
|
82
|
+
* Called after a successful full-state save to IndexedDB or Supabase persist,
|
|
83
|
+
* since the updates have been captured in the full state snapshot.
|
|
84
|
+
*
|
|
85
|
+
* @param documentId - The document to clear updates for.
|
|
86
|
+
* @returns The number of updates cleared.
|
|
87
|
+
*/
|
|
88
|
+
export declare function clearPendingUpdates(documentId: string): Promise<number>;
|
|
89
|
+
/**
|
|
90
|
+
* Check whether a specific document is stored for offline access.
|
|
91
|
+
*
|
|
92
|
+
* @param documentId - The document to check.
|
|
93
|
+
* @returns `true` if the document has `offlineEnabled: 1` in IndexedDB.
|
|
94
|
+
*/
|
|
95
|
+
export declare function isOfflineEnabled(documentId: string): Promise<boolean>;
|
|
96
|
+
/**
|
|
97
|
+
* Get all documents that are stored for offline access.
|
|
98
|
+
*
|
|
99
|
+
* @returns Array of document records with `offlineEnabled: 1`.
|
|
100
|
+
*/
|
|
101
|
+
export declare function getOfflineDocuments(): Promise<CRDTDocumentRecord[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Count the number of documents currently stored for offline access.
|
|
104
|
+
*
|
|
105
|
+
* Used by {@link ./offline.ts#enableOffline} to enforce the
|
|
106
|
+
* `maxOfflineDocuments` limit.
|
|
107
|
+
*
|
|
108
|
+
* @returns The number of offline-enabled documents.
|
|
109
|
+
*/
|
|
110
|
+
export declare function getOfflineDocumentCount(): Promise<number>;
|
|
111
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/crdt/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAMrE;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACrC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAGzC;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjF;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3E;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAGzC;AAMD;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/F;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAGzF;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAG7E;AAMD;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAGzE;AAED;;;;;;;GAOG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC,MAAM,CAAC,CAG/D"}
|