@abloatai/ablo 0.25.0 → 0.26.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 (244) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +3 -3
  4. package/dist/BaseSyncedStore.d.ts +72 -195
  5. package/dist/BaseSyncedStore.js +240 -625
  6. package/dist/Database.d.ts +8 -9
  7. package/dist/Database.js +9 -14
  8. package/dist/LazyReferenceCollection.js +5 -0
  9. package/dist/Model.js +6 -8
  10. package/dist/ModelRegistry.d.ts +25 -6
  11. package/dist/ModelRegistry.js +12 -6
  12. package/dist/NetworkMonitor.js +1 -1
  13. package/dist/ObjectPool.js +9 -3
  14. package/dist/SyncClient.d.ts +15 -5
  15. package/dist/SyncClient.js +152 -66
  16. package/dist/SyncEngineContext.d.ts +1 -3
  17. package/dist/SyncEngineContext.js +1 -2
  18. package/dist/agent/Agent.d.ts +21 -8
  19. package/dist/agent/Agent.js +26 -13
  20. package/dist/ai-sdk/coordination-context.js +9 -3
  21. package/dist/ai-sdk/wrap.d.ts +1 -1
  22. package/dist/ai-sdk/wrap.js +1 -1
  23. package/dist/auth/credentialPolicy.d.ts +10 -2
  24. package/dist/auth/credentialSource.d.ts +1 -13
  25. package/dist/auth/credentialSource.js +6 -13
  26. package/dist/auth/index.js +3 -3
  27. package/dist/cli.cjs +328 -262
  28. package/dist/client/Ablo.d.ts +35 -764
  29. package/dist/client/Ablo.js +64 -688
  30. package/dist/client/ApiClient.d.ts +14 -1
  31. package/dist/client/ApiClient.js +63 -9
  32. package/dist/client/auth.d.ts +8 -17
  33. package/dist/client/auth.js +42 -6
  34. package/dist/client/consoleLogger.d.ts +36 -0
  35. package/dist/client/consoleLogger.js +45 -0
  36. package/dist/client/createInternalComponents.js +1 -1
  37. package/dist/client/createModelProxy.js +10 -6
  38. package/dist/client/credentialEndpoint.d.ts +63 -0
  39. package/dist/client/credentialEndpoint.js +87 -0
  40. package/dist/client/hostedEndpoints.d.ts +24 -0
  41. package/dist/client/hostedEndpoints.js +24 -0
  42. package/dist/client/httpClient.d.ts +1 -1
  43. package/dist/client/identity.js +2 -1
  44. package/dist/client/modelRegistration.d.ts +14 -0
  45. package/dist/client/modelRegistration.js +321 -0
  46. package/dist/client/options.d.ts +396 -0
  47. package/dist/client/options.js +10 -0
  48. package/dist/client/resourceTypes.d.ts +344 -0
  49. package/dist/client/resourceTypes.js +13 -0
  50. package/dist/client/schemaConfig.d.ts +56 -0
  51. package/dist/client/schemaConfig.js +188 -0
  52. package/dist/client/sessionMint.d.ts +1 -1
  53. package/dist/client/validateAbloOptions.js +1 -1
  54. package/dist/client/wsMutationExecutor.d.ts +33 -0
  55. package/dist/client/wsMutationExecutor.js +77 -0
  56. package/dist/context.js +0 -3
  57. package/dist/coordination/index.d.ts +8 -1
  58. package/dist/coordination/index.js +24 -1
  59. package/dist/coordination/trace.d.ts +0 -1
  60. package/dist/coordination/trace.js +0 -1
  61. package/dist/core/DatabaseManager.d.ts +0 -1
  62. package/dist/core/DatabaseManager.js +26 -24
  63. package/dist/core/QueryProcessor.js +7 -8
  64. package/dist/core/QueryView.d.ts +14 -2
  65. package/dist/core/QueryView.js +4 -5
  66. package/dist/core/StoreManager.d.ts +2 -2
  67. package/dist/core/StoreManager.js +5 -1
  68. package/dist/core/index.d.ts +1 -1
  69. package/dist/core/openIDBWithTimeout.js +15 -11
  70. package/dist/core/query-utils.js +1 -1
  71. package/dist/core/storeContract.d.ts +143 -0
  72. package/dist/core/storeContract.js +16 -0
  73. package/dist/errorCodes.d.ts +11 -2
  74. package/dist/errorCodes.js +155 -137
  75. package/dist/errors.d.ts +18 -7
  76. package/dist/errors.js +14 -1
  77. package/dist/index.js +5 -0
  78. package/dist/interfaces/index.d.ts +6 -20
  79. package/dist/keys/index.js +18 -9
  80. package/dist/mutators/UndoManager.js +6 -1
  81. package/dist/mutators/defineMutators.d.ts +1 -3
  82. package/dist/policy/types.d.ts +1 -1
  83. package/dist/query/client.d.ts +11 -0
  84. package/dist/query/client.js +81 -52
  85. package/dist/react/AbloProvider.d.ts +6 -10
  86. package/dist/react/AbloProvider.js +22 -15
  87. package/dist/react/context.d.ts +2 -118
  88. package/dist/react/useAblo.js +1 -1
  89. package/dist/react/useErrorListener.js +1 -1
  90. package/dist/react/useMutationFailureListener.js +1 -1
  91. package/dist/react/useMutators.js +7 -1
  92. package/dist/react/useReactive.js +0 -1
  93. package/dist/react/useUndoScope.js +2 -2
  94. package/dist/schema/ddlLock.d.ts +39 -0
  95. package/dist/schema/ddlLock.js +51 -0
  96. package/dist/schema/field.js +1 -1
  97. package/dist/schema/index.d.ts +4 -3
  98. package/dist/schema/index.js +15 -5
  99. package/dist/schema/model.d.ts +3 -3
  100. package/dist/schema/model.js +2 -2
  101. package/dist/schema/openapi.js +5 -1
  102. package/dist/schema/queries.d.ts +1 -1
  103. package/dist/schema/queries.js +1 -0
  104. package/dist/schema/residency.d.ts +35 -0
  105. package/dist/schema/residency.js +27 -0
  106. package/dist/schema/roles.d.ts +25 -0
  107. package/dist/schema/roles.js +37 -0
  108. package/dist/schema/schema.d.ts +3 -3
  109. package/dist/schema/schema.js +11 -7
  110. package/dist/schema/serialize.d.ts +8 -8
  111. package/dist/schema/sync-delta-row.d.ts +2 -2
  112. package/dist/schema/sync-delta-row.js +2 -2
  113. package/dist/server/storage-mode.d.ts +8 -1
  114. package/dist/server/storage-mode.js +14 -4
  115. package/dist/source/adapter.d.ts +1 -1
  116. package/dist/source/adapters/drizzle.js +4 -3
  117. package/dist/source/adapters/kysely.js +4 -3
  118. package/dist/source/adapters/memory.js +1 -1
  119. package/dist/source/adapters/prisma.js +5 -4
  120. package/dist/source/conformance.js +10 -8
  121. package/dist/source/connector.js +4 -2
  122. package/dist/source/factory.d.ts +106 -0
  123. package/dist/source/factory.js +273 -0
  124. package/dist/source/index.d.ts +11 -461
  125. package/dist/source/index.js +14 -416
  126. package/dist/source/next.d.ts +1 -1
  127. package/dist/source/next.js +1 -1
  128. package/dist/source/pushQueue.d.ts +1 -1
  129. package/dist/source/pushQueue.js +6 -3
  130. package/dist/source/signing.d.ts +59 -0
  131. package/dist/source/signing.js +142 -0
  132. package/dist/source/types.d.ts +334 -0
  133. package/dist/source/types.js +44 -0
  134. package/dist/stores/ObjectStore.d.ts +1 -1
  135. package/dist/stores/ObjectStore.js +23 -23
  136. package/dist/stores/SyncActionStore.d.ts +1 -1
  137. package/dist/stores/SyncActionStore.js +70 -35
  138. package/dist/surface.d.ts +1 -1
  139. package/dist/surface.js +1 -0
  140. package/dist/sync/AreaOfInterestManager.d.ts +2 -2
  141. package/dist/sync/AreaOfInterestManager.js +1 -1
  142. package/dist/sync/BootstrapHelper.d.ts +1 -3
  143. package/dist/sync/BootstrapHelper.js +29 -8
  144. package/dist/sync/ConnectionManager.d.ts +8 -0
  145. package/dist/sync/ConnectionManager.js +19 -4
  146. package/dist/sync/HydrationCoordinator.d.ts +12 -0
  147. package/dist/sync/HydrationCoordinator.js +25 -5
  148. package/dist/sync/NetworkProbe.d.ts +3 -2
  149. package/dist/sync/NetworkProbe.js +7 -6
  150. package/dist/sync/SyncWebSocket.d.ts +71 -120
  151. package/dist/sync/SyncWebSocket.js +324 -630
  152. package/dist/sync/awaitClaimGrant.js +27 -19
  153. package/dist/sync/bootstrapApply.d.ts +60 -0
  154. package/dist/sync/bootstrapApply.js +65 -0
  155. package/dist/sync/commitFrames.d.ts +43 -0
  156. package/dist/sync/commitFrames.js +94 -0
  157. package/dist/sync/createClaimStream.js +3 -2
  158. package/dist/sync/createSnapshot.d.ts +1 -3
  159. package/dist/sync/credentialLifecycle.d.ts +175 -0
  160. package/dist/sync/credentialLifecycle.js +329 -0
  161. package/dist/sync/deltaPipeline.d.ts +111 -0
  162. package/dist/sync/deltaPipeline.js +262 -0
  163. package/dist/sync/groupChange.d.ts +111 -0
  164. package/dist/sync/groupChange.js +246 -0
  165. package/dist/sync/heartbeat.d.ts +62 -0
  166. package/dist/sync/heartbeat.js +91 -0
  167. package/dist/sync/participants.d.ts +8 -8
  168. package/dist/sync/syncCursor.d.ts +44 -0
  169. package/dist/sync/syncCursor.js +59 -0
  170. package/dist/sync/syncPlan.d.ts +62 -0
  171. package/dist/sync/syncPlan.js +51 -0
  172. package/dist/sync/wsFrameHandlers.d.ts +117 -0
  173. package/dist/sync/wsFrameHandlers.js +378 -0
  174. package/dist/testing/fixtures/bootstrap.d.ts +9 -11
  175. package/dist/testing/fixtures/deltas.d.ts +1 -1
  176. package/dist/testing/fixtures/models.js +1 -0
  177. package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
  178. package/dist/testing/helpers/sync-engine-harness.js +2 -1
  179. package/dist/testing/index.d.ts +1 -1
  180. package/dist/testing/index.js +1 -1
  181. package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
  182. package/dist/testing/mocks/MockSyncContext.js +1 -32
  183. package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
  184. package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
  185. package/dist/transactions/TransactionQueue.d.ts +20 -56
  186. package/dist/transactions/TransactionQueue.js +101 -487
  187. package/dist/transactions/TransactionStore.d.ts +18 -0
  188. package/dist/transactions/TransactionStore.js +51 -0
  189. package/dist/transactions/coalesceRules.d.ts +34 -0
  190. package/dist/transactions/coalesceRules.js +117 -0
  191. package/dist/transactions/commitPayload.d.ts +134 -0
  192. package/dist/transactions/commitPayload.js +152 -0
  193. package/dist/transactions/deltaConfirmation.d.ts +60 -0
  194. package/dist/transactions/deltaConfirmation.js +223 -0
  195. package/dist/transactions/optimistic.d.ts +24 -0
  196. package/dist/transactions/optimistic.js +45 -0
  197. package/dist/transactions/persistedReplay.d.ts +93 -0
  198. package/dist/transactions/persistedReplay.js +105 -0
  199. package/dist/types/index.d.ts +3 -3
  200. package/dist/types/index.js +0 -5
  201. package/dist/types/modelData.d.ts +12 -0
  202. package/dist/types/modelData.js +11 -0
  203. package/dist/types/participant.d.ts +21 -0
  204. package/dist/types/participant.js +12 -0
  205. package/dist/types/streams.d.ts +8 -14
  206. package/dist/utils/mobx-setup.d.ts +1 -1
  207. package/dist/utils/mobx-setup.js +2 -2
  208. package/dist/wire/errorEnvelope.d.ts +11 -2
  209. package/dist/wire/errorEnvelope.js +14 -2
  210. package/dist/wire/frames.d.ts +90 -0
  211. package/dist/wire/frames.js +55 -1
  212. package/dist/wire/index.d.ts +4 -1
  213. package/dist/wire/index.js +20 -1
  214. package/dist/wire/protocol.d.ts +45 -0
  215. package/dist/wire/protocol.js +45 -0
  216. package/dist/wire/protocolVersion.d.ts +56 -0
  217. package/dist/wire/protocolVersion.js +63 -0
  218. package/docs/api-keys.md +4 -3
  219. package/docs/examples/existing-python-backend.md +3 -3
  220. package/docs/identity.md +4 -4
  221. package/docs/integration-guide.md +1 -1
  222. package/docs/react.md +1 -1
  223. package/docs/sessions.md +5 -7
  224. package/package.json +15 -13
  225. package/dist/client/index.d.ts +0 -36
  226. package/dist/client/index.js +0 -33
  227. package/dist/config/index.d.ts +0 -10
  228. package/dist/config/index.js +0 -12
  229. package/dist/interfaces/headless.d.ts +0 -95
  230. package/dist/interfaces/headless.js +0 -41
  231. package/dist/query/index.d.ts +0 -6
  232. package/dist/query/index.js +0 -5
  233. package/dist/realtime/index.d.ts +0 -10
  234. package/dist/realtime/index.js +0 -9
  235. package/dist/schema/plane.d.ts +0 -23
  236. package/dist/schema/plane.js +0 -19
  237. package/dist/server/next.d.ts +0 -51
  238. package/dist/server/next.js +0 -47
  239. package/dist/sync/OfflineFlush.d.ts +0 -9
  240. package/dist/sync/OfflineFlush.js +0 -22
  241. package/dist/sync/OfflineTransactionStore.d.ts +0 -37
  242. package/dist/sync/OfflineTransactionStore.js +0 -263
  243. package/dist/transactions/index.d.ts +0 -16
  244. package/dist/transactions/index.js +0 -7
