@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.
- package/CHANGELOG.md +29 -3
- package/README.md +1 -1
- package/dist/BaseSyncedStore.js +8 -9
- package/dist/Database.d.ts +14 -1
- package/dist/Database.js +225 -28
- package/dist/Model.d.ts +17 -0
- package/dist/Model.js +32 -1
- package/dist/SyncClient.d.ts +10 -6
- package/dist/SyncClient.js +379 -76
- package/dist/adapters/inMemoryStorage.d.ts +1 -0
- package/dist/adapters/inMemoryStorage.js +12 -0
- package/dist/cli.cjs +6 -2
- package/dist/client/Ablo.d.ts +24 -1
- package/dist/client/Ablo.js +4 -3
- package/dist/client/ApiClient.js +309 -43
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/modelRegistration.js +11 -0
- package/dist/client/options.d.ts +23 -0
- package/dist/client/wsMutationExecutor.d.ts +3 -2
- package/dist/client/wsMutationExecutor.js +3 -2
- package/dist/commit/contract.d.ts +493 -0
- package/dist/commit/contract.js +187 -0
- package/dist/commit/index.d.ts +6 -0
- package/dist/commit/index.js +5 -0
- package/dist/core/StoreManager.d.ts +2 -0
- package/dist/core/StoreManager.js +12 -0
- package/dist/errorCodes.js +6 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2 -0
- package/dist/interfaces/index.d.ts +2 -2
- package/dist/mutators/UndoManager.d.ts +2 -0
- package/dist/mutators/UndoManager.js +32 -0
- package/dist/react/useAblo.d.ts +6 -4
- package/dist/react/useAblo.js +25 -3
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/schema.d.ts +31 -1
- package/dist/stores/ObjectStore.d.ts +14 -1
- package/dist/stores/ObjectStore.js +27 -4
- package/dist/stores/ObjectStoreContract.d.ts +2 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -0
- package/dist/sync/SyncWebSocket.d.ts +2 -1
- package/dist/sync/persistedPrefix.d.ts +12 -0
- package/dist/sync/persistedPrefix.js +22 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
- package/dist/testing/mocks/FakeDatabase.js +10 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -1
- package/dist/transactions/TransactionQueue.d.ts +66 -8
- package/dist/transactions/TransactionQueue.js +607 -89
- package/dist/transactions/commitEnvelope.d.ts +132 -0
- package/dist/transactions/commitEnvelope.js +139 -0
- package/dist/transactions/commitOutboxStore.d.ts +32 -0
- package/dist/transactions/commitOutboxStore.js +26 -0
- package/dist/transactions/commitPayload.d.ts +15 -0
- package/dist/transactions/commitPayload.js +6 -0
- package/dist/transactions/httpCommitEnvelope.d.ts +43 -0
- package/dist/transactions/httpCommitEnvelope.js +179 -0
- package/dist/transactions/replayValidation.d.ts +83 -0
- package/dist/transactions/replayValidation.js +46 -1
- package/dist/wire/bootstrapReason.d.ts +9 -0
- package/dist/wire/bootstrapReason.js +8 -0
- package/dist/wire/frames.d.ts +236 -0
- package/dist/wire/frames.js +21 -0
- package/dist/wire/index.d.ts +4 -2
- package/dist/wire/index.js +2 -1
- package/docs/api.md +10 -10
- package/docs/coordination.md +2 -2
- package/docs/mcp.md +1 -1
- 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?:
|
|
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
|
+
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/testing/index.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
220
|
-
*
|
|
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
|