@abloatai/ablo 0.10.1 → 0.11.1

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 (105) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +63 -23
  3. package/dist/BaseSyncedStore.d.ts +75 -0
  4. package/dist/BaseSyncedStore.js +193 -8
  5. package/dist/Database.d.ts +10 -2
  6. package/dist/Database.js +15 -1
  7. package/dist/SyncClient.d.ts +12 -1
  8. package/dist/SyncClient.js +110 -26
  9. package/dist/agent/Agent.d.ts +9 -9
  10. package/dist/agent/Agent.js +16 -16
  11. package/dist/agent/index.d.ts +1 -1
  12. package/dist/agent/index.js +2 -2
  13. package/dist/agent/types.d.ts +1 -1
  14. package/dist/agent/types.js +1 -1
  15. package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
  16. package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
  17. package/dist/ai-sdk/coordination-context.d.ts +9 -9
  18. package/dist/ai-sdk/coordination-context.js +8 -8
  19. package/dist/ai-sdk/index.d.ts +1 -1
  20. package/dist/ai-sdk/index.js +1 -1
  21. package/dist/ai-sdk/wrap.d.ts +4 -4
  22. package/dist/ai-sdk/wrap.js +4 -4
  23. package/dist/api/index.d.ts +2 -2
  24. package/dist/cli.cjs +369 -67
  25. package/dist/client/Ablo.d.ts +30 -63
  26. package/dist/client/Ablo.js +124 -103
  27. package/dist/client/ApiClient.d.ts +6 -5
  28. package/dist/client/ApiClient.js +86 -62
  29. package/dist/client/auth.d.ts +9 -4
  30. package/dist/client/auth.js +40 -5
  31. package/dist/client/createModelProxy.d.ts +41 -54
  32. package/dist/client/createModelProxy.js +123 -20
  33. package/dist/client/httpClient.d.ts +2 -0
  34. package/dist/client/httpClient.js +1 -1
  35. package/dist/client/index.d.ts +3 -3
  36. package/dist/client/writeOptionsSchema.d.ts +4 -4
  37. package/dist/client/writeOptionsSchema.js +4 -4
  38. package/dist/coordination/schema.d.ts +249 -38
  39. package/dist/coordination/schema.js +172 -39
  40. package/dist/core/index.d.ts +2 -2
  41. package/dist/core/index.js +4 -4
  42. package/dist/errorCodes.d.ts +9 -9
  43. package/dist/errorCodes.js +16 -16
  44. package/dist/errors.d.ts +51 -2
  45. package/dist/errors.js +94 -5
  46. package/dist/interfaces/index.d.ts +8 -4
  47. package/dist/policy/index.d.ts +1 -1
  48. package/dist/policy/types.d.ts +13 -13
  49. package/dist/policy/types.js +8 -8
  50. package/dist/react/AbloProvider.d.ts +51 -4
  51. package/dist/react/AbloProvider.js +95 -11
  52. package/dist/react/context.d.ts +26 -9
  53. package/dist/react/context.js +2 -2
  54. package/dist/react/index.d.ts +4 -4
  55. package/dist/react/index.js +4 -4
  56. package/dist/react/useAblo.js +5 -5
  57. package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
  58. package/dist/react/useClaim.js +42 -0
  59. package/dist/schema/index.js +1 -1
  60. package/dist/schema/schema.d.ts +3 -3
  61. package/dist/schema/sugar.d.ts +3 -3
  62. package/dist/schema/sugar.js +3 -3
  63. package/dist/schema/sync-delta-wire.d.ts +8 -8
  64. package/dist/server/commit.d.ts +2 -2
  65. package/dist/sync/AreaOfInterestManager.d.ts +162 -0
  66. package/dist/sync/AreaOfInterestManager.js +233 -0
  67. package/dist/sync/BootstrapHelper.d.ts +9 -1
  68. package/dist/sync/BootstrapHelper.js +15 -5
  69. package/dist/sync/NetworkProbe.d.ts +1 -1
  70. package/dist/sync/NetworkProbe.js +1 -1
  71. package/dist/sync/SyncWebSocket.d.ts +59 -25
  72. package/dist/sync/SyncWebSocket.js +123 -26
  73. package/dist/sync/awaitClaimGrant.d.ts +40 -0
  74. package/dist/sync/awaitClaimGrant.js +86 -0
  75. package/dist/sync/createClaimStream.d.ts +34 -0
  76. package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
  77. package/dist/sync/createPresenceStream.js +3 -2
  78. package/dist/sync/participants.d.ts +10 -10
  79. package/dist/sync/participants.js +17 -10
  80. package/dist/sync/schemas.d.ts +8 -8
  81. package/dist/transactions/TransactionQueue.d.ts +23 -0
  82. package/dist/transactions/TransactionQueue.js +186 -12
  83. package/dist/types/global.d.ts +18 -13
  84. package/dist/types/global.js +11 -6
  85. package/dist/types/index.d.ts +9 -7
  86. package/dist/types/index.js +2 -2
  87. package/dist/types/streams.d.ts +114 -98
  88. package/dist/types/streams.js +1 -1
  89. package/dist/utils/asyncIterator.d.ts +1 -1
  90. package/dist/utils/asyncIterator.js +1 -1
  91. package/dist/wire/frames.d.ts +2 -2
  92. package/docs/api.md +3 -3
  93. package/docs/client-behavior.md +6 -3
  94. package/docs/coordination.md +13 -3
  95. package/docs/data-sources.md +29 -9
  96. package/docs/migration.md +40 -0
  97. package/docs/quickstart.md +61 -33
  98. package/docs/react.md +46 -0
  99. package/llms-full.txt +25 -8
  100. package/llms.txt +11 -9
  101. package/package.json +3 -2
  102. package/dist/react/useIntent.js +0 -42
  103. package/dist/sync/awaitIntentGrant.d.ts +0 -40
  104. package/dist/sync/awaitIntentGrant.js +0 -62
  105. package/dist/sync/createIntentStream.d.ts +0 -34
