@abloatai/ablo 0.3.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/CHANGELOG.md +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear Sync Engine - Sync Action Store
|
|
3
|
+
*
|
|
4
|
+
* Stores and manages sync actions received from the server.
|
|
5
|
+
* Critical for delta sync and maintaining sync state consistency.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* SyncActionStore - Manages sync actions (deltas)
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Stores sync actions by ID for replay
|
|
12
|
+
* - Tracks applied vs pending actions
|
|
13
|
+
* - Enables rewind/replay for conflict resolution
|
|
14
|
+
* - Maintains sync watermark
|
|
15
|
+
*/
|
|
16
|
+
export class SyncActionStore {
|
|
17
|
+
db;
|
|
18
|
+
storeName = 'sync_action_table';
|
|
19
|
+
lastAppliedSyncId = 0;
|
|
20
|
+
pendingActions = new Map();
|
|
21
|
+
constructor(db) {
|
|
22
|
+
this.db = db;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Initialize store (create if needed)
|
|
26
|
+
*/
|
|
27
|
+
async initialize() {
|
|
28
|
+
// Store is created during database migration
|
|
29
|
+
// Load last applied sync ID from metadata
|
|
30
|
+
const metadata = await this.getMetadata();
|
|
31
|
+
if (metadata) {
|
|
32
|
+
this.lastAppliedSyncId = metadata.lastSyncId || 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Store a sync action
|
|
37
|
+
*/
|
|
38
|
+
async storeSyncAction(action) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
41
|
+
const store = tx.objectStore(this.storeName);
|
|
42
|
+
const request = store.put({
|
|
43
|
+
...action,
|
|
44
|
+
storedAt: Date.now(),
|
|
45
|
+
applied: false,
|
|
46
|
+
});
|
|
47
|
+
tx.oncomplete = () => {
|
|
48
|
+
// Add to pending if not yet applied
|
|
49
|
+
if (action.id > this.lastAppliedSyncId) {
|
|
50
|
+
this.pendingActions.set(action.id, action);
|
|
51
|
+
}
|
|
52
|
+
resolve();
|
|
53
|
+
};
|
|
54
|
+
tx.onerror = () => reject(tx.error);
|
|
55
|
+
request.onerror = () => reject(request.error);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Store multiple sync actions
|
|
60
|
+
*/
|
|
61
|
+
async storeSyncActions(actions) {
|
|
62
|
+
if (actions.length === 0)
|
|
63
|
+
return;
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
66
|
+
const store = tx.objectStore(this.storeName);
|
|
67
|
+
let completed = 0;
|
|
68
|
+
const total = actions.length;
|
|
69
|
+
for (const action of actions) {
|
|
70
|
+
const request = store.put({
|
|
71
|
+
...action,
|
|
72
|
+
storedAt: Date.now(),
|
|
73
|
+
applied: false,
|
|
74
|
+
});
|
|
75
|
+
request.onsuccess = () => {
|
|
76
|
+
completed++;
|
|
77
|
+
if (completed === total) {
|
|
78
|
+
// Add to pending
|
|
79
|
+
for (const action of actions) {
|
|
80
|
+
if (action.id > this.lastAppliedSyncId) {
|
|
81
|
+
this.pendingActions.set(action.id, action);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
request.onerror = () => reject(request.error);
|
|
87
|
+
}
|
|
88
|
+
tx.oncomplete = () => resolve();
|
|
89
|
+
tx.onerror = () => reject(tx.error);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get sync action by ID
|
|
94
|
+
*/
|
|
95
|
+
async getSyncAction(id) {
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const tx = this.db.transaction([this.storeName], 'readonly');
|
|
98
|
+
const store = tx.objectStore(this.storeName);
|
|
99
|
+
const request = store.get(id);
|
|
100
|
+
request.onsuccess = () => {
|
|
101
|
+
const data = request.result;
|
|
102
|
+
if (!data) {
|
|
103
|
+
resolve(undefined);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const { storedAt, applied, ...action } = data;
|
|
107
|
+
resolve(action);
|
|
108
|
+
};
|
|
109
|
+
request.onerror = () => reject(request.error);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Get sync actions in range
|
|
114
|
+
*/
|
|
115
|
+
async getSyncActionsInRange(startId, endId) {
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const tx = this.db.transaction([this.storeName], 'readonly');
|
|
118
|
+
const store = tx.objectStore(this.storeName);
|
|
119
|
+
const index = store.index('syncId');
|
|
120
|
+
const range = IDBKeyRange.bound(startId, endId);
|
|
121
|
+
const request = index.getAll(range);
|
|
122
|
+
request.onsuccess = () => {
|
|
123
|
+
const allData = request.result;
|
|
124
|
+
const actions = allData.map(({ storedAt, applied, ...action }) => action);
|
|
125
|
+
resolve(actions);
|
|
126
|
+
};
|
|
127
|
+
request.onerror = () => reject(request.error);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Get pending sync actions (not yet applied)
|
|
132
|
+
*/
|
|
133
|
+
async getPendingSyncActions() {
|
|
134
|
+
if (this.pendingActions.size === 0) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
// Return sorted by sync ID
|
|
138
|
+
const sorted = Array.from(this.pendingActions.values()).sort((a, b) => a.id - b.id);
|
|
139
|
+
return sorted;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Mark sync action as applied
|
|
143
|
+
*/
|
|
144
|
+
async markAsApplied(syncId) {
|
|
145
|
+
return new Promise(async (resolve, reject) => {
|
|
146
|
+
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
147
|
+
const store = tx.objectStore(this.storeName);
|
|
148
|
+
const getRequest = store.get(syncId);
|
|
149
|
+
getRequest.onsuccess = () => {
|
|
150
|
+
const existing = getRequest.result;
|
|
151
|
+
if (existing) {
|
|
152
|
+
existing.applied = true;
|
|
153
|
+
existing.appliedAt = Date.now();
|
|
154
|
+
const putRequest = store.put(existing);
|
|
155
|
+
putRequest.onerror = () => reject(putRequest.error);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
getRequest.onerror = () => reject(getRequest.error);
|
|
159
|
+
tx.oncomplete = async () => {
|
|
160
|
+
// Update tracking
|
|
161
|
+
if (syncId > this.lastAppliedSyncId) {
|
|
162
|
+
this.lastAppliedSyncId = syncId;
|
|
163
|
+
}
|
|
164
|
+
// Remove from pending
|
|
165
|
+
this.pendingActions.delete(syncId);
|
|
166
|
+
// Update metadata
|
|
167
|
+
try {
|
|
168
|
+
await this.updateLastSyncId(syncId);
|
|
169
|
+
resolve();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
reject(error);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
tx.onerror = () => reject(tx.error);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Mark multiple actions as applied
|
|
180
|
+
*/
|
|
181
|
+
async markManyAsApplied(syncIds) {
|
|
182
|
+
if (syncIds.length === 0)
|
|
183
|
+
return;
|
|
184
|
+
return new Promise(async (resolve, reject) => {
|
|
185
|
+
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
186
|
+
const store = tx.objectStore(this.storeName);
|
|
187
|
+
let processed = 0;
|
|
188
|
+
const total = syncIds.length;
|
|
189
|
+
for (const syncId of syncIds) {
|
|
190
|
+
const getRequest = store.get(syncId);
|
|
191
|
+
getRequest.onsuccess = () => {
|
|
192
|
+
const existing = getRequest.result;
|
|
193
|
+
if (existing) {
|
|
194
|
+
existing.applied = true;
|
|
195
|
+
existing.appliedAt = Date.now();
|
|
196
|
+
const putRequest = store.put(existing);
|
|
197
|
+
putRequest.onsuccess = () => {
|
|
198
|
+
processed++;
|
|
199
|
+
// Remove from pending
|
|
200
|
+
this.pendingActions.delete(syncId);
|
|
201
|
+
if (processed === total) {
|
|
202
|
+
// All processed, update last applied ID
|
|
203
|
+
const maxId = Math.max(...syncIds);
|
|
204
|
+
if (maxId > this.lastAppliedSyncId) {
|
|
205
|
+
this.lastAppliedSyncId = maxId;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
putRequest.onerror = () => reject(putRequest.error);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
processed++;
|
|
213
|
+
this.pendingActions.delete(syncId);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
getRequest.onerror = () => reject(getRequest.error);
|
|
217
|
+
}
|
|
218
|
+
tx.oncomplete = async () => {
|
|
219
|
+
// Update metadata with the highest sync ID
|
|
220
|
+
const maxId = Math.max(...syncIds);
|
|
221
|
+
if (maxId > this.lastAppliedSyncId) {
|
|
222
|
+
try {
|
|
223
|
+
await this.updateLastSyncId(maxId);
|
|
224
|
+
resolve();
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
reject(error);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
resolve();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
tx.onerror = () => reject(tx.error);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Get last applied sync ID
|
|
239
|
+
*/
|
|
240
|
+
getLastAppliedSyncId() {
|
|
241
|
+
return this.lastAppliedSyncId;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if we have a gap in sync IDs
|
|
245
|
+
*/
|
|
246
|
+
async hasGap(fromId, toId) {
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const tx = this.db.transaction([this.storeName], 'readonly');
|
|
249
|
+
const store = tx.objectStore(this.storeName);
|
|
250
|
+
const index = store.index('syncId');
|
|
251
|
+
let currentId = fromId;
|
|
252
|
+
let hasGap = false;
|
|
253
|
+
const checkNext = () => {
|
|
254
|
+
if (currentId > toId) {
|
|
255
|
+
resolve(hasGap);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const request = index.get(currentId);
|
|
259
|
+
request.onsuccess = () => {
|
|
260
|
+
if (!request.result) {
|
|
261
|
+
hasGap = true;
|
|
262
|
+
resolve(true);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
currentId++;
|
|
266
|
+
checkNext();
|
|
267
|
+
};
|
|
268
|
+
request.onerror = () => reject(request.error);
|
|
269
|
+
};
|
|
270
|
+
checkNext();
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Get missing sync IDs in range
|
|
275
|
+
*/
|
|
276
|
+
async getMissingSyncIds(fromId, toId) {
|
|
277
|
+
return new Promise((resolve, reject) => {
|
|
278
|
+
const missing = [];
|
|
279
|
+
const tx = this.db.transaction([this.storeName], 'readonly');
|
|
280
|
+
const store = tx.objectStore(this.storeName);
|
|
281
|
+
const index = store.index('syncId');
|
|
282
|
+
let currentId = fromId;
|
|
283
|
+
const checkNext = () => {
|
|
284
|
+
if (currentId > toId) {
|
|
285
|
+
resolve(missing);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const request = index.get(currentId);
|
|
289
|
+
request.onsuccess = () => {
|
|
290
|
+
if (!request.result) {
|
|
291
|
+
missing.push(currentId);
|
|
292
|
+
}
|
|
293
|
+
currentId++;
|
|
294
|
+
checkNext();
|
|
295
|
+
};
|
|
296
|
+
request.onerror = () => reject(request.error);
|
|
297
|
+
};
|
|
298
|
+
checkNext();
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Clean up old sync actions
|
|
303
|
+
*/
|
|
304
|
+
async cleanup(keepDays = 7) {
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1000;
|
|
307
|
+
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
308
|
+
const store = tx.objectStore(this.storeName);
|
|
309
|
+
const request = store.openCursor();
|
|
310
|
+
let cleaned = 0;
|
|
311
|
+
request.onsuccess = (event) => {
|
|
312
|
+
const cursor = event.target.result;
|
|
313
|
+
if (cursor) {
|
|
314
|
+
if (cursor.value.applied && cursor.value.appliedAt < cutoff) {
|
|
315
|
+
const deleteRequest = cursor.delete();
|
|
316
|
+
deleteRequest.onsuccess = () => {
|
|
317
|
+
cleaned++;
|
|
318
|
+
cursor.continue();
|
|
319
|
+
};
|
|
320
|
+
deleteRequest.onerror = () => reject(deleteRequest.error);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
cursor.continue();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
// Cursor finished
|
|
328
|
+
resolve(cleaned);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
request.onerror = () => reject(request.error);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Clear all sync actions
|
|
336
|
+
*/
|
|
337
|
+
async clear() {
|
|
338
|
+
return new Promise((resolve, reject) => {
|
|
339
|
+
const tx = this.db.transaction([this.storeName], 'readwrite');
|
|
340
|
+
const store = tx.objectStore(this.storeName);
|
|
341
|
+
const request = store.clear();
|
|
342
|
+
tx.oncomplete = () => {
|
|
343
|
+
this.pendingActions.clear();
|
|
344
|
+
this.lastAppliedSyncId = 0;
|
|
345
|
+
resolve();
|
|
346
|
+
};
|
|
347
|
+
tx.onerror = () => reject(tx.error);
|
|
348
|
+
request.onerror = () => reject(request.error);
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Get statistics
|
|
353
|
+
*/
|
|
354
|
+
async getStats() {
|
|
355
|
+
return new Promise((resolve, reject) => {
|
|
356
|
+
const tx = this.db.transaction([this.storeName], 'readonly');
|
|
357
|
+
const store = tx.objectStore(this.storeName);
|
|
358
|
+
const request = store.getAll();
|
|
359
|
+
request.onsuccess = () => {
|
|
360
|
+
const allActions = request.result;
|
|
361
|
+
const applied = allActions.filter((a) => a.applied).length;
|
|
362
|
+
const pending = allActions.filter((a) => !a.applied).length;
|
|
363
|
+
const timestamps = allActions
|
|
364
|
+
.map((a) => a.storedAt)
|
|
365
|
+
.filter(Boolean)
|
|
366
|
+
.sort();
|
|
367
|
+
resolve({
|
|
368
|
+
total: allActions.length,
|
|
369
|
+
applied,
|
|
370
|
+
pending,
|
|
371
|
+
lastAppliedId: this.lastAppliedSyncId,
|
|
372
|
+
oldestAction: timestamps.length > 0 ? new Date(timestamps[0]) : null,
|
|
373
|
+
newestAction: timestamps.length > 0 ? new Date(timestamps[timestamps.length - 1]) : null,
|
|
374
|
+
});
|
|
375
|
+
};
|
|
376
|
+
request.onerror = () => reject(request.error);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Update last sync ID in metadata
|
|
381
|
+
*/
|
|
382
|
+
async updateLastSyncId(syncId) {
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
const tx = this.db.transaction(['__meta'], 'readwrite');
|
|
385
|
+
const store = tx.objectStore('__meta');
|
|
386
|
+
const getRequest = store.get('metadata');
|
|
387
|
+
getRequest.onsuccess = () => {
|
|
388
|
+
const metadata = getRequest.result || {};
|
|
389
|
+
metadata.lastSyncId = syncId;
|
|
390
|
+
metadata.updatedAt = new Date();
|
|
391
|
+
const putRequest = store.put(metadata, 'metadata');
|
|
392
|
+
putRequest.onerror = () => reject(putRequest.error);
|
|
393
|
+
};
|
|
394
|
+
getRequest.onerror = () => reject(getRequest.error);
|
|
395
|
+
tx.oncomplete = () => resolve();
|
|
396
|
+
tx.onerror = () => reject(tx.error);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get metadata. Shape mirrors what's written by the database manager
|
|
401
|
+
* — currently only `lastSyncId` is consumed here (in `initialize`),
|
|
402
|
+
* so the return type is narrowed to that read surface. Returns
|
|
403
|
+
* `undefined` when the row hasn't been written yet (fresh DB).
|
|
404
|
+
*/
|
|
405
|
+
async getMetadata() {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const tx = this.db.transaction(['__meta'], 'readonly');
|
|
408
|
+
const store = tx.objectStore('__meta');
|
|
409
|
+
const request = store.get('metadata');
|
|
410
|
+
request.onsuccess = () => {
|
|
411
|
+
const raw = request.result;
|
|
412
|
+
if (raw && typeof raw === 'object' && 'lastSyncId' in raw) {
|
|
413
|
+
resolve(raw);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
resolve(undefined);
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
request.onerror = () => reject(request.error);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Rewind to a specific sync ID (for conflict resolution)
|
|
424
|
+
*/
|
|
425
|
+
async rewindTo(syncId) {
|
|
426
|
+
return new Promise(async (resolve, reject) => {
|
|
427
|
+
// Get all actions after this sync ID
|
|
428
|
+
const tx = this.db.transaction([this.storeName], 'readonly');
|
|
429
|
+
const store = tx.objectStore(this.storeName);
|
|
430
|
+
const index = store.index('syncId');
|
|
431
|
+
const range = IDBKeyRange.lowerBound(syncId, false);
|
|
432
|
+
const request = index.getAll(range);
|
|
433
|
+
request.onsuccess = async () => {
|
|
434
|
+
const actionsToRewind = request.result;
|
|
435
|
+
// Mark them as not applied
|
|
436
|
+
const writeTx = this.db.transaction([this.storeName], 'readwrite');
|
|
437
|
+
const writeStore = writeTx.objectStore(this.storeName);
|
|
438
|
+
let processed = 0;
|
|
439
|
+
const total = actionsToRewind.length;
|
|
440
|
+
if (total === 0) {
|
|
441
|
+
this.lastAppliedSyncId = syncId - 1;
|
|
442
|
+
try {
|
|
443
|
+
await this.updateLastSyncId(this.lastAppliedSyncId);
|
|
444
|
+
resolve([]);
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
reject(error);
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
for (const action of actionsToRewind) {
|
|
452
|
+
action.applied = false;
|
|
453
|
+
delete action.appliedAt;
|
|
454
|
+
const putRequest = writeStore.put(action);
|
|
455
|
+
putRequest.onsuccess = () => {
|
|
456
|
+
processed++;
|
|
457
|
+
// Add back to pending
|
|
458
|
+
this.pendingActions.set(action.id, action);
|
|
459
|
+
if (processed === total) {
|
|
460
|
+
// All processed
|
|
461
|
+
this.lastAppliedSyncId = syncId - 1;
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
putRequest.onerror = () => reject(putRequest.error);
|
|
465
|
+
}
|
|
466
|
+
writeTx.oncomplete = async () => {
|
|
467
|
+
try {
|
|
468
|
+
await this.updateLastSyncId(this.lastAppliedSyncId);
|
|
469
|
+
const result = actionsToRewind.map(({ storedAt, applied, ...action }) => action);
|
|
470
|
+
resolve(result);
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
reject(error);
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
writeTx.onerror = () => reject(writeTx.error);
|
|
477
|
+
};
|
|
478
|
+
request.onerror = () => reject(request.error);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BootstrapHelper - Fixed to always fetch fresh data
|
|
3
|
+
* Removed problematic caching that was serving stale data
|
|
4
|
+
*/
|
|
5
|
+
export interface BootstrapData {
|
|
6
|
+
type: 'full' | 'partial';
|
|
7
|
+
lastSyncId: number;
|
|
8
|
+
/**
|
|
9
|
+
* Model rows keyed by typename. Each row is opaque to the SDK at this
|
|
10
|
+
* boundary — the per-model shape is asserted by the consumer (sync
|
|
11
|
+
* engine reduce + IDB write) using the registered schema.
|
|
12
|
+
*/
|
|
13
|
+
models?: {
|
|
14
|
+
[typename: string]: unknown[];
|
|
15
|
+
};
|
|
16
|
+
deltas?: ValidatedServerDelta[];
|
|
17
|
+
deltaCount?: number;
|
|
18
|
+
/** Model types whose server-side query failed (timeout, RLS error, etc.) */
|
|
19
|
+
failedModels?: string[];
|
|
20
|
+
timestamp: number;
|
|
21
|
+
}
|
|
22
|
+
export interface BootstrapFetchResult {
|
|
23
|
+
notModified: boolean;
|
|
24
|
+
data?: BootstrapData;
|
|
25
|
+
etag?: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface BootstrapOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Full base URL of the sync server's HTTP API, **including the `/api`
|
|
30
|
+
* prefix**. The bootstrap endpoint is appended as `/sync/bootstrap`, so
|
|
31
|
+
* the final request hits `${baseUrl}/sync/bootstrap`.
|
|
32
|
+
*
|
|
33
|
+
* Example: `'http://localhost:8080/api'` → `http://localhost:8080/api/sync/bootstrap`
|
|
34
|
+
*
|
|
35
|
+
* Default: `'http://localhost:8080/api'`.
|
|
36
|
+
*/
|
|
37
|
+
baseUrl?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Private cache namespace for offline bootstrap fallback. Hosted SDK
|
|
40
|
+
* callers do not pass this; Ablo sets it after auth resolves the
|
|
41
|
+
* account scope.
|
|
42
|
+
*/
|
|
43
|
+
cacheScope?: string | null;
|
|
44
|
+
/**
|
|
45
|
+
* @deprecated Use `cacheScope`. Kept so older self-hosted code that
|
|
46
|
+
* still constructs BootstrapHelper directly keeps its cache namespace.
|
|
47
|
+
*/
|
|
48
|
+
organizationId?: string;
|
|
49
|
+
syncGroups?: string[];
|
|
50
|
+
maxRetries?: number;
|
|
51
|
+
retryDelay?: number;
|
|
52
|
+
/** Timeout for individual fetch requests in ms (default: 30000) */
|
|
53
|
+
fetchTimeout?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Model names to request in bootstrap. When set, the server only returns
|
|
56
|
+
* these models — everything else is skipped. Derived from the schema's
|
|
57
|
+
* `load` strategy: only models with `load: 'instant'` (or unset, which
|
|
58
|
+
* defaults to instant) are included.
|
|
59
|
+
*
|
|
60
|
+
* When absent, the server returns all models (backward compatible with
|
|
61
|
+
* old clients that don't send a models param).
|
|
62
|
+
*/
|
|
63
|
+
instantModels?: string[];
|
|
64
|
+
}
|
|
65
|
+
import { type ValidatedServerDelta } from './schemas.js';
|
|
66
|
+
export declare class BootstrapHelper {
|
|
67
|
+
private options;
|
|
68
|
+
private abortController;
|
|
69
|
+
get baseUrl(): string;
|
|
70
|
+
constructor(options: BootstrapOptions);
|
|
71
|
+
/**
|
|
72
|
+
* Update the offline-cache namespace once auth has resolved the server-side
|
|
73
|
+
* account scope. This is intentionally not a public organizationId input.
|
|
74
|
+
*/
|
|
75
|
+
setCacheScope(cacheScope: string): void;
|
|
76
|
+
setSyncGroups(syncGroups: readonly string[] | undefined): void;
|
|
77
|
+
setAuthToken(authToken: string | undefined): void;
|
|
78
|
+
/**
|
|
79
|
+
* Create a promise that rejects after a timeout
|
|
80
|
+
* Used to race against fetch requests that may hang indefinitely
|
|
81
|
+
*/
|
|
82
|
+
private createTimeoutPromise;
|
|
83
|
+
/**
|
|
84
|
+
* Wrap a promise with a timeout - if the promise doesn't resolve within
|
|
85
|
+
* the timeout period, the AbortController is triggered and an error is thrown
|
|
86
|
+
*/
|
|
87
|
+
private withTimeout;
|
|
88
|
+
/**
|
|
89
|
+
* Fetch bootstrap data from sync engine with partial bootstrap support
|
|
90
|
+
* @param lastSyncId - Optional: client's current lastSyncId for partial bootstrap
|
|
91
|
+
* @returns Bootstrap data (either full snapshot or delta batch)
|
|
92
|
+
*/
|
|
93
|
+
fetchBootstrap(lastSyncId?: number): Promise<BootstrapData>;
|
|
94
|
+
/**
|
|
95
|
+
* Fetch bootstrap with ETag, returning 304 hints
|
|
96
|
+
*/
|
|
97
|
+
fetchBootstrapWithETag(): Promise<BootstrapFetchResult>;
|
|
98
|
+
/**
|
|
99
|
+
* Perform the actual fetch request with timeout protection
|
|
100
|
+
*/
|
|
101
|
+
private performFetch;
|
|
102
|
+
/**
|
|
103
|
+
* Fetch a single entity by ID (on-demand self-healing).
|
|
104
|
+
* Returns `null` for 404 (entity deleted) — this is an expected state, not an error.
|
|
105
|
+
* Throws for unexpected HTTP errors (5xx, network failures).
|
|
106
|
+
*/
|
|
107
|
+
fetchEntity(modelName: string, id: string): Promise<Record<string, unknown> | null>;
|
|
108
|
+
/**
|
|
109
|
+
* Clear all cached bootstrap data
|
|
110
|
+
*/
|
|
111
|
+
clearCache(): void;
|
|
112
|
+
private getBootstrapCacheKey;
|
|
113
|
+
private saveCachedBootstrap;
|
|
114
|
+
private loadCachedBootstrap;
|
|
115
|
+
/**
|
|
116
|
+
* Abort ongoing fetch request
|
|
117
|
+
*/
|
|
118
|
+
abort(): void;
|
|
119
|
+
/**
|
|
120
|
+
* Helper to delay execution
|
|
121
|
+
*/
|
|
122
|
+
private delay;
|
|
123
|
+
/**
|
|
124
|
+
* Get health status of sync engine
|
|
125
|
+
*/
|
|
126
|
+
checkHealth(): Promise<boolean>;
|
|
127
|
+
}
|