@abloatai/ablo 0.27.0 → 0.29.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 +59 -3
- package/README.md +1 -1
- package/dist/BaseSyncedStore.js +8 -9
- package/dist/Database.d.ts +14 -1
- package/dist/Database.js +228 -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 +390 -75
- package/dist/adapters/inMemoryStorage.d.ts +1 -0
- package/dist/adapters/inMemoryStorage.js +29 -13
- package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
- package/dist/auth/schemas.d.ts +6 -0
- package/dist/auth/schemas.js +7 -0
- package/dist/cli.cjs +821 -402
- package/dist/client/Ablo.d.ts +53 -24
- package/dist/client/Ablo.js +12 -199
- package/dist/client/claimHeartbeatLoop.d.ts +1 -1
- package/dist/client/claimHeartbeatLoop.js +1 -1
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +3 -1
- package/dist/client/durableWrites.d.ts +21 -0
- package/dist/client/durableWrites.js +46 -0
- package/dist/client/httpClient.d.ts +21 -45
- package/dist/client/httpClient.js +104 -26
- package/dist/client/httpTransport.d.ts +8 -0
- package/dist/client/{ApiClient.js → httpTransport.js} +361 -308
- package/dist/client/modelRegistration.js +11 -0
- package/dist/client/options.d.ts +54 -7
- package/dist/client/options.js +3 -2
- package/dist/client/resourceTypes.d.ts +9 -85
- package/dist/client/resourceTypes.js +1 -1
- package/dist/client/sessionMint.d.ts +5 -6
- package/dist/client/sessionMint.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/client/wsMutationExecutor.d.ts +3 -2
- package/dist/client/wsMutationExecutor.js +3 -2
- package/dist/coordination/schema.d.ts +30 -0
- package/dist/coordination/schema.js +14 -0
- package/dist/core/StoreManager.d.ts +2 -0
- package/dist/core/StoreManager.js +12 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/errorCodes.js +6 -2
- package/dist/errors.d.ts +4 -40
- package/dist/errors.js +5 -5
- package/dist/index.d.ts +20 -8
- package/dist/index.js +9 -5
- package/dist/interfaces/index.d.ts +5 -2
- package/dist/mutators/UndoManager.d.ts +2 -0
- package/dist/mutators/UndoManager.js +32 -0
- package/dist/mutators/defineMutators.d.ts +18 -0
- package/dist/mutators/defineMutators.js +4 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/react/useAblo.d.ts +6 -4
- package/dist/react/useAblo.js +25 -3
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +1 -1
- package/dist/schema/schema.d.ts +31 -1
- package/dist/schema/select.d.ts +15 -0
- package/dist/schema/select.js +27 -3
- package/dist/source/connector.d.ts +3 -3
- package/dist/source/factory.d.ts +4 -6
- package/dist/source/factory.js +4 -7
- package/dist/source/index.d.ts +4 -4
- package/dist/source/index.js +2 -2
- package/dist/source/signing.d.ts +0 -3
- package/dist/source/types.d.ts +0 -26
- package/dist/stores/ObjectStore.d.ts +14 -1
- package/dist/stores/ObjectStore.js +33 -10
- package/dist/stores/ObjectStoreContract.d.ts +2 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +3 -0
- package/dist/sync/BootstrapFetcher.d.ts +10 -0
- package/dist/sync/BootstrapFetcher.js +27 -4
- package/dist/sync/SyncWebSocket.d.ts +2 -1
- package/dist/sync/createClaimStream.d.ts +11 -2
- package/dist/sync/createClaimStream.js +4 -3
- 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/durableWriteStore.d.ts +123 -0
- package/dist/transactions/durableWriteStore.js +30 -0
- package/dist/transactions/httpCommitEnvelope.d.ts +44 -0
- package/dist/transactions/httpCommitEnvelope.js +181 -0
- package/dist/transactions/idempotencyKey.d.ts +10 -0
- package/dist/transactions/idempotencyKey.js +9 -0
- package/dist/transactions/replayValidation.d.ts +83 -0
- package/dist/transactions/replayValidation.js +46 -1
- package/dist/types/global.d.ts +10 -29
- package/dist/types/global.js +4 -7
- package/dist/types/streams.d.ts +2 -28
- package/dist/wire/bootstrapReason.d.ts +9 -0
- package/dist/wire/bootstrapReason.js +8 -0
- package/dist/wire/frames.d.ts +6 -21
- package/dist/wire/frames.js +4 -4
- package/dist/wire/index.d.ts +6 -3
- package/dist/wire/index.js +3 -2
- package/dist/wire/protocolVersion.d.ts +30 -17
- package/dist/wire/protocolVersion.js +34 -18
- package/docs/agents.md +8 -3
- package/docs/api.md +16 -14
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +4 -5
- package/docs/data-sources.md +5 -8
- package/docs/guarantees.md +21 -0
- package/docs/integration-guide.md +1 -0
- package/docs/mcp.md +1 -1
- package/docs/migration.md +21 -1
- package/docs/quickstart.md +11 -0
- package/docs/react.md +0 -46
- package/examples/README.md +6 -5
- package/llms.txt +15 -15
- package/package.json +3 -3
- package/dist/client/ApiClient.d.ts +0 -177
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ModelMetadata } from '../types/index.js';
|
|
6
6
|
import type { ObjectStoreContract } from './ObjectStoreContract.js';
|
|
7
|
+
/**
|
|
8
|
+
* IDB transaction options type (TypeScript's lib.dom.d.ts may be outdated)
|
|
9
|
+
* durability: 'relaxed' provides ~16x write performance improvement
|
|
10
|
+
* by not requiring fsync on each transaction commit.
|
|
11
|
+
* Safe for optimistic sync engines that can recover from server state.
|
|
12
|
+
*/
|
|
13
|
+
interface IDBTransactionOptionsWithDurability {
|
|
14
|
+
durability?: 'default' | 'relaxed' | 'strict';
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* An IndexedDB-backed store holding the records of a single model.
|
|
9
18
|
*
|
|
@@ -21,8 +30,11 @@ export declare class ObjectStore implements ObjectStoreContract {
|
|
|
21
30
|
protected modelName: string;
|
|
22
31
|
protected storeName: string;
|
|
23
32
|
protected metadata: ModelMetadata;
|
|
33
|
+
private readonly writeDurability;
|
|
24
34
|
private isClosing;
|
|
25
|
-
constructor(db: IDBDatabase, modelName: string, storeName: string, metadata: ModelMetadata);
|
|
35
|
+
constructor(db: IDBDatabase, modelName: string, storeName: string, metadata: ModelMetadata, writeDurability?: NonNullable<IDBTransactionOptionsWithDurability['durability']>);
|
|
36
|
+
/** Insert without replacing an existing key (used by the commit outbox). */
|
|
37
|
+
add(data: Record<string, unknown>): Promise<void>;
|
|
26
38
|
/**
|
|
27
39
|
* Mark this store as closing to prevent new operations
|
|
28
40
|
*/
|
|
@@ -100,3 +112,4 @@ export declare class ObjectStore implements ObjectStoreContract {
|
|
|
100
112
|
*/
|
|
101
113
|
performMaintenance(): Promise<void>;
|
|
102
114
|
}
|
|
115
|
+
export {};
|
|
@@ -19,12 +19,35 @@ export class ObjectStore {
|
|
|
19
19
|
modelName;
|
|
20
20
|
storeName;
|
|
21
21
|
metadata;
|
|
22
|
+
writeDurability;
|
|
22
23
|
isClosing = false;
|
|
23
|
-
constructor(db, modelName, storeName, metadata) {
|
|
24
|
+
constructor(db, modelName, storeName, metadata, writeDurability = 'relaxed') {
|
|
24
25
|
this.db = db;
|
|
25
26
|
this.modelName = modelName;
|
|
26
27
|
this.storeName = storeName;
|
|
27
28
|
this.metadata = metadata;
|
|
29
|
+
this.writeDurability = writeDurability;
|
|
30
|
+
}
|
|
31
|
+
/** Insert without replacing an existing key (used by the commit outbox). */
|
|
32
|
+
async add(data) {
|
|
33
|
+
if (!this.checkDatabaseAvailable()) {
|
|
34
|
+
return Promise.reject(new Error('IndexedDB not available (closing or invalid)'));
|
|
35
|
+
}
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
try {
|
|
38
|
+
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
39
|
+
durability: this.writeDurability,
|
|
40
|
+
});
|
|
41
|
+
const store = tx.objectStore(this.storeName);
|
|
42
|
+
const request = store.add(data);
|
|
43
|
+
tx.oncomplete = () => { resolve(); };
|
|
44
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
45
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
28
51
|
}
|
|
29
52
|
/**
|
|
30
53
|
* Mark this store as closing to prevent new operations
|
|
@@ -65,13 +88,13 @@ export class ObjectStore {
|
|
|
65
88
|
try {
|
|
66
89
|
// Use relaxed durability for ~16x write performance (safe with optimistic sync)
|
|
67
90
|
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
68
|
-
durability:
|
|
91
|
+
durability: this.writeDurability,
|
|
69
92
|
});
|
|
70
93
|
const store = tx.objectStore(this.storeName);
|
|
71
94
|
const request = store.put(data);
|
|
72
95
|
tx.oncomplete = () => { resolve(); };
|
|
73
|
-
tx.onerror = () => { reject(tx.error
|
|
74
|
-
request.onerror = () => { reject(request.error
|
|
96
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
97
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
75
98
|
}
|
|
76
99
|
catch (error) {
|
|
77
100
|
// Propagate failure so callers do not continue with inconsistent state
|
|
@@ -169,13 +192,13 @@ export class ObjectStore {
|
|
|
169
192
|
try {
|
|
170
193
|
// Use relaxed durability for ~16x write performance (safe with optimistic sync)
|
|
171
194
|
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
172
|
-
durability:
|
|
195
|
+
durability: this.writeDurability,
|
|
173
196
|
});
|
|
174
197
|
const store = tx.objectStore(this.storeName);
|
|
175
198
|
const request = store.delete(id);
|
|
176
199
|
tx.oncomplete = () => { resolve(); };
|
|
177
|
-
tx.onerror = () => { reject(tx.error
|
|
178
|
-
request.onerror = () => { reject(request.error
|
|
200
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
201
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
179
202
|
}
|
|
180
203
|
catch (error) {
|
|
181
204
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -205,13 +228,13 @@ export class ObjectStore {
|
|
|
205
228
|
try {
|
|
206
229
|
// Use relaxed durability for ~16x write performance (safe with optimistic sync)
|
|
207
230
|
const tx = this.db.transaction([this.storeName], 'readwrite', {
|
|
208
|
-
durability:
|
|
231
|
+
durability: this.writeDurability,
|
|
209
232
|
});
|
|
210
233
|
const store = tx.objectStore(this.storeName);
|
|
211
234
|
const request = store.clear();
|
|
212
235
|
tx.oncomplete = () => { resolve(); };
|
|
213
|
-
tx.onerror = () => { reject(tx.error
|
|
214
|
-
request.onerror = () => { reject(request.error
|
|
236
|
+
tx.onerror = () => { reject(tx.error ?? new Error('IndexedDB transaction error')); };
|
|
237
|
+
request.onerror = () => { reject(request.error ?? new Error('IndexedDB request error')); };
|
|
215
238
|
}
|
|
216
239
|
catch (error) {
|
|
217
240
|
reject(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* a silent failure in a caller.
|
|
16
16
|
*/
|
|
17
17
|
export interface ObjectStoreContract {
|
|
18
|
+
/** Insert a record only when its key is absent. */
|
|
19
|
+
add(data: Record<string, unknown>): Promise<void>;
|
|
18
20
|
/** Insert or update a record. The record must carry an `id` field. */
|
|
19
21
|
put(data: Record<string, unknown>): Promise<void>;
|
|
20
22
|
/** Look up a record by id. */
|
package/dist/surface.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orde
|
|
|
30
30
|
* The keys of the client constructor options, {@link AbloOptions}. Only
|
|
31
31
|
* `schema` is required; every other key is optional.
|
|
32
32
|
*/
|
|
33
|
-
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
33
|
+
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "authEndpoint", "databaseUrl", "persistence", "durableWrites", "commitOutbox", "commitOutboxScope", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
34
34
|
export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
|
|
35
35
|
export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
|
|
36
36
|
export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
|
package/dist/surface.js
CHANGED
|
@@ -88,6 +88,16 @@ export declare class BootstrapFetcher {
|
|
|
88
88
|
* means the app's schema and the deployed schema have diverged — reads/writes
|
|
89
89
|
* relying on undeployed changes will later fail with an opaque DB constraint
|
|
90
90
|
* error. Warn once, actionably; never throws or blocks the bootstrap.
|
|
91
|
+
*
|
|
92
|
+
* The message names the SERVER it connected to, and spans all three real
|
|
93
|
+
* causes rather than assuming "you forgot to push". Drift most often means the
|
|
94
|
+
* schema was pushed to a different server, project, or environment than this
|
|
95
|
+
* client points at (a bare `ablo push` targets the hosted default; a local app
|
|
96
|
+
* usually reads a local server) — so the first, load-bearing pointer is `ablo
|
|
97
|
+
* status`, which names the exact org/project/environment the key resolves to
|
|
98
|
+
* and the deployed hash, turning "which of these is it?" into one glance. The
|
|
99
|
+
* older "Run `ablo push`" copy sent everyone down one path and confused the
|
|
100
|
+
* common wrong-target and version-skew cases.
|
|
91
101
|
*/
|
|
92
102
|
private warnOnSchemaDrift;
|
|
93
103
|
constructor(options: BootstrapOptions);
|
|
@@ -25,6 +25,16 @@ export class BootstrapFetcher {
|
|
|
25
25
|
* means the app's schema and the deployed schema have diverged — reads/writes
|
|
26
26
|
* relying on undeployed changes will later fail with an opaque DB constraint
|
|
27
27
|
* error. Warn once, actionably; never throws or blocks the bootstrap.
|
|
28
|
+
*
|
|
29
|
+
* The message names the SERVER it connected to, and spans all three real
|
|
30
|
+
* causes rather than assuming "you forgot to push". Drift most often means the
|
|
31
|
+
* schema was pushed to a different server, project, or environment than this
|
|
32
|
+
* client points at (a bare `ablo push` targets the hosted default; a local app
|
|
33
|
+
* usually reads a local server) — so the first, load-bearing pointer is `ablo
|
|
34
|
+
* status`, which names the exact org/project/environment the key resolves to
|
|
35
|
+
* and the deployed hash, turning "which of these is it?" into one glance. The
|
|
36
|
+
* older "Run `ablo push`" copy sent everyone down one path and confused the
|
|
37
|
+
* common wrong-target and version-skew cases.
|
|
28
38
|
*/
|
|
29
39
|
warnOnSchemaDrift(serverHash) {
|
|
30
40
|
if (this.schemaDriftWarned || !serverHash)
|
|
@@ -33,14 +43,27 @@ export class BootstrapFetcher {
|
|
|
33
43
|
if (!clientHash || clientHash === serverHash)
|
|
34
44
|
return;
|
|
35
45
|
this.schemaDriftWarned = true;
|
|
46
|
+
const org = this.options.organizationId;
|
|
47
|
+
const where = org ? `${this.baseUrl} (org ${org})` : this.baseUrl;
|
|
36
48
|
// Self-brand the message ("Ablo:") rather than rely on the default logger's
|
|
37
49
|
// `[Ablo]` namespace — consumers wiring their own logger (pino, etc.) lose
|
|
38
50
|
// that prefix, and a drift warning that reads like the app's own log is
|
|
39
51
|
// worse than none. The brand tells them at a glance who is talking.
|
|
40
|
-
getContext().logger.warn(`Ablo: Schema drift
|
|
41
|
-
`
|
|
42
|
-
`
|
|
43
|
-
|
|
52
|
+
getContext().logger.warn(`Ablo: Schema drift — the schema this client was built with (${clientHash}) is not the ` +
|
|
53
|
+
`one active on the server it connected to (${serverHash} at ${where}). Until they match, ` +
|
|
54
|
+
`operations that depend on the difference will fail later with an opaque database error. ` +
|
|
55
|
+
`This is usually one of three things. The schema may have been pushed to a different ` +
|
|
56
|
+
`server, project, or environment than this client points at — run \`ablo status\` to see ` +
|
|
57
|
+
`the exact org, project, and environment your key resolves to, alongside the deployed ` +
|
|
58
|
+
`hash, and confirm they match here. Your local schema may simply not be pushed to this ` +
|
|
59
|
+
`server yet — run \`ablo push\` against it. Or this client and the server may have been ` +
|
|
60
|
+
`built with different Ablo versions, which can hash an identical schema differently — ` +
|
|
61
|
+
`align the versions. This check is advisory and never blocks the connection.`, {
|
|
62
|
+
clientSchemaHash: clientHash,
|
|
63
|
+
serverSchemaHash: serverHash,
|
|
64
|
+
serverUrl: this.baseUrl,
|
|
65
|
+
...(org ? { organizationId: org } : {}),
|
|
66
|
+
});
|
|
44
67
|
}
|
|
45
68
|
constructor(options) {
|
|
46
69
|
// Defaults are spread first; the explicit `baseUrl` then takes precedence,
|
|
@@ -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
|
}
|
|
@@ -23,6 +23,15 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { SyncWebSocket } from './SyncWebSocket.js';
|
|
25
25
|
import type { ClaimOptions, Claim, ClaimStream, PresenceTarget } from '../types/streams.js';
|
|
26
|
+
/**
|
|
27
|
+
* The wire capability the claim stream actually uses: subscribe to typed
|
|
28
|
+
* inbound frames, check liveness, and send outbound frames. `SyncWebSocket`
|
|
29
|
+
* satisfies it, so production wiring is unchanged — but depending on the port
|
|
30
|
+
* rather than the whole socket class lets a test drive it with a plain object,
|
|
31
|
+
* no cast. `Pick` carries the exact (generic, typed-payload) `subscribe`
|
|
32
|
+
* signature, so every handler stays fully typed.
|
|
33
|
+
*/
|
|
34
|
+
export type ClaimTransport = Pick<SyncWebSocket, 'subscribe' | 'isConnected' | 'send'>;
|
|
26
35
|
export interface ClaimStreamConfig {
|
|
27
36
|
/** Identity used to filter our own active claims out of `others`. */
|
|
28
37
|
participantId: string;
|
|
@@ -36,7 +45,7 @@ export interface AttachableClaimStream extends ClaimStream {
|
|
|
36
45
|
* `ablo.<model>.claim({ id })`, which is built on this.
|
|
37
46
|
*/
|
|
38
47
|
claim(target: PresenceTarget, opts?: ClaimOptions): Claim;
|
|
39
|
-
attach(transport:
|
|
48
|
+
attach(transport: ClaimTransport): void;
|
|
40
49
|
dispose(): void;
|
|
41
50
|
}
|
|
42
|
-
export declare function createClaimStream(config: ClaimStreamConfig, transport?:
|
|
51
|
+
export declare function createClaimStream(config: ClaimStreamConfig, transport?: ClaimTransport | null): AttachableClaimStream;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { asyncIteratorFrom } from '../utils/asyncIterator.js';
|
|
25
25
|
import { toMs } from '../utils/duration.js';
|
|
26
|
-
import { claimHeartbeatAckPayloadSchema, descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
26
|
+
import { claimHeartbeatAckPayloadSchema, claimLostSchema, descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
27
27
|
import { AbloClaimedError, AbloConnectionError } from '../errors.js';
|
|
28
28
|
import { resolveHeartbeatOptions } from '../client/claimHeartbeatLoop.js';
|
|
29
29
|
import { getContext } from '../context.js';
|
|
@@ -173,9 +173,10 @@ export function createClaimStream(config, transport = null) {
|
|
|
173
173
|
// (preempted or expired). Distinct from a rejection, which is a claim
|
|
174
174
|
// the server refused.
|
|
175
175
|
unsubs.push(t.subscribe('claim_lost', (payload) => {
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
176
|
+
const parsed = claimLostSchema.safeParse(payload);
|
|
177
|
+
if (!parsed.success)
|
|
178
178
|
return;
|
|
179
|
+
const lost = parsed.data;
|
|
179
180
|
if (ownClaims.has(lost.claimId)) {
|
|
180
181
|
const c = ownClaims.get(lost.claimId);
|
|
181
182
|
getContext().logger.info(`claim: lost ${c ? claimLabel(c.entityType, c.entityId, c.field) : lost.claimId} (preempted or expired)`, { claimId: lost.claimId });
|
|
@@ -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 DurableWriteStore 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 { DurableWriteStore } from './durableWriteStore.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: DurableWriteStore): 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
|