@fatagnus/dink-sync 1.0.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.
Files changed (186) hide show
  1. package/README.md +312 -0
  2. package/dist/client/attachment.d.ts +225 -0
  3. package/dist/client/attachment.d.ts.map +1 -0
  4. package/dist/client/attachment.js +402 -0
  5. package/dist/client/attachment.js.map +1 -0
  6. package/dist/client/binary-encoding.d.ts +45 -0
  7. package/dist/client/binary-encoding.d.ts.map +1 -0
  8. package/dist/client/binary-encoding.js +90 -0
  9. package/dist/client/binary-encoding.js.map +1 -0
  10. package/dist/client/collection.d.ts +10 -0
  11. package/dist/client/collection.d.ts.map +1 -0
  12. package/dist/client/collection.js +924 -0
  13. package/dist/client/collection.js.map +1 -0
  14. package/dist/client/compression.d.ts +56 -0
  15. package/dist/client/compression.d.ts.map +1 -0
  16. package/dist/client/compression.js +173 -0
  17. package/dist/client/compression.js.map +1 -0
  18. package/dist/client/crdt/index.d.ts +2 -0
  19. package/dist/client/crdt/index.d.ts.map +1 -0
  20. package/dist/client/crdt/index.js +2 -0
  21. package/dist/client/crdt/index.js.map +1 -0
  22. package/dist/client/crdt/yjs-doc.d.ts +88 -0
  23. package/dist/client/crdt/yjs-doc.d.ts.map +1 -0
  24. package/dist/client/crdt/yjs-doc.js +123 -0
  25. package/dist/client/crdt/yjs-doc.js.map +1 -0
  26. package/dist/client/index.d.ts +66 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +233 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client/mock-transport.d.ts +155 -0
  31. package/dist/client/mock-transport.d.ts.map +1 -0
  32. package/dist/client/mock-transport.js +292 -0
  33. package/dist/client/mock-transport.js.map +1 -0
  34. package/dist/client/network-detector.d.ts +65 -0
  35. package/dist/client/network-detector.d.ts.map +1 -0
  36. package/dist/client/network-detector.js +147 -0
  37. package/dist/client/network-detector.js.map +1 -0
  38. package/dist/client/provisioning.d.ts +126 -0
  39. package/dist/client/provisioning.d.ts.map +1 -0
  40. package/dist/client/provisioning.js +125 -0
  41. package/dist/client/provisioning.js.map +1 -0
  42. package/dist/client/signal.d.ts +13 -0
  43. package/dist/client/signal.d.ts.map +1 -0
  44. package/dist/client/signal.js +27 -0
  45. package/dist/client/signal.js.map +1 -0
  46. package/dist/client/sync-engine.d.ts +298 -0
  47. package/dist/client/sync-engine.d.ts.map +1 -0
  48. package/dist/client/sync-engine.js +904 -0
  49. package/dist/client/sync-engine.js.map +1 -0
  50. package/dist/client/synced-edge.d.ts +109 -0
  51. package/dist/client/synced-edge.d.ts.map +1 -0
  52. package/dist/client/synced-edge.js +179 -0
  53. package/dist/client/synced-edge.js.map +1 -0
  54. package/dist/client/synced-offline-edge-types.d.ts +540 -0
  55. package/dist/client/synced-offline-edge-types.d.ts.map +1 -0
  56. package/dist/client/synced-offline-edge-types.js +10 -0
  57. package/dist/client/synced-offline-edge-types.js.map +1 -0
  58. package/dist/client/synced-offline-edge.d.ts +54 -0
  59. package/dist/client/synced-offline-edge.d.ts.map +1 -0
  60. package/dist/client/synced-offline-edge.js +731 -0
  61. package/dist/client/synced-offline-edge.js.map +1 -0
  62. package/dist/client/transport.d.ts +202 -0
  63. package/dist/client/transport.d.ts.map +1 -0
  64. package/dist/client/transport.js +409 -0
  65. package/dist/client/transport.js.map +1 -0
  66. package/dist/client/types.d.ts +622 -0
  67. package/dist/client/types.d.ts.map +1 -0
  68. package/dist/client/types.js +60 -0
  69. package/dist/client/types.js.map +1 -0
  70. package/dist/client/validation.d.ts +61 -0
  71. package/dist/client/validation.d.ts.map +1 -0
  72. package/dist/client/validation.js +57 -0
  73. package/dist/client/validation.js.map +1 -0
  74. package/dist/client/versioning.d.ts +134 -0
  75. package/dist/client/versioning.d.ts.map +1 -0
  76. package/dist/client/versioning.js +304 -0
  77. package/dist/client/versioning.js.map +1 -0
  78. package/dist/index.d.ts +40 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +51 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/persistence/encryption.d.ts +114 -0
  83. package/dist/persistence/encryption.d.ts.map +1 -0
  84. package/dist/persistence/encryption.js +286 -0
  85. package/dist/persistence/encryption.js.map +1 -0
  86. package/dist/persistence/index.d.ts +21 -0
  87. package/dist/persistence/index.d.ts.map +1 -0
  88. package/dist/persistence/index.js +20 -0
  89. package/dist/persistence/index.js.map +1 -0
  90. package/dist/persistence/memory.d.ts +32 -0
  91. package/dist/persistence/memory.d.ts.map +1 -0
  92. package/dist/persistence/memory.js +57 -0
  93. package/dist/persistence/memory.js.map +1 -0
  94. package/dist/persistence/migrations.d.ts +106 -0
  95. package/dist/persistence/migrations.d.ts.map +1 -0
  96. package/dist/persistence/migrations.js +176 -0
  97. package/dist/persistence/migrations.js.map +1 -0
  98. package/dist/persistence/pending-queue.d.ts +109 -0
  99. package/dist/persistence/pending-queue.d.ts.map +1 -0
  100. package/dist/persistence/pending-queue.js +249 -0
  101. package/dist/persistence/pending-queue.js.map +1 -0
  102. package/dist/persistence/pglite.d.ts +72 -0
  103. package/dist/persistence/pglite.d.ts.map +1 -0
  104. package/dist/persistence/pglite.js +126 -0
  105. package/dist/persistence/pglite.js.map +1 -0
  106. package/dist/persistence/quota-manager.d.ts +134 -0
  107. package/dist/persistence/quota-manager.d.ts.map +1 -0
  108. package/dist/persistence/quota-manager.js +242 -0
  109. package/dist/persistence/quota-manager.js.map +1 -0
  110. package/dist/persistence/types.d.ts +54 -0
  111. package/dist/persistence/types.d.ts.map +1 -0
  112. package/dist/persistence/types.js +2 -0
  113. package/dist/persistence/types.js.map +1 -0
  114. package/dist/react/OfflineEdgeProvider.d.ts +91 -0
  115. package/dist/react/OfflineEdgeProvider.d.ts.map +1 -0
  116. package/dist/react/OfflineEdgeProvider.js +127 -0
  117. package/dist/react/OfflineEdgeProvider.js.map +1 -0
  118. package/dist/react/SyncedOfflineEdgeProvider.d.ts +105 -0
  119. package/dist/react/SyncedOfflineEdgeProvider.d.ts.map +1 -0
  120. package/dist/react/SyncedOfflineEdgeProvider.js +138 -0
  121. package/dist/react/SyncedOfflineEdgeProvider.js.map +1 -0
  122. package/dist/react/index.d.ts +50 -0
  123. package/dist/react/index.d.ts.map +1 -0
  124. package/dist/react/index.js +51 -0
  125. package/dist/react/index.js.map +1 -0
  126. package/dist/react/useCollection.d.ts +77 -0
  127. package/dist/react/useCollection.d.ts.map +1 -0
  128. package/dist/react/useCollection.js +113 -0
  129. package/dist/react/useCollection.js.map +1 -0
  130. package/dist/react/useCollectionSyncMode.d.ts +61 -0
  131. package/dist/react/useCollectionSyncMode.d.ts.map +1 -0
  132. package/dist/react/useCollectionSyncMode.js +93 -0
  133. package/dist/react/useCollectionSyncMode.js.map +1 -0
  134. package/dist/react/useConnectionState.d.ts +44 -0
  135. package/dist/react/useConnectionState.d.ts.map +1 -0
  136. package/dist/react/useConnectionState.js +46 -0
  137. package/dist/react/useConnectionState.js.map +1 -0
  138. package/dist/react/useDocumentSyncStatus.d.ts +72 -0
  139. package/dist/react/useDocumentSyncStatus.d.ts.map +1 -0
  140. package/dist/react/useDocumentSyncStatus.js +110 -0
  141. package/dist/react/useDocumentSyncStatus.js.map +1 -0
  142. package/dist/react/useOfflineEdge.d.ts +58 -0
  143. package/dist/react/useOfflineEdge.d.ts.map +1 -0
  144. package/dist/react/useOfflineEdge.js +54 -0
  145. package/dist/react/useOfflineEdge.js.map +1 -0
  146. package/dist/react/usePendingChanges.d.ts +67 -0
  147. package/dist/react/usePendingChanges.d.ts.map +1 -0
  148. package/dist/react/usePendingChanges.js +90 -0
  149. package/dist/react/usePendingChanges.js.map +1 -0
  150. package/dist/react/useRejectedDocuments.d.ts +112 -0
  151. package/dist/react/useRejectedDocuments.d.ts.map +1 -0
  152. package/dist/react/useRejectedDocuments.js +213 -0
  153. package/dist/react/useRejectedDocuments.js.map +1 -0
  154. package/dist/react/useSyncControls.d.ts +96 -0
  155. package/dist/react/useSyncControls.d.ts.map +1 -0
  156. package/dist/react/useSyncControls.js +112 -0
  157. package/dist/react/useSyncControls.js.map +1 -0
  158. package/dist/react/useSyncProgress.d.ts +78 -0
  159. package/dist/react/useSyncProgress.d.ts.map +1 -0
  160. package/dist/react/useSyncProgress.js +90 -0
  161. package/dist/react/useSyncProgress.js.map +1 -0
  162. package/dist/react/useSyncRejected.d.ts +47 -0
  163. package/dist/react/useSyncRejected.d.ts.map +1 -0
  164. package/dist/react/useSyncRejected.js +55 -0
  165. package/dist/react/useSyncRejected.js.map +1 -0
  166. package/dist/react/useSyncStatus.d.ts +56 -0
  167. package/dist/react/useSyncStatus.d.ts.map +1 -0
  168. package/dist/react/useSyncStatus.js +59 -0
  169. package/dist/react/useSyncStatus.js.map +1 -0
  170. package/dist/react/useSyncedOfflineEdge.d.ts +69 -0
  171. package/dist/react/useSyncedOfflineEdge.d.ts.map +1 -0
  172. package/dist/react/useSyncedOfflineEdge.js +65 -0
  173. package/dist/react/useSyncedOfflineEdge.js.map +1 -0
  174. package/dist/service-worker/index.d.ts +7 -0
  175. package/dist/service-worker/index.d.ts.map +1 -0
  176. package/dist/service-worker/index.js +7 -0
  177. package/dist/service-worker/index.js.map +1 -0
  178. package/dist/service-worker/sync-worker.d.ts +230 -0
  179. package/dist/service-worker/sync-worker.d.ts.map +1 -0
  180. package/dist/service-worker/sync-worker.js +471 -0
  181. package/dist/service-worker/sync-worker.js.map +1 -0
  182. package/dist/types.d.ts +6 -0
  183. package/dist/types.d.ts.map +1 -0
  184. package/dist/types.js +3 -0
  185. package/dist/types.js.map +1 -0
  186. package/package.json +95 -0
