@abloatai/ablo 0.17.0 → 0.19.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 +70 -0
- package/dist/BaseSyncedStore.d.ts +5 -4
- package/dist/BaseSyncedStore.js +38 -14
- package/dist/NetworkMonitor.js +3 -1
- package/dist/ObjectPool.d.ts +14 -1
- package/dist/ObjectPool.js +8 -1
- package/dist/SyncClient.js +7 -1
- package/dist/SyncEngineContext.js +2 -0
- package/dist/ai-sdk/coordination-context.d.ts +2 -1
- package/dist/ai-sdk/coordination-context.js +3 -3
- package/dist/ai-sdk/index.d.ts +3 -4
- package/dist/ai-sdk/index.js +2 -3
- package/dist/ai-sdk/wrap.d.ts +13 -18
- package/dist/ai-sdk/wrap.js +2 -6
- package/dist/cli.cjs +459 -205
- package/dist/client/Ablo.d.ts +116 -23
- package/dist/client/Ablo.js +128 -38
- package/dist/client/ApiClient.d.ts +2 -2
- package/dist/client/ApiClient.js +27 -32
- package/dist/client/auth.d.ts +26 -0
- package/dist/client/auth.js +209 -1
- package/dist/client/createModelProxy.d.ts +16 -11
- package/dist/client/createModelProxy.js +15 -15
- package/dist/client/sessionMint.d.ts +10 -0
- package/dist/client/sessionMint.js +8 -1
- package/dist/coordination/schema.d.ts +10 -10
- package/dist/coordination/schema.js +7 -8
- package/dist/coordination/trace.d.ts +91 -0
- package/dist/coordination/trace.js +147 -0
- package/dist/core/StoreManager.js +3 -1
- package/dist/errorCodes.d.ts +2 -0
- package/dist/errorCodes.js +2 -0
- package/dist/errors.d.ts +10 -2
- package/dist/errors.js +20 -9
- package/dist/index.d.ts +7 -1
- package/dist/index.js +12 -0
- package/dist/interfaces/index.d.ts +45 -2
- package/dist/policy/types.d.ts +33 -9
- package/dist/policy/types.js +42 -11
- package/dist/react/AbloProvider.d.ts +2 -2
- package/dist/schema/ddl.d.ts +36 -0
- package/dist/schema/ddl.js +66 -0
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -0
- package/dist/sync/HydrationCoordinator.js +7 -3
- package/dist/sync/SyncWebSocket.d.ts +11 -2
- package/dist/sync/SyncWebSocket.js +68 -4
- package/dist/sync/awaitClaimGrant.js +9 -2
- package/dist/sync/createClaimStream.d.ts +9 -2
- package/dist/sync/createClaimStream.js +41 -7
- package/dist/sync/participants.d.ts +12 -11
- package/dist/sync/participants.js +5 -5
- package/dist/transactions/TransactionQueue.d.ts +1 -0
- package/dist/transactions/TransactionQueue.js +40 -3
- package/dist/types/streams.d.ts +57 -135
- package/dist/wire/errorEnvelope.d.ts +13 -0
- package/docs/coordination.md +16 -0
- package/docs/debugging.md +194 -0
- package/docs/index.md +1 -0
- package/package.json +1 -1
- package/dist/ai-sdk/claim-broadcast.d.ts +0 -83
- package/dist/ai-sdk/claim-broadcast.js +0 -79
|
@@ -63,7 +63,14 @@ export async function mintSession(params, ctx) {
|
|
|
63
63
|
userMeta: params.userMeta ?? { id: res.participantId },
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
|
-
const operations = Object.entries(params.can).flatMap(([model, ops]) =>
|
|
66
|
+
const operations = Object.entries(params.can).flatMap(([model, ops]) => {
|
|
67
|
+
// Translate the schema key the developer used in `can` to the wire
|
|
68
|
+
// typename the Hub gates on — see `modelTypenames` above. Falls back to
|
|
69
|
+
// the key when no map is supplied (schemaless client) or the key isn't
|
|
70
|
+
// in it, preserving the prior behaviour exactly for those callers.
|
|
71
|
+
const ns = ctx.modelTypenames?.[model] ?? model;
|
|
72
|
+
return (ops ?? []).map((op) => `${ns.toLowerCase()}.${op}`);
|
|
73
|
+
});
|
|
67
74
|
const res = await exchangeApiKey({
|
|
68
75
|
apiKey,
|
|
69
76
|
baseUrl,
|
|
@@ -209,7 +209,7 @@ export declare const wireClaimSummarySchema: z.ZodObject<{
|
|
|
209
209
|
entityId: z.ZodString;
|
|
210
210
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
211
211
|
claimId: z.ZodString;
|
|
212
|
-
|
|
212
|
+
reason: z.ZodString;
|
|
213
213
|
declaredAt: z.ZodNumber;
|
|
214
214
|
expiresAt: z.ZodNumber;
|
|
215
215
|
}, z.core.$strip>;
|
|
@@ -227,7 +227,7 @@ export declare const claimErrorSchema: z.ZodObject<{
|
|
|
227
227
|
entityId: z.ZodString;
|
|
228
228
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
229
229
|
claimId: z.ZodString;
|
|
230
|
-
|
|
230
|
+
reason: z.ZodString;
|
|
231
231
|
declaredAt: z.ZodNumber;
|
|
232
232
|
expiresAt: z.ZodNumber;
|
|
233
233
|
}, z.core.$strip>>;
|
|
@@ -237,7 +237,7 @@ export type ClaimError = z.infer<typeof claimErrorSchema>;
|
|
|
237
237
|
/**
|
|
238
238
|
* A declared pending-mutation claim — the unit broadcast in presence
|
|
239
239
|
* `activeClaims`. Clients supply the descriptive `targetRef` fields, an
|
|
240
|
-
* `
|
|
240
|
+
* explanatory `reason`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
|
|
241
241
|
* `expiresAt` and may set `status` / `error`.
|
|
242
242
|
*
|
|
243
243
|
* `status` and `error` are OPTIONAL: this single shape serves both the
|
|
@@ -259,7 +259,7 @@ export declare const wireClaimSchema: z.ZodObject<{
|
|
|
259
259
|
field: z.ZodOptional<z.ZodString>;
|
|
260
260
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
261
261
|
claimId: z.ZodString;
|
|
262
|
-
|
|
262
|
+
reason: z.ZodString;
|
|
263
263
|
declaredAt: z.ZodNumber;
|
|
264
264
|
expiresAt: z.ZodNumber;
|
|
265
265
|
status: z.ZodOptional<z.ZodEnum<{
|
|
@@ -280,7 +280,7 @@ export declare const wireClaimSchema: z.ZodObject<{
|
|
|
280
280
|
entityId: z.ZodString;
|
|
281
281
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
282
282
|
claimId: z.ZodString;
|
|
283
|
-
|
|
283
|
+
reason: z.ZodString;
|
|
284
284
|
declaredAt: z.ZodNumber;
|
|
285
285
|
expiresAt: z.ZodNumber;
|
|
286
286
|
}, z.core.$strip>>;
|
|
@@ -313,7 +313,7 @@ export declare const claimRejectionSchema: z.ZodObject<{
|
|
|
313
313
|
entityId: z.ZodString;
|
|
314
314
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
315
315
|
claimId: z.ZodString;
|
|
316
|
-
|
|
316
|
+
reason: z.ZodString;
|
|
317
317
|
declaredAt: z.ZodNumber;
|
|
318
318
|
expiresAt: z.ZodNumber;
|
|
319
319
|
}, z.core.$strip>>;
|
|
@@ -385,7 +385,7 @@ export declare const modelClaimSchema: z.ZodReadonly<z.ZodObject<{
|
|
|
385
385
|
}, z.core.$strip>>;
|
|
386
386
|
export type ModelClaim = z.infer<typeof modelClaimSchema>;
|
|
387
387
|
/**
|
|
388
|
-
* `claim_begin` payload (client → server). The descriptive target +
|
|
388
|
+
* `claim_begin` payload (client → server). The descriptive target + reason,
|
|
389
389
|
* plus an optional duration hint and the opt-in fair-queue flag. The server
|
|
390
390
|
* stamps the lifecycle/timestamp fields, so they are NOT part of the inbound
|
|
391
391
|
* shape — this is exactly what the WS ingest validates.
|
|
@@ -403,7 +403,7 @@ export declare const claimBeginPayloadSchema: z.ZodObject<{
|
|
|
403
403
|
field: z.ZodOptional<z.ZodString>;
|
|
404
404
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
405
405
|
claimId: z.ZodString;
|
|
406
|
-
|
|
406
|
+
reason: z.ZodString;
|
|
407
407
|
estimatedMs: z.ZodOptional<z.ZodNumber>;
|
|
408
408
|
queue: z.ZodOptional<z.ZodBoolean>;
|
|
409
409
|
}, z.core.$strip>;
|
|
@@ -585,7 +585,7 @@ export declare const presenceUpdateFrameSchema: z.ZodObject<{
|
|
|
585
585
|
field: z.ZodOptional<z.ZodString>;
|
|
586
586
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
587
587
|
claimId: z.ZodString;
|
|
588
|
-
|
|
588
|
+
reason: z.ZodString;
|
|
589
589
|
declaredAt: z.ZodNumber;
|
|
590
590
|
expiresAt: z.ZodNumber;
|
|
591
591
|
status: z.ZodOptional<z.ZodEnum<{
|
|
@@ -606,7 +606,7 @@ export declare const presenceUpdateFrameSchema: z.ZodObject<{
|
|
|
606
606
|
entityId: z.ZodString;
|
|
607
607
|
meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
608
608
|
claimId: z.ZodString;
|
|
609
|
-
|
|
609
|
+
reason: z.ZodString;
|
|
610
610
|
declaredAt: z.ZodNumber;
|
|
611
611
|
expiresAt: z.ZodNumber;
|
|
612
612
|
}, z.core.$strip>>;
|
|
@@ -209,8 +209,8 @@ export const claimStatusSchema = z.enum([
|
|
|
209
209
|
]);
|
|
210
210
|
const wireClaimBaseSchema = targetRefSchema.extend({
|
|
211
211
|
claimId: z.string(),
|
|
212
|
-
/**
|
|
213
|
-
|
|
212
|
+
/** Human-readable phase: 'editing' | 'reviewing' | 'forecasting' … */
|
|
213
|
+
reason: z.string(),
|
|
214
214
|
/** Server-stamped declaration time (epoch ms). */
|
|
215
215
|
declaredAt: z.number(),
|
|
216
216
|
/** Server-computed TTL deadline (epoch ms). Readers treat as advisory. */
|
|
@@ -219,7 +219,7 @@ const wireClaimBaseSchema = targetRefSchema.extend({
|
|
|
219
219
|
});
|
|
220
220
|
export const wireClaimSummarySchema = wireClaimBaseSchema.pick({
|
|
221
221
|
claimId: true,
|
|
222
|
-
|
|
222
|
+
reason: true,
|
|
223
223
|
declaredAt: true,
|
|
224
224
|
expiresAt: true,
|
|
225
225
|
entityType: true,
|
|
@@ -243,7 +243,7 @@ export const claimErrorSchema = z.object({
|
|
|
243
243
|
/**
|
|
244
244
|
* A declared pending-mutation claim — the unit broadcast in presence
|
|
245
245
|
* `activeClaims`. Clients supply the descriptive `targetRef` fields, an
|
|
246
|
-
* `
|
|
246
|
+
* explanatory `reason`, and a chosen `claimId`; the SERVER stamps `declaredAt` /
|
|
247
247
|
* `expiresAt` and may set `status` / `error`.
|
|
248
248
|
*
|
|
249
249
|
* `status` and `error` are OPTIONAL: this single shape serves both the
|
|
@@ -298,8 +298,7 @@ export const modelClaimSchema = z
|
|
|
298
298
|
id: z.string(),
|
|
299
299
|
actor: z.string(),
|
|
300
300
|
participantKind: wireParticipantKindSchema,
|
|
301
|
-
/** Human-readable phase (`'editing'`).
|
|
302
|
-
* wire carries the same value as `action` (healed on read). */
|
|
301
|
+
/** Human-readable phase (`'editing'`). */
|
|
303
302
|
reason: z.string(),
|
|
304
303
|
description: z.string().optional(),
|
|
305
304
|
field: z.string().optional(),
|
|
@@ -310,14 +309,14 @@ export const modelClaimSchema = z
|
|
|
310
309
|
})
|
|
311
310
|
.readonly();
|
|
312
311
|
/**
|
|
313
|
-
* `claim_begin` payload (client → server). The descriptive target +
|
|
312
|
+
* `claim_begin` payload (client → server). The descriptive target + reason,
|
|
314
313
|
* plus an optional duration hint and the opt-in fair-queue flag. The server
|
|
315
314
|
* stamps the lifecycle/timestamp fields, so they are NOT part of the inbound
|
|
316
315
|
* shape — this is exactly what the WS ingest validates.
|
|
317
316
|
*/
|
|
318
317
|
export const claimBeginPayloadSchema = targetRefSchema.extend({
|
|
319
318
|
claimId: z.string(),
|
|
320
|
-
|
|
319
|
+
reason: z.string(),
|
|
321
320
|
/** Hint for `expiresAt`; the server caps it. */
|
|
322
321
|
estimatedMs: z.number().optional(),
|
|
323
322
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim log — the easy way to SEE agents colliding.
|
|
3
|
+
*
|
|
4
|
+
* The engine emits the claim lifecycle (`acquired → queued → granted → lost /
|
|
5
|
+
* rejected / expired`) and the notify-instead-of-abort stale-write collision
|
|
6
|
+
* through two seams: human `logger` lines (visible with `new Ablo({ debug: true })`)
|
|
7
|
+
* and structured `observability.captureClaim` / `captureConflict` calls.
|
|
8
|
+
*
|
|
9
|
+
* This is the third, evals-shaped path: hand a {@link ClaimLog} to
|
|
10
|
+
* `Ablo({ observability })`, run your scenario, then read back an ordered list
|
|
11
|
+
* you can print for eyeballing or `collisions()` for assertions.
|
|
12
|
+
*/
|
|
13
|
+
import type { SyncObservabilityProvider, ClaimEvent, ConflictEvent } from '../interfaces/index.js';
|
|
14
|
+
/** A claim state change as one quiet, greppable line. */
|
|
15
|
+
export declare function formatClaim(e: ClaimEvent): string;
|
|
16
|
+
/** A notify-instead-of-abort stale write as one readable line. */
|
|
17
|
+
export declare function formatConflict(e: ConflictEvent): string;
|
|
18
|
+
/** One ordered entry in a {@link ClaimLog}. */
|
|
19
|
+
export interface ClaimLogEntry {
|
|
20
|
+
/** Monotonic order index — deterministic, clock-free, eval-friendly. */
|
|
21
|
+
readonly seq: number;
|
|
22
|
+
/** The same one-line text the console and breadcrumb seams emit. */
|
|
23
|
+
readonly line: string;
|
|
24
|
+
/** A collision worth flagging: a rejected/lost claim or a stale write. */
|
|
25
|
+
readonly collision: boolean;
|
|
26
|
+
readonly claim?: ClaimEvent;
|
|
27
|
+
readonly conflict?: ConflictEvent;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Collects the claim lifecycle + stale-write collisions into an ordered list.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const log = new ClaimLog();
|
|
35
|
+
* const ablo = new Ablo({ schema, apiKey, observability: log });
|
|
36
|
+
* // …run the agents…
|
|
37
|
+
* console.log(`${log}`); // pretty timeline for eyeballing
|
|
38
|
+
* expect(log.collisions()).toHaveLength(0); // assert no one stepped on anyone
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Implements the full {@link SyncObservabilityProvider} (every method it does not
|
|
42
|
+
* care about is an inert no-op), so it drops straight into the `observability`
|
|
43
|
+
* slot with no adapter.
|
|
44
|
+
*/
|
|
45
|
+
export declare class ClaimLog implements SyncObservabilityProvider {
|
|
46
|
+
private readonly max;
|
|
47
|
+
private rows;
|
|
48
|
+
private seq;
|
|
49
|
+
private readonly listeners;
|
|
50
|
+
/** Cap the buffer so a long-running session can't grow unbounded. */
|
|
51
|
+
constructor(max?: number);
|
|
52
|
+
captureClaim(claim: ClaimEvent): void;
|
|
53
|
+
captureConflict(conflict: ConflictEvent): void;
|
|
54
|
+
/**
|
|
55
|
+
* Fire `listener` every time an event lands. Returns an unsubscribe fn. Same
|
|
56
|
+
* contract as `ablo.claims.onChange` — drop it into `useSyncExternalStore` in
|
|
57
|
+
* React or `autorun` in MobX to render a live activity feed.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```tsx
|
|
61
|
+
* const log = useMemo(() => new ClaimLog(), []);
|
|
62
|
+
* const entries = useSyncExternalStore(log.onChange, () => log.entries);
|
|
63
|
+
* return <ul>{entries.map((e) => <li key={e.seq}>{e.line}</li>)}</ul>;
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
readonly onChange: (listener: () => void) => (() => void);
|
|
67
|
+
/** The full ordered list. Stable reference until the next event lands. */
|
|
68
|
+
get entries(): readonly ClaimLogEntry[];
|
|
69
|
+
/** Only the collisions: rejected/lost claims + stale writes. */
|
|
70
|
+
collisions(): readonly ClaimLogEntry[];
|
|
71
|
+
/** Drop everything — useful between scenarios in one process. */
|
|
72
|
+
clear(): void;
|
|
73
|
+
/** Printable: one event per line, collisions marked. */
|
|
74
|
+
toString(): string;
|
|
75
|
+
private add;
|
|
76
|
+
private notify;
|
|
77
|
+
setContext(): void;
|
|
78
|
+
setConnectionState(): void;
|
|
79
|
+
breadcrumb(): void;
|
|
80
|
+
captureRollback(): void;
|
|
81
|
+
captureTransactionFailure(): void;
|
|
82
|
+
captureBootstrapFailure(): void;
|
|
83
|
+
captureReconciliation(): void;
|
|
84
|
+
captureDeltaRetryExhausted(): void;
|
|
85
|
+
captureWebSocketError(): void;
|
|
86
|
+
captureOfflineFlushFailure(): void;
|
|
87
|
+
captureSelfHealing(): void;
|
|
88
|
+
captureCommitZeroSyncId(): void;
|
|
89
|
+
startSpan<T>(_name: string, _op: string, fn: () => T): T;
|
|
90
|
+
startSpanAsync<T>(_name: string, _op: string, fn: () => Promise<T>): Promise<T>;
|
|
91
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claim log — the easy way to SEE agents colliding.
|
|
3
|
+
*
|
|
4
|
+
* The engine emits the claim lifecycle (`acquired → queued → granted → lost /
|
|
5
|
+
* rejected / expired`) and the notify-instead-of-abort stale-write collision
|
|
6
|
+
* through two seams: human `logger` lines (visible with `new Ablo({ debug: true })`)
|
|
7
|
+
* and structured `observability.captureClaim` / `captureConflict` calls.
|
|
8
|
+
*
|
|
9
|
+
* This is the third, evals-shaped path: hand a {@link ClaimLog} to
|
|
10
|
+
* `Ablo({ observability })`, run your scenario, then read back an ordered list
|
|
11
|
+
* you can print for eyeballing or `collisions()` for assertions.
|
|
12
|
+
*/
|
|
13
|
+
// ─────────────────────────────────────────────
|
|
14
|
+
// Formatters — one readable line per event. Shared by the WS logger seam and
|
|
15
|
+
// the log so console output and log output never drift.
|
|
16
|
+
// ─────────────────────────────────────────────
|
|
17
|
+
/** A claim state change as one quiet, greppable line. */
|
|
18
|
+
export function formatClaim(e) {
|
|
19
|
+
const target = e.model && e.id ? `${e.model}/${e.id}` : e.claimId ?? 'unknown target';
|
|
20
|
+
const scope = e.field ? `#${e.field}` : '';
|
|
21
|
+
const pos = e.position !== undefined ? ` [pos ${e.position}]` : '';
|
|
22
|
+
const actor = e.actor
|
|
23
|
+
? `${e.actor}${e.participantKind ? ` (${e.participantKind})` : ''}`
|
|
24
|
+
: '';
|
|
25
|
+
// `rejected`/`lost` name the BLOCKING holder; the rest name us (or no one).
|
|
26
|
+
const by = actor
|
|
27
|
+
? e.phase === 'rejected' || e.phase === 'lost'
|
|
28
|
+
? ` — held by ${actor}`
|
|
29
|
+
: ` — by ${actor}`
|
|
30
|
+
: '';
|
|
31
|
+
const why = e.reason ? `: ${e.reason}` : '';
|
|
32
|
+
return `claim ${e.phase}: ${target}${scope}${pos}${by}${why}`;
|
|
33
|
+
}
|
|
34
|
+
/** A notify-instead-of-abort stale write as one readable line. */
|
|
35
|
+
export function formatConflict(e) {
|
|
36
|
+
const rows = e.rows.map((r) => `${r.model}/${r.id}(${r.fields.join(',')})`).join(', ');
|
|
37
|
+
return `conflict: tx ${e.clientTxId} — ${e.rows.length} row(s) changed underneath${rows ? `: ${rows}` : ''}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Collects the claim lifecycle + stale-write collisions into an ordered list.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const log = new ClaimLog();
|
|
45
|
+
* const ablo = new Ablo({ schema, apiKey, observability: log });
|
|
46
|
+
* // …run the agents…
|
|
47
|
+
* console.log(`${log}`); // pretty timeline for eyeballing
|
|
48
|
+
* expect(log.collisions()).toHaveLength(0); // assert no one stepped on anyone
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* Implements the full {@link SyncObservabilityProvider} (every method it does not
|
|
52
|
+
* care about is an inert no-op), so it drops straight into the `observability`
|
|
53
|
+
* slot with no adapter.
|
|
54
|
+
*/
|
|
55
|
+
export class ClaimLog {
|
|
56
|
+
max;
|
|
57
|
+
// Immutable list: a NEW array reference on every change. This is what lets
|
|
58
|
+
// `useSyncExternalStore(log.onChange, () => log.entries)` detect updates —
|
|
59
|
+
// it compares snapshots by reference, so an in-place push would never render.
|
|
60
|
+
rows = [];
|
|
61
|
+
seq = 0;
|
|
62
|
+
listeners = new Set();
|
|
63
|
+
/** Cap the buffer so a long-running session can't grow unbounded. */
|
|
64
|
+
constructor(max = 1_000) {
|
|
65
|
+
this.max = max;
|
|
66
|
+
}
|
|
67
|
+
// —— the two seams we record ——
|
|
68
|
+
captureClaim(claim) {
|
|
69
|
+
this.add({
|
|
70
|
+
line: formatClaim(claim),
|
|
71
|
+
collision: claim.phase === 'rejected' || claim.phase === 'lost',
|
|
72
|
+
claim,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
captureConflict(conflict) {
|
|
76
|
+
this.add({ line: formatConflict(conflict), collision: true, conflict });
|
|
77
|
+
}
|
|
78
|
+
// —— reactivity ——
|
|
79
|
+
/**
|
|
80
|
+
* Fire `listener` every time an event lands. Returns an unsubscribe fn. Same
|
|
81
|
+
* contract as `ablo.claims.onChange` — drop it into `useSyncExternalStore` in
|
|
82
|
+
* React or `autorun` in MobX to render a live activity feed.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```tsx
|
|
86
|
+
* const log = useMemo(() => new ClaimLog(), []);
|
|
87
|
+
* const entries = useSyncExternalStore(log.onChange, () => log.entries);
|
|
88
|
+
* return <ul>{entries.map((e) => <li key={e.seq}>{e.line}</li>)}</ul>;
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
onChange = (listener) => {
|
|
92
|
+
this.listeners.add(listener);
|
|
93
|
+
return () => this.listeners.delete(listener);
|
|
94
|
+
};
|
|
95
|
+
// —— inspection ——
|
|
96
|
+
/** The full ordered list. Stable reference until the next event lands. */
|
|
97
|
+
get entries() {
|
|
98
|
+
return this.rows;
|
|
99
|
+
}
|
|
100
|
+
/** Only the collisions: rejected/lost claims + stale writes. */
|
|
101
|
+
collisions() {
|
|
102
|
+
return this.rows.filter((e) => e.collision);
|
|
103
|
+
}
|
|
104
|
+
/** Drop everything — useful between scenarios in one process. */
|
|
105
|
+
clear() {
|
|
106
|
+
this.rows = [];
|
|
107
|
+
this.seq = 0;
|
|
108
|
+
this.notify();
|
|
109
|
+
}
|
|
110
|
+
/** Printable: one event per line, collisions marked. */
|
|
111
|
+
toString() {
|
|
112
|
+
if (this.rows.length === 0)
|
|
113
|
+
return 'claim log: (empty)';
|
|
114
|
+
const w = String(this.rows.length).length;
|
|
115
|
+
return this.rows
|
|
116
|
+
.map((e) => `${String(e.seq).padStart(w)} ${e.collision ? '⚠ ' : ' '}${e.line}`)
|
|
117
|
+
.join('\n');
|
|
118
|
+
}
|
|
119
|
+
add(entry) {
|
|
120
|
+
const next = [...this.rows, { seq: this.seq++, ...entry }];
|
|
121
|
+
this.rows = next.length > this.max ? next.slice(next.length - this.max) : next;
|
|
122
|
+
this.notify();
|
|
123
|
+
}
|
|
124
|
+
notify() {
|
|
125
|
+
for (const listener of this.listeners)
|
|
126
|
+
listener();
|
|
127
|
+
}
|
|
128
|
+
// —— inert provider surface (this log only cares about claims) ——
|
|
129
|
+
setContext() { }
|
|
130
|
+
setConnectionState() { }
|
|
131
|
+
breadcrumb() { }
|
|
132
|
+
captureRollback() { }
|
|
133
|
+
captureTransactionFailure() { }
|
|
134
|
+
captureBootstrapFailure() { }
|
|
135
|
+
captureReconciliation() { }
|
|
136
|
+
captureDeltaRetryExhausted() { }
|
|
137
|
+
captureWebSocketError() { }
|
|
138
|
+
captureOfflineFlushFailure() { }
|
|
139
|
+
captureSelfHealing() { }
|
|
140
|
+
captureCommitZeroSyncId() { }
|
|
141
|
+
startSpan(_name, _op, fn) {
|
|
142
|
+
return fn();
|
|
143
|
+
}
|
|
144
|
+
startSpanAsync(_name, _op, fn) {
|
|
145
|
+
return fn();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -274,7 +274,9 @@ export class StoreManager {
|
|
|
274
274
|
* Clear all stores
|
|
275
275
|
*/
|
|
276
276
|
async clearAllStores() {
|
|
277
|
-
|
|
277
|
+
// Lifecycle chatter (logout / identity switch / reset) — NOT a warning.
|
|
278
|
+
// `debug` so it's silent under the default `warn` threshold.
|
|
279
|
+
getContext().logger.debug('Clearing all stores');
|
|
278
280
|
const promises = Array.from(this.stores.values()).map((store) => store.clear());
|
|
279
281
|
await Promise.all(promises);
|
|
280
282
|
getContext().logger.info('All stores cleared');
|
package/dist/errorCodes.d.ts
CHANGED
|
@@ -135,6 +135,7 @@ export declare const ERROR_CODES: {
|
|
|
135
135
|
readonly browser_database_url_blocked: ErrorCodeSpec;
|
|
136
136
|
readonly datasource_registration_failed: ErrorCodeSpec;
|
|
137
137
|
readonly datasource_connection_unsupported: ErrorCodeSpec;
|
|
138
|
+
readonly datasource_direct_deprecated: ErrorCodeSpec;
|
|
138
139
|
readonly capability_scope_denied: ErrorCodeSpec;
|
|
139
140
|
readonly issuer_register_forbidden: ErrorCodeSpec;
|
|
140
141
|
readonly capability_invalid: ErrorCodeSpec;
|
|
@@ -188,6 +189,7 @@ export declare const ERROR_CODES: {
|
|
|
188
189
|
readonly unique_violation: ErrorCodeSpec;
|
|
189
190
|
readonly check_violation: ErrorCodeSpec;
|
|
190
191
|
readonly constraint_violation: ErrorCodeSpec;
|
|
192
|
+
readonly column_type_mismatch: ErrorCodeSpec;
|
|
191
193
|
readonly server_execute_unknown_model: ErrorCodeSpec;
|
|
192
194
|
readonly mutate_create_unknown_model: ErrorCodeSpec;
|
|
193
195
|
readonly tenant_model_columns_unknown: ErrorCodeSpec;
|
package/dist/errorCodes.js
CHANGED
|
@@ -125,6 +125,7 @@ export const ERROR_CODES = {
|
|
|
125
125
|
browser_database_url_blocked: client('auth', 'A database connection string must not be used from a browser context — it carries DB credentials.'),
|
|
126
126
|
datasource_registration_failed: client('auth', 'Failed to register the provided databaseUrl as a datasource.'),
|
|
127
127
|
datasource_connection_unsupported: wire('validation', 400, false, 'This deployment cannot register a direct (connection string) datasource — use the signed endpoint kind.'),
|
|
128
|
+
datasource_direct_deprecated: wire('validation', 410, false, 'The direct (connection string) datasource is deprecated. Register a signed Data Source endpoint instead — your app owns the write and your credentials never leave it.'),
|
|
128
129
|
// ── permission / capability (403) ──────────────────────────────────
|
|
129
130
|
capability_scope_denied: wire('capability', 403, false, "The connection's resolved scope does not cover the attempted action."),
|
|
130
131
|
issuer_register_forbidden: wire('permission', 403, false, 'Registering a trusted issuer requires a secret (sk_) API key.'),
|
|
@@ -201,6 +202,7 @@ export const ERROR_CODES = {
|
|
|
201
202
|
unique_violation: wire('conflict', 409, false, 'A value violates a uniqueness constraint.'),
|
|
202
203
|
check_violation: wire('validation', 400, false, 'A value violates a database check constraint.'),
|
|
203
204
|
constraint_violation: wire('validation', 400, false, 'A database integrity constraint was violated.'),
|
|
205
|
+
column_type_mismatch: wire('validation', 400, false, 'A structured (JSON) value was written to a column whose database type is not jsonb. The column was not provisioned as jsonb (often a pre-existing column adopted by `ablo push`); storing the value would silently corrupt it to "[object Object]". Run `ablo migrate` to alter the column to jsonb.'),
|
|
204
206
|
// ── tenant / unknown model (400) ───────────────────────────────────
|
|
205
207
|
server_execute_unknown_model: wire('tenant', 400, false, 'Wrote to a model the server does not know. The server keeps its own copy of the schema — run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` before writing to new or changed models.'),
|
|
206
208
|
mutate_create_unknown_model: wire('tenant', 400, false, 'Created a model the server does not know. Run `ablo push` (or keep `ablo dev` running) to upload `ablo/schema.ts` first — the server keeps its own copy of the schema.'),
|
package/dist/errors.d.ts
CHANGED
|
@@ -77,6 +77,15 @@ export declare class AbloError extends Error {
|
|
|
77
77
|
request_id?: string;
|
|
78
78
|
[key: string]: unknown;
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* A single, leak-proof line for logs and `String(err)` / template
|
|
82
|
+
* interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
|
|
85
|
+
* makes `console.error(richError)` an unreadable wall of text. The structured
|
|
86
|
+
* payload stays available via {@link toJSON}; this is the human one-liner.
|
|
87
|
+
*/
|
|
88
|
+
toString(): string;
|
|
80
89
|
}
|
|
81
90
|
/**
|
|
82
91
|
* Map a stable error `code` to its docs URL — the one place the convention
|
|
@@ -176,8 +185,7 @@ export interface ClaimContext {
|
|
|
176
185
|
readonly claimId?: string;
|
|
177
186
|
readonly actor?: string;
|
|
178
187
|
readonly participantKind?: ParticipantKind;
|
|
179
|
-
/** Human-readable phase the holder is in (`'editing'`).
|
|
180
|
-
* claim surface; the wire summary carries the same value as `action`. */
|
|
188
|
+
/** Human-readable phase the holder is in (`'editing'`). */
|
|
181
189
|
readonly reason?: string;
|
|
182
190
|
readonly description?: string;
|
|
183
191
|
readonly field?: string;
|
package/dist/errors.js
CHANGED
|
@@ -91,6 +91,20 @@ export class AbloError extends Error {
|
|
|
91
91
|
...(this.details ?? {}),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* A single, leak-proof line for logs and `String(err)` / template
|
|
96
|
+
* interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
|
|
99
|
+
* makes `console.error(richError)` an unreadable wall of text. The structured
|
|
100
|
+
* payload stays available via {@link toJSON}; this is the human one-liner.
|
|
101
|
+
*/
|
|
102
|
+
toString() {
|
|
103
|
+
const code = this.code ? ` [${this.code}]` : '';
|
|
104
|
+
const docs = this.docUrl ? ` (see ${this.docUrl})` : '';
|
|
105
|
+
const req = this.requestId ? ` [request_id: ${this.requestId}]` : '';
|
|
106
|
+
return `${this.name}${code}: ${this.message}${docs}${req}`;
|
|
107
|
+
}
|
|
94
108
|
}
|
|
95
109
|
/**
|
|
96
110
|
* Map a stable error `code` to its docs URL — the one place the convention
|
|
@@ -182,13 +196,10 @@ export class AbloStaleContextError extends AbloError {
|
|
|
182
196
|
this.conflicts = options.conflicts;
|
|
183
197
|
}
|
|
184
198
|
}
|
|
185
|
-
function
|
|
199
|
+
function claimReason(claim) {
|
|
186
200
|
if (!claim)
|
|
187
201
|
return undefined;
|
|
188
|
-
|
|
189
|
-
// `WireClaimSummary` projection still carries it under `action`. Read both.
|
|
190
|
-
const c = claim;
|
|
191
|
-
return c.reason ?? c.action;
|
|
202
|
+
return claim.reason;
|
|
192
203
|
}
|
|
193
204
|
function claimDescription(claim) {
|
|
194
205
|
if (!claim)
|
|
@@ -215,20 +226,20 @@ function secondsUntil(ms, now = Date.now()) {
|
|
|
215
226
|
}
|
|
216
227
|
export function formatClaimedErrorMessage(args) {
|
|
217
228
|
const holder = claimActor(args.claim, args.heldBy);
|
|
218
|
-
const
|
|
229
|
+
const reason = claimReason(args.claim);
|
|
219
230
|
const description = claimDescription(args.claim);
|
|
220
231
|
const expiresIn = secondsUntil(claimExpiresAt(args.claim));
|
|
221
|
-
if (!holder && !
|
|
232
|
+
if (!holder && !reason && !description) {
|
|
222
233
|
return args.fallback ?? `Model row is claimed: ${args.targetLabel}.`;
|
|
223
234
|
}
|
|
224
235
|
const actor = holder ?? 'another participant';
|
|
225
|
-
const
|
|
236
|
+
const reasonPart = reason ? ` (${reason})` : '';
|
|
226
237
|
const descriptionPart = description ? `: ${description}` : '';
|
|
227
238
|
const expiresPart = expiresIn !== undefined ? ` - expires in ${expiresIn}s` : '';
|
|
228
239
|
const policyPart = args.policyReason
|
|
229
240
|
? ` Policy reason: ${args.policyReason}.`
|
|
230
241
|
: '';
|
|
231
|
-
return `Claimed by ${actor}${
|
|
242
|
+
return `Claimed by ${actor}${reasonPart}${descriptionPart}${expiresPart} on ${args.targetLabel}.${policyPart}`;
|
|
232
243
|
}
|
|
233
244
|
/**
|
|
234
245
|
* The target entity is currently claimed by another participant and the caller
|
package/dist/index.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export type { MutationExecutor } from './interfaces/index.js';
|
|
|
52
52
|
export type { HttpClaimApi, InternalAbloOptions } from './client/Ablo.js';
|
|
53
53
|
export { type AbloHttpClientOptions, type AbloHttpClient, type HttpModelClient, } from './client/httpClient.js';
|
|
54
54
|
export { ABLO_DEFAULT_BASE_URL, ABLO_HOSTED_API_DOMAIN, ABLO_HOSTED_HTTP_BASE_URL, normalizeAbloHostedBaseUrl, } from './client/auth.js';
|
|
55
|
-
export type { AbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams,
|
|
55
|
+
export type { AbloOptions, LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ModelOperations, } from './client/Ablo.js';
|
|
56
56
|
export type { AbloPersistence } from './client/persistence.js';
|
|
57
57
|
import { Ablo } from './client/Ablo.js';
|
|
58
58
|
export default Ablo;
|
|
@@ -62,6 +62,8 @@ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '.
|
|
|
62
62
|
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloNotFoundError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
63
63
|
export type { CommitReceipt, RequiredCapability } from './errors.js';
|
|
64
64
|
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errors.js';
|
|
65
|
+
export { errorEnvelope, statusForType } from './wire/errorEnvelope.js';
|
|
66
|
+
export type { ErrorEnvelope } from './wire/errorEnvelope.js';
|
|
65
67
|
export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
|
|
66
68
|
export { ENVIRONMENTS, environmentSchema, normalizeEnvironment, environmentFromKeyPrefix, environmentToKeyPrefix, isSandboxEnvironment, } from './environment.js';
|
|
67
69
|
export type { Environment, KeyPrefixEnvironment } from './environment.js';
|
|
@@ -70,6 +72,10 @@ export type { WriteOptionsInput } from './client/writeOptionsSchema.js';
|
|
|
70
72
|
export type { WriteOptions, MutationOptions } from './interfaces/index.js';
|
|
71
73
|
export { staleNotificationSchema, readDependencySchema } from './coordination/schema.js';
|
|
72
74
|
export type { StaleNotification, ReadDependency } from './coordination/schema.js';
|
|
75
|
+
export { ClaimLog, formatClaim, formatConflict } from './coordination/trace.js';
|
|
76
|
+
export type { ClaimLogEntry } from './coordination/trace.js';
|
|
77
|
+
export type { ClaimEvent, ConflictEvent, SyncObservabilityProvider, } from './interfaces/index.js';
|
|
78
|
+
export { noopObservability } from './SyncEngineContext.js';
|
|
73
79
|
export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
|
|
74
80
|
export { PUBLIC_MODEL_VERBS, PUBLIC_LIST_OPTION_KEYS, PUBLIC_ABLO_OPTION_KEYS, } from './surface.js';
|
|
75
81
|
export type { ModelVerb, ListOptionKey, AbloOptionKey } from './surface.js';
|
package/dist/index.js
CHANGED
|
@@ -85,6 +85,11 @@ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '.
|
|
|
85
85
|
// consumers need to discriminate failures (`e instanceof AbloX` or
|
|
86
86
|
// `e.type === 'AbloX'`) plus the HTTP-response translator.
|
|
87
87
|
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloNotFoundError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
88
|
+
// Canonical wire-egress contract (dependency-free): the error envelope shape +
|
|
89
|
+
// the AbloError-subclass→HTTP-status table. Re-exported so server consumers
|
|
90
|
+
// (e.g. apps/sync-server, which keeps its own self-contained copy) can assert
|
|
91
|
+
// against the ONE source instead of silently drifting. See wire/errorEnvelope.ts.
|
|
92
|
+
export { errorEnvelope, statusForType } from './wire/errorEnvelope.js';
|
|
88
93
|
export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
|
|
89
94
|
export { ENVIRONMENTS, environmentSchema, normalizeEnvironment, environmentFromKeyPrefix, environmentToKeyPrefix, isSandboxEnvironment, } from './environment.js';
|
|
90
95
|
// THE write-options contract — the one Zod schema for the option bag every
|
|
@@ -99,6 +104,13 @@ export { writeOptionsSchema, onStaleModeSchema, assertWriteOptions, } from './cl
|
|
|
99
104
|
// agent can self-heal rather than discard work (see coordination/schema.ts and
|
|
100
105
|
// docs/coordination.md → "Notify, do not abort").
|
|
101
106
|
export { staleNotificationSchema, readDependencySchema } from './coordination/schema.js';
|
|
107
|
+
// Claim log — collect claim events + stale-write collisions into an ordered list
|
|
108
|
+
// you can print for eyeballing or collisions() for evals assertions. Hand
|
|
109
|
+
// `new ClaimLog()` to `Ablo({ observability })`.
|
|
110
|
+
export { ClaimLog, formatClaim, formatConflict } from './coordination/trace.js';
|
|
111
|
+
// Spread this to provide a custom `observability` that overrides only the hooks
|
|
112
|
+
// you care about (e.g. captureClaim) and no-ops the rest.
|
|
113
|
+
export { noopObservability } from './SyncEngineContext.js';
|
|
102
114
|
// Storage-wedge detection — lets app shells render a recovery screen when the
|
|
103
115
|
// IndexedDB backing store is stuck (see core/openIDBWithTimeout.ts).
|
|
104
116
|
export { IDBOpenTimeoutError, isStorageOpenTimeout } from './core/openIDBWithTimeout.js';
|