@abloatai/ablo 0.27.0 → 0.28.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 (73) hide show
  1. package/CHANGELOG.md +29 -3
  2. package/README.md +1 -1
  3. package/dist/BaseSyncedStore.js +8 -9
  4. package/dist/Database.d.ts +14 -1
  5. package/dist/Database.js +225 -28
  6. package/dist/Model.d.ts +17 -0
  7. package/dist/Model.js +32 -1
  8. package/dist/SyncClient.d.ts +10 -6
  9. package/dist/SyncClient.js +379 -76
  10. package/dist/adapters/inMemoryStorage.d.ts +1 -0
  11. package/dist/adapters/inMemoryStorage.js +12 -0
  12. package/dist/cli.cjs +6 -2
  13. package/dist/client/Ablo.d.ts +24 -1
  14. package/dist/client/Ablo.js +4 -3
  15. package/dist/client/ApiClient.js +309 -43
  16. package/dist/client/createInternalComponents.d.ts +2 -0
  17. package/dist/client/createInternalComponents.js +1 -1
  18. package/dist/client/httpClient.d.ts +2 -0
  19. package/dist/client/modelRegistration.js +11 -0
  20. package/dist/client/options.d.ts +23 -0
  21. package/dist/client/wsMutationExecutor.d.ts +3 -2
  22. package/dist/client/wsMutationExecutor.js +3 -2
  23. package/dist/commit/contract.d.ts +493 -0
  24. package/dist/commit/contract.js +187 -0
  25. package/dist/commit/index.d.ts +6 -0
  26. package/dist/commit/index.js +5 -0
  27. package/dist/core/StoreManager.d.ts +2 -0
  28. package/dist/core/StoreManager.js +12 -0
  29. package/dist/errorCodes.js +6 -2
  30. package/dist/index.d.ts +6 -0
  31. package/dist/index.js +2 -0
  32. package/dist/interfaces/index.d.ts +2 -2
  33. package/dist/mutators/UndoManager.d.ts +2 -0
  34. package/dist/mutators/UndoManager.js +32 -0
  35. package/dist/react/useAblo.d.ts +6 -4
  36. package/dist/react/useAblo.js +25 -3
  37. package/dist/schema/index.d.ts +1 -1
  38. package/dist/schema/schema.d.ts +31 -1
  39. package/dist/stores/ObjectStore.d.ts +14 -1
  40. package/dist/stores/ObjectStore.js +27 -4
  41. package/dist/stores/ObjectStoreContract.d.ts +2 -0
  42. package/dist/surface.d.ts +1 -1
  43. package/dist/surface.js +2 -0
  44. package/dist/sync/SyncWebSocket.d.ts +2 -1
  45. package/dist/sync/persistedPrefix.d.ts +12 -0
  46. package/dist/sync/persistedPrefix.js +22 -0
  47. package/dist/testing/index.d.ts +2 -0
  48. package/dist/testing/index.js +1 -0
  49. package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
  50. package/dist/testing/mocks/FakeDatabase.js +10 -0
  51. package/dist/testing/mocks/MockWebSocket.d.ts +2 -1
  52. package/dist/transactions/TransactionQueue.d.ts +66 -8
  53. package/dist/transactions/TransactionQueue.js +607 -89
  54. package/dist/transactions/commitEnvelope.d.ts +132 -0
  55. package/dist/transactions/commitEnvelope.js +139 -0
  56. package/dist/transactions/commitOutboxStore.d.ts +32 -0
  57. package/dist/transactions/commitOutboxStore.js +26 -0
  58. package/dist/transactions/commitPayload.d.ts +15 -0
  59. package/dist/transactions/commitPayload.js +6 -0
  60. package/dist/transactions/httpCommitEnvelope.d.ts +43 -0
  61. package/dist/transactions/httpCommitEnvelope.js +179 -0
  62. package/dist/transactions/replayValidation.d.ts +83 -0
  63. package/dist/transactions/replayValidation.js +46 -1
  64. package/dist/wire/bootstrapReason.d.ts +9 -0
  65. package/dist/wire/bootstrapReason.js +8 -0
  66. package/dist/wire/frames.d.ts +236 -0
  67. package/dist/wire/frames.js +21 -0
  68. package/dist/wire/index.d.ts +4 -2
  69. package/dist/wire/index.js +2 -1
  70. package/docs/api.md +10 -10
  71. package/docs/coordination.md +2 -2
  72. package/docs/mcp.md +1 -1
  73. package/package.json +7 -2