@@ -4,6 +4,36 @@
4
4
  * Stores and manages sync actions received from the server.
5
5
  * Critical for delta sync and maintaining sync state consistency.
6
6
  */
7
+ // Uses native IndexedDB for maximum performance
8
+ import { z } from 'zod';
9
+ import { getContext } from '../context.js';
10
+ /**
11
+ * Zod boundary for rows read back from the sync-action store (T1.8): they
12
+ * were written by a previous session, so their shape is not guaranteed by
13
+ * this build. Default (strip) mode drops the storage bookkeeping fields
14
+ * (`storedAt`/`applied`/`appliedAt`) that used to be peeled off with an
15
+ * untyped rest-spread. Rows that don't parse are dropped + logged instead
16
+ * of being replayed as malformed deltas.
17
+ */
18
+ const storedSyncActionSchema = z.object({
19
+ id: z.number(),
20
+ modelName: z.string(),
21
+ modelId: z.string(),
22
+ action: z.enum(['I', 'U', 'A', 'D', 'C', 'G', 'S', 'V']),
23
+ data: z.unknown(),
24
+ __class: z.literal('SyncAction').default('SyncAction'),
25
+ });
26
+ /** Parse one stored row into a SyncAction, or `null` (dropped + logged). */
27
+ function toSyncAction(row) {
28
+ const parsed = storedSyncActionSchema.safeParse(row);
29
+ if (!parsed.success) {
30
+ getContext().logger.debug('[SyncActionStore] Dropping malformed stored sync action', {
31
+ issues: parsed.error.issues.map((i) => i.path.join('.')).join(', '),
32
+ });
33
+ return null;
34
+ }
35
+ return parsed.data;
36
+ }
7
37
  /**
8
38
  * SyncActionStore - Manages sync actions (deltas)
9
39
  *
@@ -51,8 +81,8 @@ export class SyncActionStore {
51
81
  }
52
82
  resolve();
53
83
  };
54
- tx.onerror = () => reject(tx.error);
55
- request.onerror = () => reject(request.error);
84
+ tx.onerror = () => { reject(tx.error); };
85
+ request.onerror = () => { reject(request.error); };
56
86
  });
57
87
  }
58
88
  /**
@@ -83,10 +113,10 @@ export class SyncActionStore {
83
113
  }
84
114
  }
85
115
  };
86
- request.onerror = () => reject(request.error);
116
+ request.onerror = () => { reject(request.error); };
87
117
  }
88
- tx.oncomplete = () => resolve();
89
- tx.onerror = () => reject(tx.error);
118
+ tx.oncomplete = () => { resolve(); };
119
+ tx.onerror = () => { reject(tx.error); };
90
120
  });
91
121
  }
92
122
  /**
@@ -103,10 +133,9 @@ export class SyncActionStore {
103
133
  resolve(undefined);
104
134
  return;
105
135
  }
106
- const { storedAt, applied, ...action } = data;
107
- resolve(action);
136
+ resolve(toSyncAction(data) ?? undefined);
108
137
  };
109
- request.onerror = () => reject(request.error);
138
+ request.onerror = () => { reject(request.error); };
110
139
  });
111
140
  }
112
141
  /**
@@ -121,10 +150,12 @@ export class SyncActionStore {
121
150
  const request = index.getAll(range);
122
151
  request.onsuccess = () => {
123
152
  const allData = request.result;
124
- const actions = allData.map(({ storedAt, applied, ...action }) => action);
153
+ const actions = allData
154
+ .map(toSyncAction)
155
+ .filter((action) => action !== null);
125
156
  resolve(actions);
126
157
  };
127
- request.onerror = () => reject(request.error);
158
+ request.onerror = () => { reject(request.error); };
128
159
  });
129
160
  }
130
161
  /**
@@ -142,7 +173,9 @@ export class SyncActionStore {
142
173
  * Mark sync action as applied
143
174
  */