@@ -225,6 +225,7 @@ export class TransactionQueue extends EventEmitter {
225
225
  // Per-model in-flight tracking and merge buffer
226
226
  inFlightByModel = new Set();
227
227
  pendingMergeByModel = new Map();
228
+ deferredDeletesByCreate = new Map();
228
229
  // Commit lane: pre-built atomic multi-op envelopes from `ablo.commits.create()`.
229
230
  // Drained serially (one envelope at a time) since each is atomic; no
230
231
  // coalescing with model-proxy transactions.
@@ -242,6 +243,102 @@ export class TransactionQueue extends EventEmitter {
242
243
  transaction.priorityScore = this.computePriorityScore(transaction.type, transaction.modelName);
243
244
  }
244
245
  }
246
+ entityKey(modelName, modelId) {
247
+ return `${modelName}:${modelId}`;
248
+ }
249
+ isTransactionForModel(transaction, modelName, modelId) {
250
+ return transaction.modelName === modelName && transaction.modelId === modelId;
251
+ }
252
+ resolveConfirmation(transaction) {
253
+ const resolver = this.confirmationResolvers.get(transaction.id);
254
+ if (!resolver)
255
+ return;
256
+ this.confirmationResolvers.delete(transaction.id);
257
+ resolver.resolve();
258
+ }
259
+ takeUnsentCreateForModel(modelName, modelId) {
260
+ const isUnsentCreate = (tx) => tx.type === 'create' &&
261
+ tx.status === 'pending' &&
262
+ tx.attempts === 0 &&
263
+ this.isTransactionForModel(tx, modelName, modelId);
264
+ const stagedIndex = this.createdTransactions.findIndex(isUnsentCreate);
265
+ if (stagedIndex >= 0) {
266
+ return this.createdTransactions.splice(stagedIndex, 1)[0];
267
+ }
268
+ const queuedIndex = this.executionQueue.findIndex(isUnsentCreate);
269
+ if (queuedIndex >= 0) {
270
+ return this.executionQueue.splice(queuedIndex, 1)[0];
271
+ }
272
+ return this.store.getByStatus('pending').find(isUnsentCreate);
273
+ }
274
+ async cancelUnsentCreateForDelete(transaction) {
275
+ this.store.updateStatus(transaction.id, 'rolled_back');
276
+ if (this.config.enableOptimistic) {
277
+ await this.rollbackOptimistic(transaction, 'model_cancelled');
278
+ }
279
+ this.resolveConfirmation(transaction);
280
+ }
281
+ findCreateBarrierForDelete(modelName, modelId) {
282
+ const liveCreates = [
283
+ ...this.store.getByStatus('pending'),
284
+ ...this.store.getByStatus('executing'),
285
+ ...this.store.getByStatus('awaiting_delta'),
286
+ ].filter((tx) => tx.type === 'create' &&
287
+ this.isTransactionForModel(tx, modelName, modelId) &&
288
+ // A never-attempted pending create can be cancelled instead. Once the
289
+ // create has been sent, even a retry-pending state is a causal barrier:
290
+ // the server may already have applied it and only the response was lost.
291
+ (tx.status !== 'pending' || tx.attempts > 0));
292
+ return liveCreates.sort((a, b) => b.createdAt - a.createdAt)[0];
293
+ }
294
+ completeLocalDelete(model, context, writeOptions) {
295
+ const actualModelName = model.getModelName();
296
+ const modelKey = normalizeModelKey(actualModelName);
297
+ const transaction = {
298
+ id: this.generateId(),
299
+ type: 'delete',
300
+ modelName: actualModelName,
301
+ modelId: model.id,
302
+ modelKey,
303
+ priorityScore: this.computePriorityScore('delete', actualModelName),
304
+ previousData: model.toJSON ? model.toJSON() : { ...model },
305
+ context,
306
+ status: 'completed',
307
+ createdAt: Date.now(),
308
+ attempts: 0,
309
+ priority: 'high',
310
+ writeOptions,
311
+ localOnly: true,
312
+ };
313
+ this.attachConfirmation(transaction);
314
+ this.store.add(transaction);
315
+ if (this.config.enableOptimistic) {
316
+ this.applyOptimisticDelete(model, transaction);
317
+ }
318
+ this.emit('transaction:created', transaction);
319
+ this.emit('transaction:completed', transaction);
320
+ this.emit(`transaction:completed:${transaction.id}`, transaction);
321
+ this.optimisticUpdates.delete(transaction.id);
322
+ return transaction;
323
+ }
324
+ deferDeleteUntilCreateSettles(createTransaction, deleteTransaction) {
325
+ const key = this.entityKey(createTransaction.modelName, createTransaction.modelId);
326
+ const deferred = this.deferredDeletesByCreate.get(key) ?? [];
327
+ deferred.push(deleteTransaction);
328
+ this.deferredDeletesByCreate.set(key, deferred);
329
+ }
330
+ releaseDeferredDeletesForCreate(createTransaction) {
331
+ const key = this.entityKey(createTransaction.modelName, createTransaction.modelId);
332
+ const deferred = this.deferredDeletesByCreate.get(key);
333
+ if (!deferred || deferred.length === 0)
334
+ return;
335
+ this.deferredDeletesByCreate.delete(key);
336
+ for (const deleteTransaction of deferred) {
337
+ if (this.store.get(deleteTransaction.id)?.status !== 'pending')
338
+ continue;
339
+ this.enqueue(deleteTransaction);
340
+ }
341
+ }
245
342
  // Merge two GraphQL update payloads with special handling for metadata fields
