@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.
- package/README.md +312 -0
- package/dist/client/attachment.d.ts +225 -0
- package/dist/client/attachment.d.ts.map +1 -0
- package/dist/client/attachment.js +402 -0
- package/dist/client/attachment.js.map +1 -0
- package/dist/client/binary-encoding.d.ts +45 -0
- package/dist/client/binary-encoding.d.ts.map +1 -0
- package/dist/client/binary-encoding.js +90 -0
- package/dist/client/binary-encoding.js.map +1 -0
- package/dist/client/collection.d.ts +10 -0
- package/dist/client/collection.d.ts.map +1 -0
- package/dist/client/collection.js +924 -0
- package/dist/client/collection.js.map +1 -0
- package/dist/client/compression.d.ts +56 -0
- package/dist/client/compression.d.ts.map +1 -0
- package/dist/client/compression.js +173 -0
- package/dist/client/compression.js.map +1 -0
- package/dist/client/crdt/index.d.ts +2 -0
- package/dist/client/crdt/index.d.ts.map +1 -0
- package/dist/client/crdt/index.js +2 -0
- package/dist/client/crdt/index.js.map +1 -0
- package/dist/client/crdt/yjs-doc.d.ts +88 -0
- package/dist/client/crdt/yjs-doc.d.ts.map +1 -0
- package/dist/client/crdt/yjs-doc.js +123 -0
- package/dist/client/crdt/yjs-doc.js.map +1 -0
- package/dist/client/index.d.ts +66 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +233 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/mock-transport.d.ts +155 -0
- package/dist/client/mock-transport.d.ts.map +1 -0
- package/dist/client/mock-transport.js +292 -0
- package/dist/client/mock-transport.js.map +1 -0
- package/dist/client/network-detector.d.ts +65 -0
- package/dist/client/network-detector.d.ts.map +1 -0
- package/dist/client/network-detector.js +147 -0
- package/dist/client/network-detector.js.map +1 -0
- package/dist/client/provisioning.d.ts +126 -0
- package/dist/client/provisioning.d.ts.map +1 -0
- package/dist/client/provisioning.js +125 -0
- package/dist/client/provisioning.js.map +1 -0
- package/dist/client/signal.d.ts +13 -0
- package/dist/client/signal.d.ts.map +1 -0
- package/dist/client/signal.js +27 -0
- package/dist/client/signal.js.map +1 -0
- package/dist/client/sync-engine.d.ts +298 -0
- package/dist/client/sync-engine.d.ts.map +1 -0
- package/dist/client/sync-engine.js +904 -0
- package/dist/client/sync-engine.js.map +1 -0
- package/dist/client/synced-edge.d.ts +109 -0
- package/dist/client/synced-edge.d.ts.map +1 -0
- package/dist/client/synced-edge.js +179 -0
- package/dist/client/synced-edge.js.map +1 -0
- package/dist/client/synced-offline-edge-types.d.ts +540 -0
- package/dist/client/synced-offline-edge-types.d.ts.map +1 -0
- package/dist/client/synced-offline-edge-types.js +10 -0
- package/dist/client/synced-offline-edge-types.js.map +1 -0
- package/dist/client/synced-offline-edge.d.ts +54 -0
- package/dist/client/synced-offline-edge.d.ts.map +1 -0
- package/dist/client/synced-offline-edge.js +731 -0
- package/dist/client/synced-offline-edge.js.map +1 -0
- package/dist/client/transport.d.ts +202 -0
- package/dist/client/transport.d.ts.map +1 -0
- package/dist/client/transport.js +409 -0
- package/dist/client/transport.js.map +1 -0
- package/dist/client/types.d.ts +622 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +60 -0
- package/dist/client/types.js.map +1 -0
- package/dist/client/validation.d.ts +61 -0
- package/dist/client/validation.d.ts.map +1 -0
- package/dist/client/validation.js +57 -0
- package/dist/client/validation.js.map +1 -0
- package/dist/client/versioning.d.ts +134 -0
- package/dist/client/versioning.d.ts.map +1 -0
- package/dist/client/versioning.js +304 -0
- package/dist/client/versioning.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/encryption.d.ts +114 -0
- package/dist/persistence/encryption.d.ts.map +1 -0
- package/dist/persistence/encryption.js +286 -0
- package/dist/persistence/encryption.js.map +1 -0
- package/dist/persistence/index.d.ts +21 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +20 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/persistence/memory.d.ts +32 -0
- package/dist/persistence/memory.d.ts.map +1 -0
- package/dist/persistence/memory.js +57 -0
- package/dist/persistence/memory.js.map +1 -0
- package/dist/persistence/migrations.d.ts +106 -0
- package/dist/persistence/migrations.d.ts.map +1 -0
- package/dist/persistence/migrations.js +176 -0
- package/dist/persistence/migrations.js.map +1 -0
- package/dist/persistence/pending-queue.d.ts +109 -0
- package/dist/persistence/pending-queue.d.ts.map +1 -0
- package/dist/persistence/pending-queue.js +249 -0
- package/dist/persistence/pending-queue.js.map +1 -0
- package/dist/persistence/pglite.d.ts +72 -0
- package/dist/persistence/pglite.d.ts.map +1 -0
- package/dist/persistence/pglite.js +126 -0
- package/dist/persistence/pglite.js.map +1 -0
- package/dist/persistence/quota-manager.d.ts +134 -0
- package/dist/persistence/quota-manager.d.ts.map +1 -0
- package/dist/persistence/quota-manager.js +242 -0
- package/dist/persistence/quota-manager.js.map +1 -0
- package/dist/persistence/types.d.ts +54 -0
- package/dist/persistence/types.d.ts.map +1 -0
- package/dist/persistence/types.js +2 -0
- package/dist/persistence/types.js.map +1 -0
- package/dist/react/OfflineEdgeProvider.d.ts +91 -0
- package/dist/react/OfflineEdgeProvider.d.ts.map +1 -0
- package/dist/react/OfflineEdgeProvider.js +127 -0
- package/dist/react/OfflineEdgeProvider.js.map +1 -0
- package/dist/react/SyncedOfflineEdgeProvider.d.ts +105 -0
- package/dist/react/SyncedOfflineEdgeProvider.d.ts.map +1 -0
- package/dist/react/SyncedOfflineEdgeProvider.js +138 -0
- package/dist/react/SyncedOfflineEdgeProvider.js.map +1 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +51 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/useCollection.d.ts +77 -0
- package/dist/react/useCollection.d.ts.map +1 -0
- package/dist/react/useCollection.js +113 -0
- package/dist/react/useCollection.js.map +1 -0
- package/dist/react/useCollectionSyncMode.d.ts +61 -0
- package/dist/react/useCollectionSyncMode.d.ts.map +1 -0
- package/dist/react/useCollectionSyncMode.js +93 -0
- package/dist/react/useCollectionSyncMode.js.map +1 -0
- package/dist/react/useConnectionState.d.ts +44 -0
- package/dist/react/useConnectionState.d.ts.map +1 -0
- package/dist/react/useConnectionState.js +46 -0
- package/dist/react/useConnectionState.js.map +1 -0
- package/dist/react/useDocumentSyncStatus.d.ts +72 -0
- package/dist/react/useDocumentSyncStatus.d.ts.map +1 -0
- package/dist/react/useDocumentSyncStatus.js +110 -0
- package/dist/react/useDocumentSyncStatus.js.map +1 -0
- package/dist/react/useOfflineEdge.d.ts +58 -0
- package/dist/react/useOfflineEdge.d.ts.map +1 -0
- package/dist/react/useOfflineEdge.js +54 -0
- package/dist/react/useOfflineEdge.js.map +1 -0
- package/dist/react/usePendingChanges.d.ts +67 -0
- package/dist/react/usePendingChanges.d.ts.map +1 -0
- package/dist/react/usePendingChanges.js +90 -0
- package/dist/react/usePendingChanges.js.map +1 -0
- package/dist/react/useRejectedDocuments.d.ts +112 -0
- package/dist/react/useRejectedDocuments.d.ts.map +1 -0
- package/dist/react/useRejectedDocuments.js +213 -0
- package/dist/react/useRejectedDocuments.js.map +1 -0
- package/dist/react/useSyncControls.d.ts +96 -0
- package/dist/react/useSyncControls.d.ts.map +1 -0
- package/dist/react/useSyncControls.js +112 -0
- package/dist/react/useSyncControls.js.map +1 -0
- package/dist/react/useSyncProgress.d.ts +78 -0
- package/dist/react/useSyncProgress.d.ts.map +1 -0
- package/dist/react/useSyncProgress.js +90 -0
- package/dist/react/useSyncProgress.js.map +1 -0
- package/dist/react/useSyncRejected.d.ts +47 -0
- package/dist/react/useSyncRejected.d.ts.map +1 -0
- package/dist/react/useSyncRejected.js +55 -0
- package/dist/react/useSyncRejected.js.map +1 -0
- package/dist/react/useSyncStatus.d.ts +56 -0
- package/dist/react/useSyncStatus.d.ts.map +1 -0
- package/dist/react/useSyncStatus.js +59 -0
- package/dist/react/useSyncStatus.js.map +1 -0
- package/dist/react/useSyncedOfflineEdge.d.ts +69 -0
- package/dist/react/useSyncedOfflineEdge.d.ts.map +1 -0
- package/dist/react/useSyncedOfflineEdge.js +65 -0
- package/dist/react/useSyncedOfflineEdge.js.map +1 -0
- package/dist/service-worker/index.d.ts +7 -0
- package/dist/service-worker/index.d.ts.map +1 -0
- package/dist/service-worker/index.js +7 -0
- package/dist/service-worker/index.js.map +1 -0
- package/dist/service-worker/sync-worker.d.ts +230 -0
- package/dist/service-worker/sync-worker.d.ts.map +1 -0
- package/dist/service-worker/sync-worker.js +471 -0
- package/dist/service-worker/sync-worker.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- 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
|