144
175
  async markAsApplied(syncId) {
145
- return new Promise(async (resolve, reject) => {
176
+ // Plain (non-async) executor: a sync throw (e.g. transaction() on a
177
+ // closing DB) must reject this promise, not vanish into an ignored one.
178
+ return new Promise((resolve, reject) => {
146
179
  const tx = this.db.transaction([this.storeName], 'readwrite');
147
180
  const store = tx.objectStore(this.storeName);
148
181
  const getRequest = store.get(syncId);
@@ -152,10 +185,10 @@ export class SyncActionStore {
152
185
  existing.applied = true;
153
186
  existing.appliedAt = Date.now();
154
187
  const putRequest = store.put(existing);
155
- putRequest.onerror = () => reject(putRequest.error);
188
+ putRequest.onerror = () => { reject(putRequest.error); };
156
189
  }
157
190
  };
158
- getRequest.onerror = () => reject(getRequest.error);
191
+ getRequest.onerror = () => { reject(getRequest.error); };
159
192
  tx.oncomplete = async () => {
160
193
  // Update tracking
161
194
  if (syncId > this.lastAppliedSyncId) {
@@ -172,7 +205,7 @@ export class SyncActionStore {
172
205
  reject(error);
173
206
  }
174
207
  };
175
- tx.onerror = () => reject(tx.error);
208
+ tx.onerror = () => { reject(tx.error); };
176
209
  });
177
210
  }
178
211
  /**
@@ -181,7 +214,7 @@ export class SyncActionStore {
181
214
  async markManyAsApplied(syncIds) {
182
215
  if (syncIds.length === 0)
183
216
  return;
184
- return new Promise(async (resolve, reject) => {
217
+ return new Promise((resolve, reject) => {
185
218
  const tx = this.db.transaction([this.storeName], 'readwrite');
186
219
  const store = tx.objectStore(this.storeName);
187
220
  let processed = 0;
@@ -206,14 +239,14 @@ export class SyncActionStore {
206
239
  }
207
240
  }
208
241
  };
209
- putRequest.onerror = () => reject(putRequest.error);
242
+ putRequest.onerror = () => { reject(putRequest.error); };
210
243
  }
211
244
  else {
212
245
  processed++;
213
246
  this.pendingActions.delete(syncId);
214
247
  }
215
248
  };
216
- getRequest.onerror = () => reject(getRequest.error);
249
+ getRequest.onerror = () => { reject(getRequest.error); };
217
250
  }
218
251
  tx.oncomplete = async () => {
219
252
  // Update metadata with the highest sync ID
@@ -231,7 +264,7 @@ export class SyncActionStore {
231
264
  resolve();
232
265
  }
233
266
  };
234
- tx.onerror = () => reject(tx.error);
267
+ tx.onerror = () => { reject(tx.error); };
235
268
  });
236
269
  }
237
270
  /**
@@ -265,7 +298,7 @@ export class SyncActionStore {
265
298
  currentId++;
266
299
  checkNext();
267
300
  };
268
- request.onerror = () => reject(request.error);
301
+ request.onerror = () => { reject(request.error); };
269
302
  };
270
303
  checkNext();
271
304
  });
@@ -293,7 +326,7 @@ export class SyncActionStore {
293
326
  currentId++;
294
327
  checkNext();
295
328
  };
296
- request.onerror = () => reject(request.error);
329
+ request.onerror = () => { reject(request.error); };
297
330
  };
298
331
  checkNext();
299
332
  });
@@ -317,7 +350,7 @@ export class SyncActionStore {
317
350
  cleaned++;
318
351
  cursor.continue();
319
352
  };
320
- deleteRequest.onerror = () => reject(deleteRequest.error);
353
+ deleteRequest.onerror = () => { reject(deleteRequest.error); };
321
354
  }
322
355
  else {
323
356
  cursor.continue();
@@ -328,7 +361,7 @@ export class SyncActionStore {
328
361
  resolve(cleaned);
329
362
  }
330
363
  };
331
- request.onerror = () => reject(request.error);
364
+ request.onerror = () => { reject(request.error); };
332
365
  });
333
366
  }
334
367
  /**
@@ -344,8 +377,8 @@ export class SyncActionStore {
344
377
  this.lastAppliedSyncId = 0;
345
378
  resolve();
346
379
  };
347
- tx.onerror = () => reject(tx.error);
348
- request.onerror = () => reject(request.error);
380
+ tx.onerror = () => { reject(tx.error); };
381
+ request.onerror = () => { reject(request.error); };
349
382
  });
350
383
  }
351
384
  /**
@@ -373,7 +406,7 @@ export class SyncActionStore {
373
406
  newestAction: timestamps.length > 0 ? new Date(timestamps[timestamps.length - 1]) : null,
374
407
  });
375
408
  };
376
- request.onerror = () => reject(request.error);
409
+ request.onerror = () => { reject(request.error); };
377
410
  });
378
411
  }
379
412
  /**
@@ -389,11 +422,11 @@ export class SyncActionStore {
389
422
  metadata.lastSyncId = syncId;
390
423
  metadata.updatedAt = new Date();
391
424
  const putRequest = store.put(metadata, 'metadata');
392
- putRequest.onerror = () => reject(putRequest.error);
425
+ putRequest.onerror = () => { reject(putRequest.error); };
393
426
  };
394
- getRequest.onerror = () => reject(getRequest.error);
395
- tx.oncomplete = () => resolve();
396
- tx.onerror = () => reject(tx.error);
427
+ getRequest.onerror = () => { reject(getRequest.error); };
428
+ tx.oncomplete = () => { resolve(); };
429
+ tx.onerror = () => { reject(tx.error); };
397
430
  });
398
431
  }
399
432
  /**
@@ -416,14 +449,14 @@ export class SyncActionStore {
416
449
  resolve(undefined);
417
450
  }
418
451
  };
419
- request.onerror = () => reject(request.error);
452
+ request.onerror = () => { reject(request.error); };
420
453
  });
421
454
  }
422
455
  /**
423
456
  * Rewind to a specific sync ID (for conflict resolution)
424
457
  */
425
458
  async rewindTo(syncId) {
426
- return new Promise(async (resolve, reject) => {
459
+ return new Promise((resolve, reject) => {
427
460
  // Get all actions after this sync ID
428
461
  const tx = this.db.transaction([this.storeName], 'readonly');
429
462
  const store = tx.objectStore(this.storeName);
@@ -461,21 +494,23 @@ export class SyncActionStore {
461
494
  this.lastAppliedSyncId = syncId - 1;
462
495
  }
463
496
  };
464
- putRequest.onerror = () => reject(putRequest.error);
497
+ putRequest.onerror = () => { reject(putRequest.error); };
465
498
  }
466
499
  writeTx.oncomplete = async () => {
467
500
  try {
468
501
  await this.updateLastSyncId(this.lastAppliedSyncId);
469
- const result = actionsToRewind.map(({ storedAt, applied, ...action }) => action);
502
+ const result = actionsToRewind
503
+ .map(toSyncAction)
504
+ .filter((action) => action !== null);
470
505
  resolve(result);
471
506
  }
472
507
  catch (error) {
473
508
  reject(error);
474
509
  }
475
510
  };
476
- writeTx.onerror = () => reject(writeTx.error);
511
+ writeTx.onerror = () => { reject(writeTx.error); };
477
512
  };
478
- request.onerror = () => reject(request.error);
513
+ request.onerror = () => { reject(request.error); };
479
514
  });
480
515
  }
481
516
  }