246
343
  mergeUpdateData(left, right, _modelName) {
247
344
  const out = { ...(left || {}) };
@@ -374,6 +471,9 @@ export class TransactionQueue extends EventEmitter {
374
471
  this.confirmationResolvers.delete(tx.id);
375
472
  r.resolve();
376
473
  }
474
+ if (tx.type === 'create') {
475
+ this.releaseDeferredDeletesForCreate(tx);
476
+ }
377
477
  });
378
478
  this.on('transaction:failed', ({ transaction, error }) => {
379
479
  const r = this.confirmationResolvers.get(transaction.id);
@@ -381,6 +481,9 @@ export class TransactionQueue extends EventEmitter {
381
481
  this.confirmationResolvers.delete(transaction.id);
382
482
  r.reject(error);
383
483
  }
484
+ if (transaction.type === 'create') {
485
+ this.releaseDeferredDeletesForCreate(transaction);
486
+ }
384
487
  });
385
488
  }
386
489
  /**
@@ -670,6 +773,15 @@ export class TransactionQueue extends EventEmitter {
670
773
  ? this.mapChangesToInput(actualModelName, precomputedChanges)
671
774
  : this.extractUpdateData(model);
672
775
  const previousData = this.extractPreviousData(model, updateInput);
776
+ // Advance the per-field baseline for the keys we just froze into this
777
+ // transaction. `Model.propertyChanged` is first-old-wins and only cleared on
778
+ // sync-ack, so without this a SECOND update to the same field before the
779
+ // first acks would re-capture the original `.old` (the pre-session value)
780
+ // instead of THIS update's result — corrupting the stream-recorded undo
781
+ // inverse (the second move's "before" would point all the way back). The
782
+ // wire payload is already frozen in `transaction.data`, so dropping the
783
+ // consumed entries is safe. Mirrors `RecordingTransaction.consumeModifiedFields`.
784
+ this.consumeModifiedFields(model, updateInput);
673
785
  const modelKey = normalizeModelKey(actualModelName);
674
786
  const priorityScore = this.computePriorityScore('update', actualModelName);
675
787
  const transaction = {
@@ -726,6 +838,7 @@ export class TransactionQueue extends EventEmitter {
726
838
  attempts: 0,
727
839
  priority: 'high',
728
840
  writeOptions,
841
+ localOnly: true,
729
842
  // Activity deletes complete synchronously (audit-record skip path).
730
843
  // Pre-resolved so consumers can still `await tx.confirmation` uniformly.
731
844
  confirmation: Promise.resolve(),
@@ -740,6 +853,11 @@ export class TransactionQueue extends EventEmitter {
740
853
  }
741
854
  const modelKey = normalizeModelKey(actualModelName);
742
855
  const priorityScore = this.computePriorityScore('delete', actualModelName);
856
+ const unsentCreate = this.takeUnsentCreateForModel(actualModelName, model.id);
857
+ if (unsentCreate) {
858
+ await this.cancelUnsentCreateForDelete(unsentCreate);
859
+ return this.completeLocalDelete(model, context, writeOptions);
860
+ }
743
861
  const transaction = {
744
862
  id: this.generateId(),
745
863
  type: 'delete',
@@ -760,15 +878,22 @@ export class TransactionQueue extends EventEmitter {
760
878
  // Cancel any pending/in-flight updates for this model to prevent "no rows" errors
761
879
  // when the delete executes before the update (race condition fix)
762
880
  this.cancelTransactionsForModel(model.id, 'update');
763
- this.pendingMergeByModel.delete(`${actualModelName}:${model.id}`);
764
- this.inFlightByModel.delete(`${actualModelName}:${model.id}`);
881
+ const entityKey = this.entityKey(actualModelName, model.id);
882
+ this.pendingMergeByModel.delete(entityKey);
883
+ this.inFlightByModel.delete(entityKey);
765
884
  // Apply optimistic delete
766
885
  if (this.config.enableOptimistic) {
767
886
  this.applyOptimisticDelete(model, transaction);
768
887
  }
769
- // LINEAR PATTERN: Stage transaction for microtask commit
770
- // All deletes in same event loop will be batched together
771
- this.stageTransaction(transaction);
888
+ const createBarrier = this.findCreateBarrierForDelete(actualModelName, model.id);
889
+ if (createBarrier) {
890
+ this.deferDeleteUntilCreateSettles(createBarrier, transaction);
891
+ }
892
+ else {
893
+ // LINEAR PATTERN: Stage transaction for microtask commit
894
+ // All deletes in same event loop will be batched together
895
+ this.stageTransaction(transaction);
896
+ }
772
897
  this.emit('transaction:created', transaction);
773
898
  return transaction;
774
899
  }
@@ -1015,7 +1140,7 @@ export class TransactionQueue extends EventEmitter {
1015
1140
  tx.syncIdNeededForCompletion = lastSyncId;
1016
1141
  // Safety net: when lastSyncId is 0, DELETE transactions should be confirmed
1017
1142
  // immediately. DELETEs are idempotent — if no delta was emitted, the entity
1018
- // is already gone and the intent was achieved. Parking DELETEs in awaiting_delta
1143
+ // is already gone and the claim was achieved. Parking DELETEs in awaiting_delta
1019
1144
  // with threshold 0 causes 30s reconciliation delays.
1020
1145
  if (lastSyncId === 0 && tx.type === 'delete') {
1021
1146
  this.store.updateStatus(tx.id, 'completed');
@@ -1110,14 +1235,14 @@ export class TransactionQueue extends EventEmitter {
1110
1235
  });
1111
1236
  // LINEAR PATTERN: Handle "no rows in result set" gracefully
1112
1237
  // This error means the entity was already deleted - for UPDATE/DELETE ops, this is success
1113
- // The intent was achieved (the data doesn't exist), so treat as completed
1238
+ // The claim was achieved (the data doesn't exist), so treat as completed
1114
1239
  if (errorMessage.includes('no rows in result set')) {
1115
1240
  getContext().logger.info('[TransactionQueue] Graceful handling: entity already deleted', {
1116
1241
  batchSize: batchOps.length,
1117
1242
  });
1118
1243
  for (const { tx, op } of batchOps) {
1119
1244
  if (op.type === 'UPDATE' || op.type === 'DELETE') {
1120
- // Entity gone = intent achieved, mark as completed
1245
+ // Entity gone = claim achieved, mark as completed
1121
1246
  this.store.updateStatus(tx.id, 'completed');
1122
1247
  this.emit('transaction:completed', tx);
1123
1248
  getContext().logger.debug('[TransactionQueue] Orphaned transaction treated as success', {
@@ -1875,16 +2000,63 @@ export class TransactionQueue extends EventEmitter {
1875
2000
  // expose a typed `getPreviousData()` accessor on Model and call that.
1876
2001
  extractPreviousData(model, updateInput) {
1877
2002
  const prev = { id: model.id };
1878
- if (model.modifiedProperties instanceof Map && model.modifiedProperties.size > 0) {
1879
- for (const [key, change] of model.modifiedProperties) {
1880
- // Only include keys that are part of this update if provided
1881
- if (updateInput && !(key in updateInput))
2003
+ const modified = model.modifiedProperties instanceof Map ? model.modifiedProperties : null;
2004
+ // When the update's written keys are known, capture a before-image for
2005
+ // EXACTLY those keys so the recorded undo inverse can revert them and only
2006
+ // them (a full-row inverse would clobber concurrent edits to unrelated
2007
+ // fields). Resolution order mirrors `RecordingTransaction.snapshotFields`:
2008
+ // 1. `modifiedProperties.old` — first-old-wins pre-session baseline, set
2009
+ // whenever the caller mutated the field in place before committing.
2010
+ // 2. `getOriginalSnapshot()` — the last loaded/acked row, the correct
2011
+ // before-image for a key written WITHOUT a prior in-place mutation
2012
+ // (e.g. a `precomputedChanges` write).
2013
+ // Without (2) such a key yields an empty `previousData`, and `buildUndoOps`
2014
+ // nulls the inverse entirely — making updates silently un-undoable where a
2015
+ // create's `delete(id)` inverse never is. This closes that asymmetry.
2016
+ if (updateInput) {
2017
+ const original = model.getOriginalSnapshot();
2018
+ for (const key of Object.keys(updateInput)) {
2019
+ if (key === 'id')
1882
2020
  continue;
2021
+ const mod = modified?.get(key);
2022
+ if (mod) {
2023
+ prev[key] = mod.old;
2024
+ }
2025
+ else if (original && key in original) {
2026
+ prev[key] = original[key];
2027
+ }
2028
+ }
2029
+ return prev;
2030
+ }
2031
+ if (modified && modified.size > 0) {
2032
+ for (const [key, change] of modified) {
1883
2033
  prev[key] = change.old;
1884
2034
  }
1885
2035
  }
1886
2036
  return prev;
1887
2037
  }
2038
+ /**
2039
+ * Re-baseline `modifiedProperties` for the fields a freshly-staged update just
2040
+ * committed. Called right after {@link extractPreviousData} freezes their
2041
+ * `.old` into the transaction, so the NEXT update to the same field sees this
2042
+ * update's result as its baseline rather than the stale pre-session `.old`
2043
+ * preserved by `Model.propertyChanged`'s first-old-wins policy. Only consumes
2044
+ * keys present in this update — untouched fields keep their baselines. Safe
2045
+ * because the wire payload lives on `transaction.data` and rollback restores
2046
+ * from `transaction.previousData`; neither re-reads `modifiedProperties`.
2047
+ */
2048
+ consumeModifiedFields(model, updateInput) {
2049
+ if (!(model.modifiedProperties instanceof Map) || model.modifiedProperties.size === 0) {
2050
+ return;
2051
+ }
2052
+ for (const key of [...model.modifiedProperties.keys()]) {
2053
+ if (key === 'id')
2054
+ continue;
2055
+ if (updateInput && !(key in updateInput))
2056
+ continue;
2057
+ model.modifiedProperties.delete(key);
2058
+ }
2059
+ }
1888
2060
  /**
1889
2061
  * Public API
1890
2062
  */
