@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.
- package/CHANGELOG.md +34 -0
- package/README.md +63 -23
- package/dist/BaseSyncedStore.d.ts +75 -0
- package/dist/BaseSyncedStore.js +193 -8
- package/dist/Database.d.ts +10 -2
- package/dist/Database.js +15 -1
- package/dist/SyncClient.d.ts +12 -1
- package/dist/SyncClient.js +110 -26
- package/dist/agent/Agent.d.ts +9 -9
- package/dist/agent/Agent.js +16 -16
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
- package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
- package/dist/ai-sdk/coordination-context.d.ts +9 -9
- package/dist/ai-sdk/coordination-context.js +8 -8
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +4 -4
- package/dist/ai-sdk/wrap.js +4 -4
- package/dist/api/index.d.ts +2 -2
- package/dist/cli.cjs +369 -67
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +124 -103
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +86 -62
- package/dist/client/auth.d.ts +9 -4
- package/dist/client/auth.js +40 -5
- package/dist/client/createModelProxy.d.ts +41 -54
- package/dist/client/createModelProxy.js +123 -20
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/httpClient.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/writeOptionsSchema.d.ts +4 -4
- package/dist/client/writeOptionsSchema.js +4 -4
- package/dist/coordination/schema.d.ts +249 -38
- package/dist/coordination/schema.js +172 -39
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/errorCodes.d.ts +9 -9
- package/dist/errorCodes.js +16 -16
- package/dist/errors.d.ts +51 -2
- package/dist/errors.js +94 -5
- package/dist/interfaces/index.d.ts +8 -4
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/types.d.ts +13 -13
- package/dist/policy/types.js +8 -8
- package/dist/react/AbloProvider.d.ts +51 -4
- package/dist/react/AbloProvider.js +95 -11
- package/dist/react/context.d.ts +26 -9
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +4 -4
- package/dist/react/useAblo.js +5 -5
- package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
- package/dist/react/useClaim.js +42 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/sugar.d.ts +3 -3
- package/dist/schema/sugar.js +3 -3
- package/dist/schema/sync-delta-wire.d.ts +8 -8
- package/dist/server/commit.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.d.ts +162 -0
- package/dist/sync/AreaOfInterestManager.js +233 -0
- package/dist/sync/BootstrapHelper.d.ts +9 -1
- package/dist/sync/BootstrapHelper.js +15 -5
- package/dist/sync/NetworkProbe.d.ts +1 -1
- package/dist/sync/NetworkProbe.js +1 -1
- package/dist/sync/SyncWebSocket.d.ts +59 -25
- package/dist/sync/SyncWebSocket.js +123 -26
- package/dist/sync/awaitClaimGrant.d.ts +40 -0
- package/dist/sync/awaitClaimGrant.js +86 -0
- package/dist/sync/createClaimStream.d.ts +34 -0
- package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
- package/dist/sync/createPresenceStream.js +3 -2
- package/dist/sync/participants.d.ts +10 -10
- package/dist/sync/participants.js +17 -10
- package/dist/sync/schemas.d.ts +8 -8
- package/dist/transactions/TransactionQueue.d.ts +23 -0
- package/dist/transactions/TransactionQueue.js +186 -12
- package/dist/types/global.d.ts +18 -13
- package/dist/types/global.js +11 -6
- package/dist/types/index.d.ts +9 -7
- package/dist/types/index.js +2 -2
- package/dist/types/streams.d.ts +114 -98
- package/dist/types/streams.js +1 -1
- package/dist/utils/asyncIterator.d.ts +1 -1
- package/dist/utils/asyncIterator.js +1 -1
- package/dist/wire/frames.d.ts +2 -2
- package/docs/api.md +3 -3
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +13 -3
- package/docs/data-sources.md +29 -9
- package/docs/migration.md +40 -0
- package/docs/quickstart.md +61 -33
- package/docs/react.md +46 -0
- package/llms-full.txt +25 -8
- package/llms.txt +11 -9
- package/package.json +3 -2
- package/dist/react/useIntent.js +0 -42
- package/dist/sync/awaitIntentGrant.d.ts +0 -40
- package/dist/sync/awaitIntentGrant.js +0 -62
- 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.
|
|
764
|
-
this.
|
|
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
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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
|
package/dist/types/global.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type registration point for SDK consumers.
|
|
3
3
|
*
|
|
4
|
-
* A consumer registers their Schema, Presence,
|
|
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`, `
|
|
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
|
-
* //
|
|
19
|
-
* import type { schema } from './
|
|
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
|
-
*
|
|
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
|
|
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
|
|
79
|
-
*
|
|
80
|
-
* `
|
|
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
|
|
83
|
-
|
|
84
|
-
} ? I : DefaultSyncShape['
|
|
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.
|
package/dist/types/global.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type registration point for SDK consumers.
|
|
3
3
|
*
|
|
4
|
-
* A consumer registers their Schema, Presence,
|
|
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`, `
|
|
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
|
-
* //
|
|
19
|
-
* import type { schema } from './
|
|
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
|
-
*
|
|
30
|
+
* Claims: { editLayer: { layerId: string } };
|
|
26
31
|
* UserMeta: { id: string; email: string };
|
|
27
32
|
* }
|
|
28
33
|
* }
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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 (
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
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
|
package/dist/types/index.js
CHANGED
|
@@ -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
|
-
//
|
|
69
|
-
// `.
|
|
68
|
+
// ClaimStream, Snapshot, etc.) consumed by `Ablo({...}).presence`,
|
|
69
|
+
// `.claims`, `.snapshot()`.
|
|
70
70
|
export * from "./streams.js";
|