@@ -9,6 +9,7 @@
9
9
  import { EventEmitter } from 'events';
10
10
  import type { MutationOperation } from '../interfaces/index.js';
11
11
  import { type ClientSyncDelta } from '../wire/delta.js';
12
+ import type { BootstrapReason } from '../wire/bootstrapReason.js';
12
13
  import type { ClaimError, ClaimRejection, StaleNotification, ReadDependency } from '../coordination/schema.js';
13
14
  import { type CommitAck } from './commitFrames.js';
14
15
  export type { CommitAck } from './commitFrames.js';
@@ -100,7 +101,7 @@ export interface SyncWebSocketOptions {
100
101
  */
101
102
  export interface BootstrapHint {
102
103
  tables?: string[];
103
- reason?: 'too_far_behind' | 'too_many_deltas' | 'missing_entities';
104
+ reason?: BootstrapReason;
104
105
  staleTables?: string[];
105
106
  totalDeltaCount?: number;
106
107
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns the highest sync id in the durable prefix of an ordered delta frame.
3
+ *
4
+ * A frame may span several IndexedDB stores, each committed in its own
5
+ * transaction. Those transactions can succeed and fail independently. The
6
+ * resume cursor may advance only through the first failed input: taking the
7
+ * maximum id from any successful store would skip an earlier failed delta on
8
+ * every later catch-up.
9
+ */
10
+ export declare function highestPersistedPrefixSyncId(deltas: readonly {
11
+ readonly syncId?: number | string;
12
+ }[], persistedIndexes: ReadonlySet<number>): number;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Returns the highest sync id in the durable prefix of an ordered delta frame.
3
+ *
4
+ * A frame may span several IndexedDB stores, each committed in its own
5
+ * transaction. Those transactions can succeed and fail independently. The
6
+ * resume cursor may advance only through the first failed input: taking the
7
+ * maximum id from any successful store would skip an earlier failed delta on
8
+ * every later catch-up.
9
+ */
10
+ export function highestPersistedPrefixSyncId(deltas, persistedIndexes) {
11
+ let through = 0;
12
+ for (let index = 0; index < deltas.length; index += 1) {
13
+ if (!persistedIndexes.has(index))
14
+ break;
15
+ const raw = deltas[index]?.syncId;
16
+ const syncId = typeof raw === 'string' ? Number(raw) : raw;
17
+ if (typeof syncId === 'number' && Number.isFinite(syncId) && syncId > through) {
18
+ through = syncId;
19
+ }
20
+ }
21
+ return through;
22
+ }
@@ -9,6 +9,8 @@ export type { CapturedMutation, MockMutationExecutorOptions } from './mocks/Mock
9
9
  export { MockNetworkMonitor } from './mocks/MockNetworkMonitor.js';
10
10
  export { MockWebSocket } from './mocks/MockWebSocket.js';
11
11
  export type { MockDelta, MockBootstrapHint } from './mocks/MockWebSocket.js';
12
+ export { fakeDatabase } from './mocks/FakeDatabase.js';
13
+ export type { FakeDatabaseOverrides } from './mocks/FakeDatabase.js';
12
14
  export { createTestContext, } from './mocks/MockSyncContext.js';
13
15
  export type { TestContextOptions, TestContextResult } from './mocks/MockSyncContext.js';
14
16
  export { TestProject, TestTask, TestComment, TestSlideDeck, TestSlide, TestSlideLayer, TEST_MODEL_PRIORITIES, registerTestModels, createTestConfig, resetFixtureCounter, createProjectFixture, createTaskFixture, createCommentFixture, createSlideDeckFixture, createSlideFixture, createSlideLayerFixture, } from './fixtures/models.js';
@@ -10,6 +10,7 @@
10
10
  export { MockMutationExecutor } from './mocks/MockMutationExecutor.js';
11
11
  export { MockNetworkMonitor } from './mocks/MockNetworkMonitor.js';
12
12
  export { MockWebSocket } from './mocks/MockWebSocket.js';
13
+ export { fakeDatabase } from './mocks/FakeDatabase.js';
13
14
  export { createTestContext, } from './mocks/MockSyncContext.js';
14
15
  // ─────────────────────────────────────────────
15
16
  // Fixtures: Models
@@ -0,0 +1,18 @@
1
+ import type { Database } from '../../Database.js';
2
+ /**
3
+ * Overrides for {@link fakeDatabase}. Everything is typechecked against
4
+ * Database's public surface except `getPersistedTransactions`, which widens to
5
+ * `unknown[]`: persisted rows are untrusted input (the CommitOutboxStore port
6
+ * documents this), and restore-path tests feed deliberately corrupt rows.
7
+ */
8
+ export type FakeDatabaseOverrides = Omit<Partial<Database>, 'getPersistedTransactions'> & {
9
+ getPersistedTransactions?: () => Promise<unknown[]>;
10
+ };
11
+ /**
12
+ * A Database double that implements ONLY the given members — no real
13
+ * prototype behind it, so a code path reaching for anything else fails
14
+ * loudly instead of silently running real IndexedDB logic. The bare `as`
15
+ * is a single narrowing assert (Database is assignable to the overrides
16
+ * type), so every override stays typechecked against the real surface.
17
+ */
18
+ export declare function fakeDatabase(overrides: FakeDatabaseOverrides): Database;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * A Database double that implements ONLY the given members — no real
3
+ * prototype behind it, so a code path reaching for anything else fails
4
+ * loudly instead of silently running real IndexedDB logic. The bare `as`
5
+ * is a single narrowing assert (Database is assignable to the overrides
6
+ * type), so every override stays typechecked against the real surface.
7
+ */
8
+ export function fakeDatabase(overrides) {
9
+ return overrides;
10
+ }
@@ -4,6 +4,7 @@
4
4
  * can drive connection changes, deltas, and bootstrap hints by hand.
5
5
  */
6
6
  import type { SyncActionType } from '../../types/index.js';
7
+ import type { BootstrapReason } from '../../wire/bootstrapReason.js';
7
8
  /** The shape of a single delta — one change the server pushes to the client. */
8
9
  export interface MockDelta {
9
10
  id: number;
@@ -17,7 +18,7 @@ export interface MockDelta {
17
18
  * rebuild its data from scratch rather than catch up one delta at a time.
18
19
  */
19
20
  export interface MockBootstrapHint {
20
- reason: 'too_far_behind' | 'too_many_deltas' | 'missing_entities';
21
+ reason: BootstrapReason;
21
22
  tables?: string[];
22
23
  staleTables?: string[];
23
24
  }
@@ -18,6 +18,8 @@ import { SyncPosition } from '../sync/syncPosition.js';
18
18
  import type { WriteOptions } from '../interfaces/index.js';
19
19
  import type { StaleNotification, ReadDependency } from '../coordination/schema.js';
20
20
  import { type MutationInput, type Transaction, type UserContext } from './commitPayload.js';
21
+ import { type DurableCommitEnvelope, type DurableCommitOperation, type CommitOutboxScope } from './commitEnvelope.js';
22
+ import type { CommitOutboxStore } from './commitOutboxStore.js';
21
23
  export type { Transaction, UserContext } from './commitPayload.js';
22
24
  /**
23
25
  * A pre-built, multi-operation commit submitted through
@@ -32,7 +34,7 @@ interface CommitTransaction {
32
34
  id: string;
33
35
  kind: 'commit';
34
36
  operations: {
35
- type: string;
37
+ type: DurableCommitOperation['type'];
36
38
  model: string;
37
39
  id: string;
38
40
  input?: Record<string, unknown>;
@@ -48,6 +50,12 @@ interface CommitTransaction {
48
50
  attempts: number;
49
51
  lastSyncId?: number;
50
52
  error?: Error;
53
+ sealedAt: number;
54
+ sequence: number;
55
+ sealPromise?: Promise<void>;
56
+ durableEnvelope?: DurableCommitEnvelope;
57
+ /** Journal entries atomically consumed when this request was sealed. */
58
+ sourceMutationIds?: string[];
51
59
  }
52
60
  interface ConflictResolution {
53
61
  strategy: 'last-write-wins' | 'merge' | 'reject' | 'custom';
@@ -89,6 +97,7 @@ interface TransactionQueueConfig {
89
97
  commitOfflineGraceMs: number;
90
98
  }
91
99
  export declare class TransactionQueue extends EventEmitter {
100
+ private static readonly DURABLE_REPLAY_WINDOW_MS;
92
101
  private store;
93
102
  private lastPermanentErrorSig?;
94
103
  private _mutationExecutor;
@@ -105,6 +114,15 @@ export declare class TransactionQueue extends EventEmitter {
105
114
  private commitLane;
106
115
  private commitStore;
107
116
  private commitProcessing;
117
+ private lastCommitSequence;
118
+ private durableReplayBlock;
119
+ /** Browser-backed strict outbox; absent for standalone/in-memory consumers. */
120
+ private commitOutbox;
121
+ private commitOutboxScope;
122
+ private nextCommitSequence;
123
+ private emitCommitLifecycle;
124
+ private assertDurableReplayOpen;
125
+ private assertEnvelopeInsideReplayWindow;
108
126
  private computePriorityScore;
109
127
  private ensureDerivedFields;
110
128
  private entityKey;
@@ -134,6 +152,38 @@ export declare class TransactionQueue extends EventEmitter {
134
152
  private get lastSeenSyncId();
135
153
  private noteAck;
136
154
  private batchIndex;
155
+ /** Mints the request identity once; retry paths only read the stored value. */
156
+ private generateCommitIdempotencyKey;
157
+ /**
158
+ * Binds an ordered transaction batch to one wire-level idempotency key.
159
+ * Existing envelopes are validated and restored to their original order;
160
+ * they are never extended with newly queued work.
161
+ */
162
+ private ensureCommitEnvelope;
163
+ /** Bind the strict local outbox used before any mutation reaches the wire. */
164
+ setCommitOutbox(outbox: CommitOutboxStore): void;
165
+ setCommitOutboxScope(scope: CommitOutboxScope): void;
166
+ private sourceMutationIdsFor;
167
+ /**
168
+ * Atomically replaces staged mutation journal rows with one exact request.
169
+ * The returned operations are the JSON-normalized values that must be sent;
170
+ * callers never send a separately reconstructed payload after sealing.
171
+ */
172
+ private sealDurableCommit;
173
+ /** Best-effort cleanup after a definitive result; replay is safe if it fails. */
174
+ private removeDurableCommit;
175
+ /**
176
+ * Takes either one complete retry envelope or a fresh batch. A retry waits
177
+ * until every original member has re-entered the queue, preventing an
178
+ * ambiguous A+B commit from being replayed later as A and B separately.
179
+ */
180
+ private takeNextExecutionBatch;
181
+ /**
182
+ * Selects one reconnect batch without changing the request identity of work
183
+ * that was already attempted. Explicit caller keys remain one-call batches;
184
+ * an existing envelope is replayed only with all of its original members.
185
+ */
186
+ private takeOfflineFlushBatch;
137
187
  /**
138
188
  * Resolvers for per-transaction `confirmation` promises. Populated in
139
189
  * `attachConfirmation` at staging time, consumed by the constructor-time
@@ -216,8 +266,8 @@ export declare class TransactionQueue extends EventEmitter {
216
266
  private commitCreatedTransactions;
217
267
  /**
218
268
  * Flushes every pending transaction in one commit, the fast path taken on
219
- * reconnect. If the batch fails, each transaction falls back to normal,
220
- * one-by-one processing.
269
+ * reconnect. If transport fails, the transactions retain this exact commit
270
+ * envelope when they fall back to normal queue processing.
221
271
  */
222
272
  flushOfflineQueue(): Promise<void>;
223
273
  /**
@@ -225,21 +275,21 @@ export declare class TransactionQueue extends EventEmitter {
225
275
  * batched commit. Returns the {@link Transaction}, whose `confirmation`
226
276
  * promise settles once the server confirms the write.
227
277
  */
228
- create(model: Model, context: UserContext, writeOptions?: WriteOptions): Promise<Transaction>;
278
+ create(model: Model, context: UserContext, writeOptions?: WriteOptions, sourceMutationId?: string): Promise<Transaction>;
229
279
  /**
230
280
  * Records an update and applies it optimistically, then stages it for the next
231
281
  * batched commit. Rapid updates to the same entity coalesce into a single wire
232
282
  * operation.
233
283
  * @param precomputedChanges - Optional pre-captured changes, used instead of re-reading them from the model.
234
284
  */
235
- update(model: Model, context: UserContext, precomputedChanges?: Record<string, unknown>, writeOptions?: WriteOptions): Promise<Transaction>;
285
+ update(model: Model, context: UserContext, precomputedChanges?: Record<string, unknown>, writeOptions?: WriteOptions, sourceMutationId?: string): Promise<Transaction>;
236
286
  /**
237
287
  * Records a delete and applies it optimistically. If the row's own create has
238
288
  * not yet been sent, both are cancelled locally rather than sending a create
239
289
  * followed by a delete; if the create is already in flight, the delete waits
240
290
  * until it settles so the server never sees a delete before the create.
241
291
  */
242
- delete(model: Model, context: UserContext, writeOptions?: WriteOptions): Promise<Transaction>;
292
+ delete(model: Model, context: UserContext, writeOptions?: WriteOptions, sourceMutationId?: string): Promise<Transaction>;
243
293
  /**
244
294
  * Uploads a single attachment, delegating to the mutation executor.
245
295
  */
@@ -263,7 +313,7 @@ export declare class TransactionQueue extends EventEmitter {
263
313
  * Records an archive and applies it optimistically, then stages it for the
264
314
  * next batched commit.
265
315
  */
266
- archive(model: Model, context: UserContext, writeOptions?: WriteOptions): Promise<Transaction>;
316
+ archive(model: Model, context: UserContext, writeOptions?: WriteOptions, sourceMutationId?: string): Promise<Transaction>;
267
317
  /**
268
318
  * Records an unarchive and applies it optimistically, then stages it for the
269
319
  * next batched commit.
@@ -309,7 +359,7 @@ export declare class TransactionQueue extends EventEmitter {
309
359
  enqueueCommit(clientTxId: string, operations: CommitTransaction['operations'], options?: {
310
360
  causedByTaskId?: string | null;
311
361
  reads?: ReadDependency[] | null;
312
- }): void;
362
+ }): Promise<void>;
313
363
  /**
314
364
  * Drains the pending commit-lane envelopes one at a time. A transient
315
365
  * failure, such as a network error, leaves the envelope at the head of the
@@ -346,6 +396,8 @@ export declare class TransactionQueue extends EventEmitter {
346
396
  * - Any other business-logic error from the server.
347
397
  */
348
398
  private isPermanentError;
399
+ /** True only when the server definitively rejected before applying. */
400
+ private isDefinitiveRejection;
349
401
  /**
350
402
  * Handles a failed transaction: retries transient failures with backoff and
351
403
  * rolls back permanent ones, settling the transaction's confirmation promise
@@ -372,6 +424,12 @@ export declare class TransactionQueue extends EventEmitter {
372
424
  * persistence is disabled.
373
425
  */
374
426
  loadPersistedTransactions(database: Database): Promise<void>;
427
+ /**
428
+ * Restore exact sealed requests after the local database is open. Sealed
429
+ * envelopes replay through the atomic commit lane and are never re-projected
430
+ * through model/schema state from the new process.
431
+ */
432
+ restoreDurableCommits(): Promise<Set<string>>;
375
433
  /**
376
434
  * Validates and rehydrates one persisted row. Rows written to the same store
377
435
  * by other subsystems are skipped, and rows that fail the persisted