@@ -1973,6 +2145,8 @@ export class TransactionQueue extends EventEmitter {
1973
2145
  this.store.clear();
1974
2146
  this.optimisticUpdates.clear();
1975
2147
  this.executionQueue = [];
2148
+ this.createdTransactions = [];
2149
+ this.deferredDeletesByCreate.clear();
1976
2150
  // Clear event listeners
1977
2151
  this.removeAllListeners();
1978
2152
  // Reset state
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Type registration point for SDK consumers.
3
3
  *
4
- * A consumer registers their Schema, Presence, Intents, and UserMeta ONCE by
4
+ * A consumer registers their Schema, Presence, Claims, and UserMeta ONCE by
5
5
  * augmenting the {@link Register} interface, and every SDK hook — `useAblo`,
6
- * `useQuery`, `useOne`, `usePresence`, `useIntent` — reads its types from the
6
+ * `useQuery`, `useOne`, `usePresence`, `useClaim` — reads its types from the
7
7
  * resolved registration. No generics at call sites, no `schema` arg per call.
8
8
  *
9
9
  * Registration is done via **module augmentation** of `@abloatai/ablo` —
@@ -12,17 +12,22 @@
12
12
  * global, not prefixed). It's a language feature, not a library trick: any file
13
13
  * in the compilation can augment it and every resolver below picks it up.