package/dist/surface.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const PUBLIC_MODEL_VERBS: readonly ["retrieve", "list", "get", "g
23
23
  export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orderBy", "limit", "offset", "state"];
24
24
  /** Public keys of `AbloOptions`. `schema` is required; the rest are optional
25
25
  * (the locked happy path is `Ablo({ schema, apiKey, databaseUrl, transport })`). */
26
- export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "databaseUrl", "persistence", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
26
+ export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
27
27
  export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
28
28
  export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
29
29
  export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
package/dist/surface.js CHANGED
@@ -48,6 +48,7 @@ export const PUBLIC_LIST_OPTION_KEYS = [
48
48
  export const PUBLIC_ABLO_OPTION_KEYS = [
49
49
  'schema',
50
50
  'apiKey',
51
+ 'authEndpoint',
51
52
  'databaseUrl',
52
53
  'persistence',
53
54
  'transport',
@@ -41,7 +41,7 @@ export interface SubscriptionTransport {
41
41
  * Resolves with the server's effective set (which the manager treats as
42
42
  * authoritative for its next diff).
43
43
  */
44
- updateSubscription(syncGroups: ReadonlyArray<string>): Promise<{
44
+ updateSubscription(syncGroups: readonly string[]): Promise<{
45
45
  syncGroups: string[];
46
46
  }>;
47
47
  }
@@ -52,7 +52,7 @@ export interface AreaOfInterestOptions {
52
52
  * Groups always present in the effective set (e.g. `org:<id>`,
53
53
  * `user:<id>`). Never warm, never expired.
54
54
  */
55
- baseGroups?: ReadonlyArray<string>;
55
+ baseGroups?: readonly string[];
56
56
  /**
57
57
  * How long a `leave`-ed group stays subscribed before it actually drops.
58
58
  * This is the hysteresis margin. Default 30s.
@@ -71,7 +71,7 @@ export class AreaOfInterestManager {
71
71
  const schedule = options.scheduler ??
72
72
  ((fn, ms) => {
73
73
  const handle = setInterval(fn, ms);
74
- return () => clearInterval(handle);
74
+ return () => { clearInterval(handle); };
75
75
  });
76
76
  this.cancelSweep = schedule(() => {
77
77
  void this.sweep();
@@ -10,9 +10,7 @@ export interface BootstrapData {
10
10
  * boundary — the per-model shape is asserted by the consumer (sync
11
11
  * engine reduce + IDB write) using the registered schema.
12
12
  */
13
- models?: {
14
- [typename: string]: unknown[];
15
- };
13
+ models?: Record<string, unknown[]>;
16
14
  deltas?: ValidatedServerDelta[];
17
15
  deltaCount?: number;
18
16
  /** Model types whose server-side query failed (timeout, RLS error, etc.) */
@@ -176,7 +176,7 @@ export class BootstrapHelper {
176
176
  // Also do NOT fallback to cache - the user must sign in again
177
177
  if (SyncSessionError.isSessionError(error)) {
178
178
  getContext().observability.breadcrumb('Bootstrap session error - redirecting to sign-in', 'sync.bootstrap', 'warning', {
179
- statusCode: error.statusCode,
179
+ statusCode: (error).statusCode,
180
180
  });
181
181
  throw error;
182
182
  }
@@ -226,7 +226,7 @@ export class BootstrapHelper {
226
226
  // client-side was historical: it predated the auth-context pipeline
227
227
  // and forced a cross-org guard to defend against the SDK lying.
228
228
  const params = new URLSearchParams();
229
- this.options.syncGroups.forEach((g) => params.append('syncGroups', g));
229
+ this.options.syncGroups.forEach((g) => { params.append('syncGroups', g); });
230
230
  if (this.options.instantModels && this.options.instantModels.length > 0) {
231
231
  params.append('models', this.options.instantModels.join(','));
232
232
  }
@@ -369,12 +369,33 @@ export class BootstrapHelper {
369
369
  */
370
370
  async fetchEntity(modelName, id) {
371
371
  const url = `${this.options.baseUrl}/sync/entity/${modelName}/${id}`;
372
- const response = await fetch(url, {
373
- method: 'GET',
374
- headers: withAuthHeaders(this.options.getAuthToken, {
375
- 'Content-Type': 'application/json',
376
- }, this.options.authToken),
377
- });
372
+ // Same `fetchTimeout` deadline `performFetch` uses — this was the one
373
+ // fetch in this file with no AbortSignal, so a hung self-heal read could
374
+ // stall its caller forever. A LOCAL controller (not `this.abortController`)
375
+ // so an entity self-heal never cancels a concurrent bootstrap fetch.
376
+ const controller = new AbortController();
377
+ const timeoutId = setTimeout(() => { controller.abort(); }, this.options.fetchTimeout);
378
+ let response;
379
+ try {
380
+ response = await fetch(url, {
381
+ method: 'GET',
382
+ headers: withAuthHeaders(this.options.getAuthToken, {
383
+ 'Content-Type': 'application/json',
384
+ }, this.options.authToken),
385
+ signal: controller.signal,
386
+ });
387
+ }
388
+ catch (error) {
389
+ // Convert abort to the existing typed timeout error (same code as the
390
+ // bootstrap fetch path) so callers get a retryable connection error.
391
+ if (error instanceof Error && error.name === 'AbortError') {
392
+ throw new AbloConnectionError(`Entity fetch timed out after ${this.options.fetchTimeout}ms`, { code: 'bootstrap_fetch_timeout', cause: error });
393
+ }
394
+ throw error;
395
+ }
396
+ finally {
397
+ clearTimeout(timeoutId);
398
+ }
378
399
  if (response.status === 404) {
379
400
  return null;
380
401
  }
@@ -167,6 +167,14 @@ export declare class ConnectionManager {
167
167
  send(event: ConnectionEvent): void;
168
168
  private transition;
169
169
  private onEnterState;
170
+ /**
171
+ * FSM effects are fire-and-forget by design (`onEnterState` is sync). Each
172
+ * effect catches its own operational failures and maps them onto an FSM
173
+ * event, so a rejection here means the machine wedged mid-transition
174
+ * (`send()` or a transition listener threw) with no retry timer armed past
175
+ * it — capture it instead of losing the reconnect loop silently.
176
+ */
177
+ private captureEffectFailure;
170
178
  private runProbe;
171
179
  /**
172
180
  * Re-mint the short-lived access key on `refreshing_credential`. Delegates to
@@ -349,16 +349,16 @@ export class ConnectionManager {
349
349
  if (event.type === 'WS_DISCONNECTED') {
350
350
  this.callbacks?.onDisconnectWebSocket();
351
351
  }
352
- this.runProbe();
352
+ void this.runProbe().catch(this.captureEffectFailure('probe'));
353
353
  break;
354
354
  case 'waiting_for_network':
355
355
  this.scheduleBackoff();
356
356
  break;
357
357
  case 'reconnecting':
358
- this.runReconnect();
358
+ void this.runReconnect().catch(this.captureEffectFailure('reconnect'));
359
359
  break;
360
360
  case 'refreshing_credential':
361
- this.runRefreshCredential();
361
+ void this.runRefreshCredential().catch(this.captureEffectFailure('refresh-credential'));
362
362
  break;
363
363
  case 'backoff':
364
364
  this.scheduleBackoff();
@@ -382,6 +382,21 @@ export class ConnectionManager {
382
382
  }
383
383
  }
384
384
  // ── Async operations ─────────────────────────────────────────────────
385
+ /**
386
+ * FSM effects are fire-and-forget by design (`onEnterState` is sync). Each
387
+ * effect catches its own operational failures and maps them onto an FSM
388
+ * event, so a rejection here means the machine wedged mid-transition
389
+ * (`send()` or a transition listener threw) with no retry timer armed past
390
+ * it — capture it instead of losing the reconnect loop silently.
391
+ */
392
+ captureEffectFailure(effect) {
393
+ return (error) => {
394
+ getContext().observability.captureWebSocketError({
395
+ context: `connection-fsm-${effect}`,
396
+ error: error instanceof Error ? error.message : String(error),
397
+ });
398
+ };
399
+ }
385
400
  async runProbe() {
386
401
  try {
387
402
  const result = await probeNetwork({
@@ -609,7 +624,7 @@ export class ConnectionManager {
609
624
  // means definitely offline"), and reloading while truly offline
610
625
  // would either serve cached content forever or fail and leave
611
626
  // the user with a broken page.
612
- const browserOnline = typeof navigator === 'undefined' || navigator.onLine === true;
627
+ const browserOnline = typeof navigator === 'undefined' || navigator.onLine;
613
628
  if (this.stuckCycles >= MAX_STUCK_CYCLES_BEFORE_RELOAD && browserOnline) {
614
629
  getContext().logger.debug('[ConnectionManager] Watchdog: sustained stuck — hard reload');
615
630
  getContext().observability.breadcrumb('Watchdog hard reload', 'sync.offline', 'error');
@@ -23,6 +23,7 @@ import type { ObjectPool } from '../ObjectPool.js';
23
23
  import type { Database } from '../Database.js';
24
24
  import type { Model } from '../Model.js';
25
25
  import type { ModelRegistry } from '../ModelRegistry.js';
26
+ import type { RecoveryClass } from '../errorCodes.js';
26
27
  import type { LoadWhere, WhereClause } from '../query/types.js';
27
28
  import type { Schema } from '../schema/schema.js';
28
29
  export interface HydrationCoordinatorOptions {
@@ -94,6 +95,15 @@ export declare class HydrationCoordinator {
94
95
  */
95
96
  private readonly hydratedKeys;
96
97
  private authTokenProvider;
98
+ /**
99
+ * The auth-recovery backbone (the store's `recoverFromAuthRejection`),
100
+ * late-bound like {@link setAuthTokenProvider} because the store doesn't
101
+ * exist yet when the coordinator is constructed. Handed to `postQuery` so a
102
+ * 401 on the lazy lane re-mints through the SAME single-flight path the WS
103
+ * probe and proactive pre-roll use, then replays once — instead of silently
104
+ * returning empty rows against an expired key forever.
105
+ */
106
+ private credentialRecovery;
97
107
  constructor(opts: HydrationCoordinatorOptions);
98
108
  /**
99
109
  * Late-bind the auth token getter. Browser cookie consumers can omit this;
@@ -101,6 +111,8 @@ export declare class HydrationCoordinator {
101
111
  * bootstrap and the WebSocket.
102
112
  */
103
113
  setAuthTokenProvider(provider: () => string | null): void;
114
+ /** Late-bind the auth-recovery backbone. See {@link credentialRecovery}. */
115
+ setCredentialRecovery(recover: (recovery: RecoveryClass) => Promise<'retry' | 'stop'>): void;
104
116
  /** @deprecated Use `setAuthTokenProvider`. */
105
117
  setCapabilityTokenProvider(provider: () => string | null): void;
106
118
  /**
@@ -45,6 +45,15 @@ export class HydrationCoordinator {
45
45
  */
46
46
  hydratedKeys = new Set();
47
47
  authTokenProvider = null;
48
+ /**
49
+ * The auth-recovery backbone (the store's `recoverFromAuthRejection`),
50
+ * late-bound like {@link setAuthTokenProvider} because the store doesn't
51
+ * exist yet when the coordinator is constructed. Handed to `postQuery` so a
52
+ * 401 on the lazy lane re-mints through the SAME single-flight path the WS
53
+ * probe and proactive pre-roll use, then replays once — instead of silently
54
+ * returning empty rows against an expired key forever.
55
+ */
56
+ credentialRecovery = null;
48
57
  constructor(opts) {
49
58
  this.opts = opts;
50
59
  this.authTokenProvider = opts.getAuthToken ?? opts.getCapabilityToken ?? null;
@@ -57,6 +66,10 @@ export class HydrationCoordinator {
57
66
  setAuthTokenProvider(provider) {
58
67
  this.authTokenProvider = provider;
59
68
  }
69
+ /** Late-bind the auth-recovery backbone. See {@link credentialRecovery}. */
70
+ setCredentialRecovery(recover) {
71
+ this.credentialRecovery = recover;
72
+ }
60
73
  /** @deprecated Use `setAuthTokenProvider`. */
61
74
  setCapabilityTokenProvider(provider) {
62
75
  this.setAuthTokenProvider(provider);
@@ -82,9 +95,15 @@ export class HydrationCoordinator {
82
95
  return inFlight;
83
96
  const work = this.runFetch(modelName, typename, ModelClass, clauses, options, queryKey);
84
97
  this.inFlight.set(queryKey, work);
85
- work.finally(() => {
98
+ // The rejection (if any) reaches callers via the returned `work`; this
99
+ // side-chain only clears the single-flight slot. Without the trailing
100
+ // catch, `.finally()` mirrors the rejection into a second, unhandled
101
+ // promise even when every caller handles theirs.
102
+ void work
103
+ .finally(() => {
86
104
  this.inFlight.delete(queryKey);
87
- });
105
+ })
106
+ .catch(() => undefined);
88
107
  return work;
89
108
  }
90
109
  async runFetch(modelName, typename, ModelClass, clauses, options, queryKey) {
@@ -282,7 +301,7 @@ export class HydrationCoordinator {
282
301
  if (!Array.isArray(all))
283
302
  return [];
284
303
  const idSet = new Set(parentIds);
285
- return all.filter((r) => idSet.has(r[foreignKey]));
304
+ return all.filter((r) => idSet.has((r)[foreignKey]));
286
305
  }
287
306
  catch {
288
307
  return [];
@@ -368,6 +387,7 @@ export class HydrationCoordinator {
368
387
  const result = await postQuery({
369
388
  baseUrl: this.opts.baseUrl,
370
389
  getAuthToken: this.authTokenProvider ?? undefined,
390
+ recoverCredential: this.credentialRecovery ?? undefined,
371
391
  }, { queries: [query] });
372
392
  const rows = Array.isArray(result.results[0]) ? result.results[0] : [];
373
393
  // Normalize: wire rows lack `__typename` when the server elides it.
@@ -523,8 +543,8 @@ async function scanIdb(database, modelName, clauses) {
523
543
  // through to full-scan + filter.
524
544
  if (clausesAreAllEquality(clauses)) {
525
545
  const indexedKeys = Object.keys(eqClauses).filter((k) => k !== 'id' && typeof eqClauses[k] === 'string');
526
- if (indexedKeys.length === 1) {
527
- const idxKey = indexedKeys[0];
546
+ const idxKey = indexedKeys.length === 1 ? indexedKeys[0] : undefined;
547
+ if (idxKey !== undefined) {
528
548
  try {
529
549
  const rows = await store.getAllFromIndex(idxKey, eqClauses[idxKey]);
530
550
  if (Array.isArray(rows)) {
@@ -59,8 +59,9 @@ export declare const probeResultSchema: z.ZodObject<{
59
59
  export type ProbeResult = z.infer<typeof probeResultSchema>;
60
60
  export interface NetworkProbeOptions {
61
61
  /**
62
- * Sync-server base URL (HTTP or WS scheme accepted). If omitted, falls
63
- * back to the legacy `NEXT_PUBLIC_GO_SERVER_URL` default.
62
+ * Sync-server base URL (HTTP or WS scheme accepted). If omitted, the probe
63
+ * targets the canonical hosted endpoint ({@link ABLO_DEFAULT_BASE_URL}) —
64
+ * the same default `Ablo()` resolves.
64
65
  */
65
66
  baseUrl?: string;
66
67
  /**
@@ -28,6 +28,7 @@ import { z } from 'zod';
28
28
  import { getContext } from '../context.js';
29
29
  import { classifyRecovery } from '../errors.js';
30
30
  import { withAuthHeaders } from '../auth/credentialSource.js';
31
+ import { ABLO_DEFAULT_BASE_URL } from '../client/hostedEndpoints.js';
31
32
  /**
32
33
  * The closed set of probe outcomes — one value carrying both reachability and
33
34
  * credential disposition, so the {@link ConnectionManager} branches on a single
@@ -64,11 +65,11 @@ const PROBE_TIMEOUT_MS = 4000;
64
65
  * normalisation in `BootstrapHelper` / `createSyncEngine`.
65
66
  */
66
67
  function resolveProbeUrl(baseUrl) {
67
- // Fall back to the legacy env var so callers that haven't been migrated
68
- // to pass an explicit baseUrl keep working.
69
- const resolved = baseUrl ??
70
- (typeof process !== 'undefined' ? process.env?.NEXT_PUBLIC_GO_SERVER_URL : undefined) ??
71
- 'http://localhost:8080';
68
+ // No explicit baseUrl probe the canonical hosted endpoint, matching the
69
+ // `Ablo()` default. (This used to fall back to the REMOVED Go engine's
70
+ // `NEXT_PUBLIC_GO_SERVER_URL` and then `http://localhost:8080`, so a probe
71
+ // without a baseUrl reported a healthy production deployment as offline.)
72
+ const resolved = baseUrl ?? ABLO_DEFAULT_BASE_URL;
72
73
  // Normalize ws → http so fetch() accepts the URL. Strip any trailing slash
73
74
  // so we don't produce `//api/auth/check`.
74
75
  const httpBase = resolved.replace(/^ws/, 'http').replace(/\/+$/, '');
@@ -98,7 +99,7 @@ export async function probeNetwork(input) {
98
99
  return { outcome: 'unreachable', latencyMs: null };
99
100
  }
100
101
  const controller = new AbortController();
101
- const timeout = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
102
+ const timeout = setTimeout(() => { controller.abort(); }, PROBE_TIMEOUT_MS);
102
103
  const start = performance.now();
103
104
  try {
104
105
  const headers = withAuthHeaders(getAuthToken, { 'Cache-Control': 'no-cache' }, authToken);