@@ -0,0 +1,731 @@
1
+ /**
2
+ * SyncedOfflineEdge - A high-level API for offline-first applications
3
+ * with configurable per-collection sync modes.
4
+ *
5
+ * Combines OfflineEdgeClient with SyncEngine for automatic sync,
6
+ * providing granular per-collection sync control, lifecycle hooks,
7
+ * and manual sync controls.
8
+ *
9
+ * @module synced-offline-edge
10
+ */
11
+ import { createSignal } from './signal.js';
12
+ import { createCollection } from './collection.js';
13
+ import { createSyncEngine } from './sync-engine.js';
14
+ import { YjsDocument } from './crdt/yjs-doc.js';
15
+ /**
16
+ * Create a SyncedOfflineEdge instance.
17
+ *
18
+ * @param options - Configuration options
19
+ * @returns A promise that resolves to a SyncedOfflineEdge instance
20
+ */
21
+ export async function createSyncedOfflineEdge(options) {
22
+ const { persistence, config, syncConfig = {}, hooks = {}, transport, debounceMs = 200, maxRetries = 5, } = options;
23
+ // Track sync modes per collection (default to 'auto')
24
+ const collectionSyncModes = new Map();
25
+ // Initialize from syncConfig
26
+ for (const [collection, mode] of Object.entries(syncConfig)) {
27
+ collectionSyncModes.set(collection, mode);
28
+ }
29
+ // Sync mode change callbacks
30
+ const syncModeChangeCallbacks = new Set();
31
+ const beforeSyncCallbacks = [];
32
+ const afterSyncCallbacks = [];
33
+ const syncErrorCallbacks = [];
34
+ const syncRejectedCallbacks = [];
35
+ // Add initial hooks from config
36
+ if (hooks.onBeforeSync) {
37
+ beforeSyncCallbacks.push(hooks.onBeforeSync);
38
+ }
39
+ if (hooks.onAfterSync) {
40
+ afterSyncCallbacks.push(hooks.onAfterSync);
41
+ }
42
+ if (hooks.onSyncError) {
43
+ syncErrorCallbacks.push((context) => hooks.onSyncError(context.collection, context.documentId, context.error));
44
+ }
45
+ if (hooks.onSyncRejected) {
46
+ syncRejectedCallbacks.push((context) => {
47
+ // Convert to the old SyncError format expected by the config hook
48
+ const syncError = {
49
+ code: context.error.code,
50
+ message: context.error.message,
51
+ field: context.error.field,
52
+ };
53
+ return hooks.onSyncRejected(context.collection, context.documentId, syncError);
54
+ });
55
+ }
56
+ // Track collections and document actors
57
+ const collections = new Map();
58
+ const documentActors = new Map();
59
+ // Track pending changes per collection for manual mode
60
+ const pendingChanges = new Map(); // collection -> Set<docId>
61
+ // Store updates for manual mode collections (queued but not sent to actor until flush)
62
+ const manualModeUpdates = new Map(); // collection -> (docId -> updates[])
63
+ // Pause state
64
+ let syncPaused = false;
65
+ // Track retry counts per document for hook context
66
+ const retryCounters = new Map();
67
+ // Helper to call before sync hooks
68
+ async function callBeforeSyncHooks(context) {
69
+ for (const callback of beforeSyncCallbacks) {
70
+ try {
71
+ const result = await callback(context);
72
+ if (result === false) {
73
+ return false; // Cancel sync
74
+ }
75
+ }
76
+ catch (err) {
77
+ console.error('Error in onBeforeSync hook:', err);
78
+ // Continue with sync on hook error
79
+ }
80
+ }
81
+ return true; // Proceed with sync
82
+ }
83
+ // Helper to call after sync hooks
84
+ async function callAfterSyncHooks(context) {
85
+ for (const callback of afterSyncCallbacks) {
86
+ try {
87
+ await callback(context);
88
+ }
89
+ catch (err) {
90
+ console.error('Error in onAfterSync hook:', err);
91
+ }
92
+ }
93
+ }
94
+ // Helper to call sync error hooks
95
+ async function callSyncErrorHooks(context) {
96
+ for (const callback of syncErrorCallbacks) {
97
+ try {
98
+ await callback(context);
99
+ }
100
+ catch (err) {
101
+ console.error('Error in onSyncError hook:', err);
102
+ }
103
+ }
104
+ }
105
+ // Helper to call sync rejected hooks
106
+ async function callSyncRejectedHooks(context) {
107
+ for (const callback of syncRejectedCallbacks) {
108
+ try {
109
+ await callback(context);
110
+ }
111
+ catch (err) {
112
+ console.error('Error in onSyncRejected hook:', err);
113
+ }
114
+ }
115
+ }
116
+ // Track documents where sync was cancelled by beforeSync hook (to skip afterSync)
117
+ const syncCancelledByHook = new Set();
118
+ // Create a transport wrapper that intercepts sendBatch to call before sync hooks
119
+ const wrappedTransport = transport ? createHookTransportWrapper(transport) : undefined;
120
+ function createHookTransportWrapper(baseTransport) {
121
+ return {
122
+ ...baseTransport,
123
+ getState: () => baseTransport.getState(),
124
+ getAppId: () => baseTransport.getAppId(),
125
+ getEdgeId: () => baseTransport.getEdgeId(),
126
+ onStateChange: (cb) => baseTransport.onStateChange(cb),
127
+ onUpdate: (cb) => baseTransport.onUpdate(cb),
128
+ onError: (cb) => baseTransport.onError(cb),
129
+ connect: () => baseTransport.connect(),
130
+ disconnect: () => baseTransport.disconnect(),
131
+ publish: (collection, changes) => baseTransport.publish(collection, changes),
132
+ exchangeStateVector: (collection, docId, localVector) => baseTransport.exchangeStateVector(collection, docId, localVector),
133
+ requestSnapshot: (collection, docId) => baseTransport.requestSnapshot(collection, docId),
134
+ forcePush: (collection, docId, stateVector, changes) => baseTransport.forcePush(collection, docId, stateVector, changes),
135
+ async sendBatch(collection, changes) {
136
+ // Call before sync hooks for each document in the batch
137
+ const filteredChanges = [];
138
+ const cancelledDocIds = new Set();
139
+ for (const change of changes) {
140
+ const shouldProceed = await callBeforeSyncHooks({
141
+ collection: change.collection,
142
+ documentId: change.docId,
143
+ changes: change.data,
144
+ });
145
+ if (shouldProceed) {
146
+ filteredChanges.push(change);
147
+ }
148
+ else {
149
+ cancelledDocIds.add(change.docId);
150
+ // Track cancelled syncs so we skip afterSync hooks
151
+ syncCancelledByHook.add(`${change.collection}:${change.docId}`);
152
+ }
153
+ }
154
+ // If all changes were cancelled, return empty success responses
155
+ if (filteredChanges.length === 0) {
156
+ return changes.map(c => ({
157
+ success: true,
158
+ docId: c.docId,
159
+ collection: c.collection,
160
+ }));
161
+ }
162
+ // Send only the non-cancelled changes
163
+ const responses = await baseTransport.sendBatch(collection, filteredChanges);
164
+ // Merge cancelled and actual responses
165
+ const responseMap = new Map();
166
+ for (const resp of responses) {
167
+ if (resp.docId) {
168
+ responseMap.set(resp.docId, resp);
169
+ }
170
+ }
171
+ return changes.map(c => {
172
+ if (cancelledDocIds.has(c.docId)) {
173
+ return { success: true, docId: c.docId, collection: c.collection };
174
+ }
175
+ return responseMap.get(c.docId) ?? { success: true, docId: c.docId, collection: c.collection };
176
+ });
177
+ },
178
+ };
179
+ }
180
+ // Create sync engine with wrapped transport
181
+ const syncEngine = createSyncEngine({
182
+ serverUrl: config.serverUrl,
183
+ apiKey: config.apiKey,
184
+ debounceMs,
185
+ maxRetries,
186
+ persistence,
187
+ transport: wrappedTransport,
188
+ });
189
+ // Subscribe to sync engine events to call lifecycle hooks
190
+ syncEngine.onSyncError(async (event) => {
191
+ const key = `${event.collection}:${event.documentId}`;
192
+ const retryCount = retryCounters.get(key) ?? 0;
193
+ const willRetry = retryCount < maxRetries;
194
+ await callSyncErrorHooks({
195
+ collection: event.collection,
196
+ documentId: event.documentId,
197
+ error: event.error,
198
+ retryCount,
199
+ willRetry,
200
+ });
201
+ // Update retry counter
202
+ retryCounters.set(key, retryCount + 1);
203
+ });
204
+ syncEngine.onSyncRejected(async (event) => {
205
+ await callSyncRejectedHooks({
206
+ collection: event.collection,
207
+ documentId: event.documentId,
208
+ error: {
209
+ code: event.error.code,
210
+ message: event.error.message,
211
+ field: event.error.field,
212
+ },
213
+ });
214
+ });
215
+ syncEngine.onSyncComplete(async (event) => {
216
+ const key = `${event.collection}:${event.documentId}`;
217
+ // Skip after sync hooks if sync was cancelled by beforeSync
218
+ if (syncCancelledByHook.has(key)) {
219
+ syncCancelledByHook.delete(key);
220
+ return;
221
+ }
222
+ // Reset retry counter on success
223
+ retryCounters.delete(key);
224
+ await callAfterSyncHooks({
225
+ collection: event.collection,
226
+ documentId: event.documentId,
227
+ success: true,
228
+ timestamp: Date.now(),
229
+ });
230
+ });
231
+ // Connect the sync engine
232
+ await syncEngine.connect();
233
+ // Create a reactive signal for connection state that tracks the sync engine
234
+ const connectionStateSignal = createSignal(syncEngine.getConnectionState());
235
+ // Subscribe to sync engine state changes and update the signal
236
+ syncEngine.onStateChange((state) => {
237
+ connectionStateSignal.set(state);
238
+ });
239
+ // Create a lightweight OfflineEdgeClient wrapper that shares persistence
240
+ // Note: We cast the connectionState signal to match OfflineEdgeClient's expected type
241
+ // Both types have the same runtime values ('online', 'offline', etc.)
242
+ const offlineClient = {
243
+ collection(name) {
244
+ // Reuse the same collection instances
245
+ let col = collections.get(name);
246
+ if (!col) {
247
+ col = createCollection(name, persistence);
248
+ collections.set(name, col);
249
+ }
250
+ return col;
251
+ },
252
+ connectionState: connectionStateSignal,
253
+ isSyncing(_name) {
254
+ // All collections sync by default in SyncedOfflineEdge
255
+ return true;
256
+ },
257
+ syncCollection(_name) {
258
+ // No-op: all collections sync by default
259
+ },
260
+ async unsyncCollection(_name) {
261
+ // No-op: not supported in SyncedOfflineEdge
262
+ },
263
+ getSyncedCollections() {
264
+ return Array.from(collections.keys());
265
+ },
266
+ onSyncCollectionsChange(_callback) {
267
+ // No-op: returns empty unsubscribe since all collections always sync
268
+ return () => { };
269
+ },
270
+ };
271
+ /**
272
+ * Get sync mode for a collection.
273
+ * Returns 'auto' if not explicitly configured.
274
+ */
275
+ function getSyncMode(collection) {
276
+ return collectionSyncModes.get(collection) ?? 'auto';
277
+ }
278
+ /**
279
+ * Set sync mode for a collection.
280
+ * Emits event if mode actually changed.
281
+ */
282
+ function setSyncMode(collection, mode) {
283
+ const currentMode = getSyncMode(collection);
284
+ if (currentMode === mode) {
285
+ return; // No change, don't emit event
286
+ }
287
+ collectionSyncModes.set(collection, mode);
288
+ // Emit change event
289
+ for (const callback of syncModeChangeCallbacks) {
290
+ callback(collection, mode);
291
+ }
292
+ // If switching to auto and not paused, flush any stored manual mode updates
293
+ if (mode === 'auto' && !syncPaused) {
294
+ const collectionUpdates = manualModeUpdates.get(collection);
295
+ if (collectionUpdates && collectionUpdates.size > 0) {
296
+ // Queue all stored updates to actors (async, but we don't need to wait)
297
+ (async () => {
298
+ for (const [docId, updates] of collectionUpdates) {
299
+ const actor = await getDocumentActor(collection, docId);
300
+ for (const update of updates) {
301
+ actor.queueChange(update);
302
+ }
303
+ }
304
+ collectionUpdates.clear();
305
+ })();
306
+ }
307
+ pendingChanges.delete(collection);
308
+ }
309
+ }
310
+ /**
311
+ * Subscribe to sync mode change events.
312
+ */
313
+ function onSyncModeChange(callback) {
314
+ syncModeChangeCallbacks.add(callback);
315
+ return () => {
316
+ syncModeChangeCallbacks.delete(callback);
317
+ };
318
+ }
319
+ /**
320
+ * Get or create a document actor for a specific document.
321
+ */
322
+ async function getDocumentActor(collectionName, docId) {
323
+ const key = `${collectionName}:${docId}`;
324
+ let actor = documentActors.get(key);
325
+ if (!actor) {
326
+ actor = await syncEngine.registerDocument(collectionName, docId);
327
+ documentActors.set(key, actor);
328
+ }
329
+ return actor;
330
+ }
331
+ /**
332
+ * Queue a change for sync based on collection's sync mode.
333
+ *
334
+ * The key insight here is:
335
+ * - Auto mode: Queue change to actor (triggers debounced sync automatically)
336
+ * - Manual mode: Store pending change info but DON'T queue to actor until flush
337
+ * - Disabled mode: Don't queue at all
338
+ */
339
+ async function queueChangeForSync(collectionName, docId, update) {
340
+ const mode = getSyncMode(collectionName);
341
+ if (mode === 'disabled') {
342
+ // Don't sync at all - changes are local only
343
+ // Don't even register the actor or queue anything
344
+ return;
345
+ }
346
+ if (mode === 'manual') {
347
+ // Track that this document has pending changes, but don't queue to actor yet
348
+ // This prevents the debounce timer from triggering automatic sync
349
+ let pending = pendingChanges.get(collectionName);
350
+ if (!pending) {
351
+ pending = new Set();
352
+ pendingChanges.set(collectionName, pending);
353
+ }
354
+ pending.add(docId);
355
+ // Store the update for later - we need to keep track of updates per document
356
+ // For now, we'll queue to actor when syncCollection is called
357
+ // The actor will hold the update, but we won't trigger sync
358
+ // Actually - we need to store updates separately for manual mode
359
+ if (!manualModeUpdates.has(collectionName)) {
360
+ manualModeUpdates.set(collectionName, new Map());
361
+ }
362
+ const collectionUpdates = manualModeUpdates.get(collectionName);
363
+ if (!collectionUpdates.has(docId)) {
364
+ collectionUpdates.set(docId, []);
365
+ }
366
+ collectionUpdates.get(docId).push(update);
367
+ return;
368
+ }
369
+ // Auto mode - queue change which will trigger debounced sync
370
+ if (!syncPaused) {
371
+ const actor = await getDocumentActor(collectionName, docId);
372
+ actor.queueChange(update);
373
+ }
374
+ else {
375
+ // When paused, track pending but still queue to actor
376
+ let pending = pendingChanges.get(collectionName);
377
+ if (!pending) {
378
+ pending = new Set();
379
+ pendingChanges.set(collectionName, pending);
380
+ }
381
+ pending.add(docId);
382
+ const actor = await getDocumentActor(collectionName, docId);
383
+ actor.queueChange(update);
384
+ }
385
+ }
386
+ /**
387
+ * Sync a specific collection immediately.
388
+ */
389
+ async function syncCollection(collectionName) {
390
+ const mode = getSyncMode(collectionName);
391
+ if (mode === 'disabled') {
392
+ // Don't sync disabled collections even when explicitly requested
393
+ return;
394
+ }
395
+ // For manual mode, first flush all stored updates to actors
396
+ const collectionUpdates = manualModeUpdates.get(collectionName);
397
+ if (collectionUpdates && collectionUpdates.size > 0) {
398
+ for (const [docId, updates] of collectionUpdates) {
399
+ const actor = await getDocumentActor(collectionName, docId);
400
+ for (const update of updates) {
401
+ actor.queueChange(update);
402
+ }
403
+ }
404
+ // Clear the stored updates after flushing
405
+ collectionUpdates.clear();
406
+ }
407
+ // Now trigger sync for all actors with pending changes
408
+ for (const [key, actor] of documentActors) {
409
+ if (key.startsWith(`${collectionName}:`)) {
410
+ if (actor.hasPendingChanges()) {
411
+ // Trigger immediate sync by resetting retries
412
+ const docId = key.split(':')[1];
413
+ syncEngine.resetRetries(collectionName, docId);
414
+ }
415
+ }
416
+ }
417
+ // Clear pending tracking for this collection
418
+ pendingChanges.delete(collectionName);
419
+ }
420
+ /**
421
+ * Sync a specific document immediately.
422
+ */
423
+ async function syncDocument(collectionName, docId) {
424
+ const mode = getSyncMode(collectionName);
425
+ if (mode === 'disabled') {
426
+ return;
427
+ }
428
+ const key = `${collectionName}:${docId}`;
429
+ const actor = documentActors.get(key);
430
+ if (actor && actor.hasPendingChanges()) {
431
+ syncEngine.resetRetries(collectionName, docId);
432
+ }
433
+ // Remove from pending
434
+ const pending = pendingChanges.get(collectionName);
435
+ if (pending) {
436
+ pending.delete(docId);
437
+ }
438
+ }
439
+ /**
440
+ * Sync all pending changes immediately.
441
+ */
442
+ async function syncNow() {
443
+ // First, flush all manual mode updates to actors
444
+ for (const [collectionName, collectionUpdates] of manualModeUpdates) {
445
+ const mode = getSyncMode(collectionName);
446
+ if (mode !== 'disabled' && collectionUpdates.size > 0) {
447
+ for (const [docId, updates] of collectionUpdates) {
448
+ const actor = await getDocumentActor(collectionName, docId);
449
+ for (const update of updates) {
450
+ actor.queueChange(update);
451
+ }
452
+ }
453
+ collectionUpdates.clear();
454
+ }
455
+ }
456
+ // Now trigger sync for all actors with pending changes
457
+ for (const [key, actor] of documentActors) {
458
+ const [collection, docId] = key.split(':');
459
+ const mode = getSyncMode(collection);
460
+ if (mode !== 'disabled' && actor.hasPendingChanges()) {
461
+ syncEngine.resetRetries(collection, docId);
462
+ }
463
+ }
464
+ // Clear all pending tracking
465
+ pendingChanges.clear();
466
+ }
467
+ /**
468
+ * Create a synced collection wrapper.
469
+ */
470
+ function createSyncedCollectionWrapper(name) {
471
+ // Get or create the base collection
472
+ let baseCollection = collections.get(name);
473
+ if (!baseCollection) {
474
+ baseCollection = createCollection(name, persistence);
475
+ collections.set(name, baseCollection);
476
+ }
477
+ const syncedCollection = {
478
+ name,
479
+ async insert(data, insertOptions) {
480
+ const doc = await baseCollection.insert(data, insertOptions);
481
+ // Queue for sync based on mode
482
+ const update = await baseCollection.getYjsUpdate(doc.id);
483
+ if (update) {
484
+ await queueChangeForSync(name, doc.id, update);
485
+ }
486
+ return doc;
487
+ },
488
+ async update(id, data, updateOptions) {
489
+ const doc = await baseCollection.update(id, data, updateOptions);
490
+ // Queue for sync based on mode
491
+ const update = await baseCollection.getYjsUpdate(id);
492
+ if (update) {
493
+ await queueChangeForSync(name, id, update);
494
+ }
495
+ return doc;
496
+ },
497
+ async delete(id) {
498
+ // Get the actor before deleting
499
+ const actor = await getDocumentActor(name, id);
500
+ // Delete locally
501
+ await baseCollection.delete(id);
502
+ // Create and queue tombstone delta
503
+ const mode = getSyncMode(name);
504
+ if (mode !== 'disabled') {
505
+ const tombstoneDoc = new YjsDocument(id);
506
+ tombstoneDoc.set('_collection', name);
507
+ tombstoneDoc.set('syncId', id);
508
+ tombstoneDoc.set('_deleted', true);
509
+ tombstoneDoc.set('_deletedAt', Date.now());
510
+ const tombstoneUpdate = tombstoneDoc.encodeStateAsUpdate();
511
+ tombstoneDoc.destroy();
512
+ await queueChangeForSync(name, id, tombstoneUpdate);
513
+ }
514
+ },
515
+ async get(id) {
516
+ return baseCollection.get(id);
517
+ },
518
+ async list() {
519
+ return baseCollection.list();
520
+ },
521
+ async query(queryOptions) {
522
+ return baseCollection.query(queryOptions);
523
+ },
524
+ subscribe(callback) {
525
+ return baseCollection.subscribe(callback);
526
+ },
527
+ onSyncRejected(callback) {
528
+ return baseCollection.onSyncRejected(callback);
529
+ },
530
+ async getSyncStatus(id) {
531
+ return baseCollection.getSyncStatus(id);
532
+ },
533
+ async retrySync(id) {
534
+ return baseCollection.retrySync(id);
535
+ },
536
+ async getPendingSyncCount() {
537
+ let count = 0;
538
+ for (const [key, actor] of documentActors) {
539
+ if (key.startsWith(`${name}:`) && actor.hasPendingChanges()) {
540
+ count++;
541
+ }
542
+ }
543
+ return count;
544
+ },
545
+ };
546
+ return syncedCollection;
547
+ }
548
+ // Build and return the SyncedOfflineEdge instance
549
+ const edge = {
550
+ // Collection Access
551
+ collection(name) {
552
+ return createSyncedCollectionWrapper(name);
553
+ },
554
+ // Raw Client Access
555
+ getOfflineClient() {
556
+ return offlineClient;
557
+ },
558
+ getSyncEngine() {
559
+ return syncEngine;
560
+ },
561
+ // Connection State Signal
562
+ connectionState: connectionStateSignal,
563
+ // Connection State Methods
564
+ getConnectionState() {
565
+ return syncEngine.getConnectionState();
566
+ },
567
+ isConnected() {
568
+ return syncEngine.getConnectionState() === 'online';
569
+ },
570
+ onConnectionChange(callback) {
571
+ return syncEngine.onStateChange(callback);
572
+ },
573
+ // Sync Mode Configuration
574
+ getSyncMode,
575
+ setSyncMode,
576
+ onSyncModeChange,
577
+ // Lifecycle Hooks
578
+ addOnBeforeSync(callback) {
579
+ beforeSyncCallbacks.push(callback);
580
+ },
581
+ removeOnBeforeSync(callback) {
582
+ const index = beforeSyncCallbacks.indexOf(callback);
583
+ if (index !== -1) {
584
+ beforeSyncCallbacks.splice(index, 1);
585
+ }
586
+ },
587
+ addOnAfterSync(callback) {
588
+ afterSyncCallbacks.push(callback);
589
+ },
590
+ removeOnAfterSync(callback) {
591
+ const index = afterSyncCallbacks.indexOf(callback);
592
+ if (index !== -1) {
593
+ afterSyncCallbacks.splice(index, 1);
594
+ }
595
+ },
596
+ addOnSyncError(callback) {
597
+ syncErrorCallbacks.push(callback);
598
+ },
599
+ removeOnSyncError(callback) {
600
+ const index = syncErrorCallbacks.indexOf(callback);
601
+ if (index !== -1) {
602
+ syncErrorCallbacks.splice(index, 1);
603
+ }
604
+ },
605
+ addOnSyncRejected(callback) {
606
+ syncRejectedCallbacks.push(callback);
607
+ },
608
+ removeOnSyncRejected(callback) {
609
+ const index = syncRejectedCallbacks.indexOf(callback);
610
+ if (index !== -1) {
611
+ syncRejectedCallbacks.splice(index, 1);
612
+ }
613
+ },
614
+ // Manual Sync Control
615
+ async flush(collection) {
616
+ // Temporarily enable sync engine if paused (manual flush should still work)
617
+ const wasPaused = syncPaused;
618
+ if (wasPaused) {
619
+ syncEngine.goOnline();
620
+ }
621
+ try {
622
+ if (collection) {
623
+ await syncCollection(collection);
624
+ }
625
+ else {
626
+ await syncNow();
627
+ }
628
+ // Wait for the sync to actually complete
629
+ await new Promise(resolve => setTimeout(resolve, 50));
630
+ }
631
+ finally {
632
+ // Restore paused state if it was paused
633
+ if (wasPaused) {
634
+ syncEngine.goOffline();
635
+ }
636
+ }
637
+ },
638
+ syncNow,
639
+ syncCollection,
640
+ syncDocument,
641
+ // Sync Pause/Resume
642
+ pause() {
643
+ syncPaused = true;
644
+ syncEngine.goOffline();
645
+ },
646
+ resume() {
647
+ syncPaused = false;
648
+ syncEngine.goOnline();
649
+ // Flush auto-mode collections
650
+ for (const [collectionName, pending] of pendingChanges) {
651
+ const mode = getSyncMode(collectionName);
652
+ if (mode === 'auto' && pending.size > 0) {
653
+ syncCollection(collectionName);
654
+ }
655
+ }
656
+ },
657
+ isPaused() {
658
+ return syncPaused;
659
+ },
660
+ // Deprecated aliases - delegate to new methods
661
+ pauseSync() {
662
+ edge.pause();
663
+ },
664
+ resumeSync() {
665
+ edge.resume();
666
+ },
667
+ isSyncPaused() {
668
+ return edge.isPaused();
669
+ },
670
+ // Manual Offline Control (delegates to SyncEngine)
671
+ goOffline() {
672
+ syncEngine.goOffline();
673
+ },
674
+ goOnline() {
675
+ syncEngine.goOnline();
676
+ },
677
+ // Pending Changes
678
+ hasPendingChanges() {
679
+ for (const actor of documentActors.values()) {
680
+ if (actor.hasPendingChanges()) {
681
+ return true;
682
+ }
683
+ }
684
+ return false;
685
+ },
686
+ async getPendingCount(collectionName) {
687
+ let count = 0;
688
+ for (const [key, actor] of documentActors) {
689
+ if (collectionName) {
690
+ if (key.startsWith(`${collectionName}:`) && actor.hasPendingChanges()) {
691
+ count++;
692
+ }
693
+ }
694
+ else {
695
+ if (actor.hasPendingChanges()) {
696
+ count++;
697
+ }
698
+ }
699
+ }
700
+ return count;
701
+ },
702
+ // Lifecycle
703
+ async destroy() {
704
+ // Unregister all document actors
705
+ for (const [key] of documentActors) {
706
+ const [collection, docId] = key.split(':');
707
+ await syncEngine.unregisterDocument(collection, docId);
708
+ }
709
+ documentActors.clear();
710
+ collections.clear();
711
+ collectionSyncModes.clear();
712
+ syncModeChangeCallbacks.clear();
713
+ pendingChanges.clear();
714
+ manualModeUpdates.clear();
715
+ beforeSyncCallbacks.length = 0;
716
+ afterSyncCallbacks.length = 0;
717
+ syncErrorCallbacks.length = 0;
718
+ syncRejectedCallbacks.length = 0;
719
+ syncCancelledByHook.clear();
720
+ retryCounters.clear();
721
+ // Destroy sync engine
722
+ await syncEngine.destroy();
723
+ // Close persistence if it has a close method
724
+ if (persistence.close) {
725
+ await persistence.close();
726
+ }
727
+ },
728
+ };
729
+ return edge;
730
+ }
731
+ //# sourceMappingURL=synced-offline-edge.js.map