14
14
  *
15
- * Consumer example:
15
+ * Consumer example (`npx ablo init` scaffolds this as `ablo/register.ts`, a
16
+ * sibling of `ablo/schema.ts`). It's a regular `.ts` module, NOT a hand-authored
17
+ * `.d.ts`: the top-level `import type { schema }` makes the `declare module`
18
+ * block MERGE (augment) this interface rather than collide with it — the same
19
+ * shape TanStack Router uses in `src/router.tsx`. Any `.ts` file in the
20
+ * `tsconfig` `include` works; it never needs to be imported.
16
21
  *
17
22
  * ```ts
18
- * // apps/your-app/src/ablo.d.ts
19
- * import type { schema } from './your-schema';
23
+ * // ablo/register.ts
24
+ * import type { schema } from './schema';
20
25
  *
21
26
  * declare module '@abloatai/ablo' {
22
27
  * interface Register {
23
28
  * Schema: typeof schema;
24
29
  * Presence: { cursor: { x: number; y: number } | null };
25
- * Intents: { editLayer: { layerId: string } };
30
+ * Claims: { editLayer: { layerId: string } };
26
31
  * UserMeta: { id: string; email: string };
27
32
  * }
28
33
  * }
@@ -44,7 +49,7 @@ export interface DefaultSyncShape {
44
49
  readonly models: Record<string, unknown>;
45
50
  };
46
51
  readonly Presence: Record<string, unknown>;
47
- readonly Intents: Record<string, unknown>;
52
+ readonly Claims: Record<string, unknown>;
48
53
  readonly UserMeta: {
49
54
  readonly id: string;
50
55
  };
@@ -75,13 +80,13 @@ export type ResolvePresence = Register extends {
75
80
  Presence: infer P;
76
81
  } ? P : DefaultSyncShape['Presence'];
77
82
  /**
78
- * The consumer's intent vocabulary, or the default if unregistered. Keys are
79
- * intent names; values are the claim payload for each intent. Used by
80
- * `useIntent(intentName)`.
83
+ * The consumer's claim vocabulary, or the default if unregistered. Keys are
84
+ * claim names; values are the claim payload for each claim. Used by
85
+ * `useClaim(claimName)`.
81
86
  */
82
- export type ResolveIntents = Register extends {
83
- Intents: infer I;
84
- } ? I : DefaultSyncShape['Intents'];
87
+ export type ResolveClaims = Register extends {
88
+ Claims: infer I;
89
+ } ? I : DefaultSyncShape['Claims'];
85
90
  /**
86
91
  * The consumer's user-metadata shape, or the default if unregistered. Carries
87
92
  * identity info the consumer trusts from their auth layer — not SDK-validated.
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Type registration point for SDK consumers.
3
3
  *
4
- * A consumer registers their Schema, Presence, Intents, and UserMeta ONCE by
4
+ * A consumer registers their Schema, Presence, Claims, and UserMeta ONCE by
5
5
  * augmenting the {@link Register} interface, and every SDK hook — `useAblo`,
6
- * `useQuery`, `useOne`, `usePresence`, `useIntent` — reads its types from the
6
+ * `useQuery`, `useOne`, `usePresence`, `useClaim` — reads its types from the
7
7
  * resolved registration. No generics at call sites, no `schema` arg per call.
8
8
  *
9
9
  * Registration is done via **module augmentation** of `@abloatai/ablo` —
@@ -12,17 +12,22 @@
12
12
  * global, not prefixed). It's a language feature, not a library trick: any file
13
13
  * in the compilation can augment it and every resolver below picks it up.
14
14
  *
15
- * Consumer example:
15
+ * Consumer example (`npx ablo init` scaffolds this as `ablo/register.ts`, a
16
+ * sibling of `ablo/schema.ts`). It's a regular `.ts` module, NOT a hand-authored
17
+ * `.d.ts`: the top-level `import type { schema }` makes the `declare module`
18
+ * block MERGE (augment) this interface rather than collide with it — the same
19
+ * shape TanStack Router uses in `src/router.tsx`. Any `.ts` file in the
20
+ * `tsconfig` `include` works; it never needs to be imported.
16
21
  *
17
22
  * ```ts
18
- * // apps/your-app/src/ablo.d.ts
19
- * import type { schema } from './your-schema';
23
+ * // ablo/register.ts
24
+ * import type { schema } from './schema';
20
25
  *
21
26
  * declare module '@abloatai/ablo' {
22
27
  * interface Register {
23
28
  * Schema: typeof schema;
24
29
  * Presence: { cursor: { x: number; y: number } | null };
25
- * Intents: { editLayer: { layerId: string } };
30
+ * Claims: { editLayer: { layerId: string } };
26
31
  * UserMeta: { id: string; email: string };
27
32
  * }
28
33
  * }
@@ -4,6 +4,7 @@
4
4
  * Foundational type definitions for the model-driven sync architecture.
5
5
  * These types define how properties are tracked, loaded, and synchronized.
6
6
  */
7
+ import type { FieldMeta } from '../schema/field.js';
7
8
  /**
8
9
  * Model Scope - lifecycle filter for queries.
9
10
  * Controls whether live, archived, or all entities are returned.
@@ -108,14 +109,15 @@ export interface ModelMetadata {
108
109
  * JSON-typed values) inside the transaction queue.
109
110
  *
110
111
  * Populated by `registerModelsFromSchema`. Each entry carries the
111
- * sync-engine type tag (`'string' | 'number' | 'boolean' | 'date' |
112
- * 'enum' | 'json'`), which tells the wire serializer how to handle
113
- * the value. Missing → projection becomes identity pass-through
114
- * (back-compat for models registered outside the schema path).
112
+ * sync-engine type tag (the canonical {@link FieldMeta.type} union),
113
+ * which tells the wire serializer how to handle the value. Missing →
114
+ * projection becomes identity pass-through (back-compat for models
115
+ * registered outside the schema path).
116
+ *
117
+ * Narrowed to the canonical union via `Pick` rather than re-declared —
118
+ * a hand-rolled copy silently drifts when a new field type lands.
115
119
  */
116
- fields?: Readonly<Record<string, {
117
- type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'json';
118
- }>>;
120
+ fields?: Readonly<Record<string, Pick<FieldMeta, 'type'>>>;
119
121
  /**
120
122
  * Fields to back-fill from the sync client identity when missing
121
123
  * during IndexedDB self-healing. Populated from
@@ -65,6 +65,6 @@ export var MutationOperationType;
65
65
  })(MutationOperationType || (MutationOperationType = {}));
66
66
  // Re-export stream + snapshot + principal types for the engine surface
67
67
  // (PresenceStream,
68
- // IntentStream, Snapshot, etc.) consumed by `Ablo({...}).presence`,
69
- // `.intents`, `.snapshot()`.
68
+ // ClaimStream, Snapshot, etc.) consumed by `Ablo({...}).presence`,
69
+ // `.claims`, `.snapshot()`.
70
70
  export * from "./streams.js";