@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.
Files changed (126) hide show
  1. package/CHANGELOG.md +59 -3
  2. package/README.md +1 -1
  3. package/dist/BaseSyncedStore.js +8 -9
  4. package/dist/Database.d.ts +14 -1
  5. package/dist/Database.js +228 -28
  6. package/dist/Model.d.ts +17 -0
  7. package/dist/Model.js +32 -1
  8. package/dist/SyncClient.d.ts +10 -6
  9. package/dist/SyncClient.js +390 -75
  10. package/dist/adapters/inMemoryStorage.d.ts +1 -0
  11. package/dist/adapters/inMemoryStorage.js +29 -13
  12. package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
  13. package/dist/auth/schemas.d.ts +6 -0
  14. package/dist/auth/schemas.js +7 -0
  15. package/dist/cli.cjs +821 -402
  16. package/dist/client/Ablo.d.ts +53 -24
  17. package/dist/client/Ablo.js +12 -199
  18. package/dist/client/claimHeartbeatLoop.d.ts +1 -1
  19. package/dist/client/claimHeartbeatLoop.js +1 -1
  20. package/dist/client/createInternalComponents.d.ts +4 -0
  21. package/dist/client/createInternalComponents.js +3 -1
  22. package/dist/client/durableWrites.d.ts +21 -0
  23. package/dist/client/durableWrites.js +46 -0
  24. package/dist/client/httpClient.d.ts +21 -45
  25. package/dist/client/httpClient.js +104 -26
  26. package/dist/client/httpTransport.d.ts +8 -0
  27. package/dist/client/{ApiClient.js → httpTransport.js} +361 -308
  28. package/dist/client/modelRegistration.js +11 -0
  29. package/dist/client/options.d.ts +54 -7
  30. package/dist/client/options.js +3 -2
  31. package/dist/client/resourceTypes.d.ts +9 -85
  32. package/dist/client/resourceTypes.js +1 -1
  33. package/dist/client/sessionMint.d.ts +5 -6
  34. package/dist/client/sessionMint.js +4 -5
  35. package/dist/client/validateAbloOptions.js +3 -3
  36. package/dist/client/wsMutationExecutor.d.ts +3 -2
  37. package/dist/client/wsMutationExecutor.js +3 -2
  38. package/dist/coordination/schema.d.ts +30 -0
  39. package/dist/coordination/schema.js +14 -0
  40. package/dist/core/StoreManager.d.ts +2 -0
  41. package/dist/core/StoreManager.js +12 -0
  42. package/dist/core/index.d.ts +1 -1
  43. package/dist/errorCodes.js +6 -2
  44. package/dist/errors.d.ts +4 -40
  45. package/dist/errors.js +5 -5
  46. package/dist/index.d.ts +20 -8
  47. package/dist/index.js +9 -5
  48. package/dist/interfaces/index.d.ts +5 -2
  49. package/dist/mutators/UndoManager.d.ts +2 -0
  50. package/dist/mutators/UndoManager.js +32 -0
  51. package/dist/mutators/defineMutators.d.ts +18 -0
  52. package/dist/mutators/defineMutators.js +4 -8
  53. package/dist/react/index.d.ts +1 -1
  54. package/dist/react/useAblo.d.ts +6 -4
  55. package/dist/react/useAblo.js +25 -3
  56. package/dist/schema/index.d.ts +2 -2
  57. package/dist/schema/index.js +1 -1
  58. package/dist/schema/schema.d.ts +31 -1
  59. package/dist/schema/select.d.ts +15 -0
  60. package/dist/schema/select.js +27 -3
  61. package/dist/source/connector.d.ts +3 -3
  62. package/dist/source/factory.d.ts +4 -6
  63. package/dist/source/factory.js +4 -7
  64. package/dist/source/index.d.ts +4 -4
  65. package/dist/source/index.js +2 -2
  66. package/dist/source/signing.d.ts +0 -3
  67. package/dist/source/types.d.ts +0 -26
  68. package/dist/stores/ObjectStore.d.ts +14 -1
  69. package/dist/stores/ObjectStore.js +33 -10
  70. package/dist/stores/ObjectStoreContract.d.ts +2 -0
  71. package/dist/surface.d.ts +1 -1
  72. package/dist/surface.js +3 -0
  73. package/dist/sync/BootstrapFetcher.d.ts +10 -0
  74. package/dist/sync/BootstrapFetcher.js +27 -4
  75. package/dist/sync/SyncWebSocket.d.ts +2 -1
  76. package/dist/sync/createClaimStream.d.ts +11 -2
  77. package/dist/sync/createClaimStream.js +4 -3
  78. package/dist/sync/persistedPrefix.d.ts +12 -0
  79. package/dist/sync/persistedPrefix.js +22 -0
  80. package/dist/testing/index.d.ts +2 -0
  81. package/dist/testing/index.js +1 -0
  82. package/dist/testing/mocks/FakeDatabase.d.ts +18 -0
  83. package/dist/testing/mocks/FakeDatabase.js +10 -0
  84. package/dist/testing/mocks/MockWebSocket.d.ts +2 -1
  85. package/dist/transactions/TransactionQueue.d.ts +66 -8
  86. package/dist/transactions/TransactionQueue.js +607 -89
  87. package/dist/transactions/commitEnvelope.d.ts +132 -0
  88. package/dist/transactions/commitEnvelope.js +139 -0
  89. package/dist/transactions/commitOutboxStore.d.ts +32 -0
  90. package/dist/transactions/commitOutboxStore.js +26 -0
  91. package/dist/transactions/commitPayload.d.ts +15 -0
  92. package/dist/transactions/commitPayload.js +6 -0
  93. package/dist/transactions/durableWriteStore.d.ts +123 -0
  94. package/dist/transactions/durableWriteStore.js +30 -0
  95. package/dist/transactions/httpCommitEnvelope.d.ts +44 -0
  96. package/dist/transactions/httpCommitEnvelope.js +181 -0
  97. package/dist/transactions/idempotencyKey.d.ts +10 -0
  98. package/dist/transactions/idempotencyKey.js +9 -0
  99. package/dist/transactions/replayValidation.d.ts +83 -0
  100. package/dist/transactions/replayValidation.js +46 -1
  101. package/dist/types/global.d.ts +10 -29
  102. package/dist/types/global.js +4 -7
  103. package/dist/types/streams.d.ts +2 -28
  104. package/dist/wire/bootstrapReason.d.ts +9 -0
  105. package/dist/wire/bootstrapReason.js +8 -0
  106. package/dist/wire/frames.d.ts +6 -21
  107. package/dist/wire/frames.js +4 -4
  108. package/dist/wire/index.d.ts +6 -3
  109. package/dist/wire/index.js +3 -2
  110. package/dist/wire/protocolVersion.d.ts +30 -17
  111. package/dist/wire/protocolVersion.js +34 -18
  112. package/docs/agents.md +8 -3
  113. package/docs/api.md +16 -14
  114. package/docs/client-behavior.md +6 -3
  115. package/docs/coordination.md +4 -5
  116. package/docs/data-sources.md +5 -8
  117. package/docs/guarantees.md +21 -0
  118. package/docs/integration-guide.md +1 -0
  119. package/docs/mcp.md +1 -1
  120. package/docs/migration.md +21 -1
  121. package/docs/quickstart.md +11 -0
  122. package/docs/react.md +0 -46
  123. package/examples/README.md +6 -5
  124. package/llms.txt +15 -15
  125. package/package.json +3 -3
  126. package/dist/client/ApiClient.d.ts +0 -177
@@ -192,8 +192,19 @@ function isZodObject(schema) {
192
192
  }
193
193
  /** Create a Model subclass for a schema-defined model */
194
194
  function createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, lazyObservable = false) {
195
+ // Names `toReactiveSnapshot` materializes onto plain snapshots: every
196
+ // `${field}Json` getter plus every schema-declared computed. Without this,
197
+ // snapshot rows silently drop getters that the schema's inferred row type
198
+ // declares — reads like `snapshot.settingsObject` would be `undefined`.
199
+ const derivedGetterNames = Object.freeze([
200
+ ...jsonSubFields.map(({ fieldName }) => `${fieldName}Json`),
201
+ ...Object.keys(computed ?? {}),
202
+ ]);
195
203
  const ModelClass = class extends Model {
196
204
  _modelName = modelName;
205
+ getDerivedGetterNames() {
206
+ return derivedGetterNames;
207
+ }
197
208
  constructor(data) {
198
209
  super(data);
199
210
  // Suppress change tracking during initial hydration. `makeObservable()`
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * The option types for the {@link Ablo} client: the public {@link AbloOptions}
3
- * bag callers pass, and the fuller {@link InternalAbloOptions} construction
4
- * surface. This module holds only types and has no runtime imports.
3
+ * bag callers pass, and the fuller monorepo-only {@link InternalAbloOptions}
4
+ * construction surface exposed through `@abloatai/ablo/core`.
5
+ * This module holds only types and has no runtime imports.
5
6
  */
6
7
  import type { Schema, SchemaRecord } from '../schema/schema.js';
7
8
  import type { SyncEngineConfig, SyncLogger, MutationExecutor, SyncObservabilityProvider, SyncAnalytics, SessionErrorDetector, OnlineStatusProvider } from '../interfaces/index.js';
8
9
  import type { AbloPersistence } from './persistence.js';
10
+ import type { DurableWriteStore, DurableWritesConfig } from '../transactions/durableWriteStore.js';
11
+ import type { CommitOutboxScope } from '../transactions/commitEnvelope.js';
9
12
  /**
10
13
  * An async function that resolves an apiKey at request time. Use it for credential
11
14
  * rotation — read from a vault, refresh from session storage, or pull from an
@@ -108,6 +111,45 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
108
111
  * @default 'memory'
109
112
  */
110
113
  persistence?: AbloPersistence;
114
+ /**
115
+ * Makes `create`, `update`, and `delete` survive process restarts and
116
+ * ambiguous network responses. Server-side agents and workers inject a
117
+ * workflow-, SQLite-, or filesystem-backed store; authenticated identity is
118
+ * used to fence replay to the actor that originally made the write.
119
+ *
120
+ * Most clients leave this unset and use Ablo's default in-memory cache.
121
+ * `durableWrites` is independent of that cache: it is the optional outbound
122
+ * write journal for workers that must recover automatically after a crash.
123
+ * Browsers only use IndexedDB when `persistence: 'indexeddb'` is explicitly
124
+ * enabled for offline/reload behavior.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * Ablo({
129
+ * schema,
130
+ * apiKey,
131
+ * durableWrites: { store, namespace: 'agent-worker' },
132
+ * })
133
+ * ```
134
+ */
135
+ durableWrites?: DurableWritesConfig;
136
+ /**
137
+ * @deprecated Use `durableWrites: { store, namespace }`.
138
+ * Retained as a compatibility alias through the next major release.
139
+ */
140
+ commitOutbox?: DurableWriteStore;
141
+ /**
142
+ * @deprecated Actor identity is resolved from authentication. Use
143
+ * `durableWrites.namespace` only when shared storage needs separate workflow
144
+ * or deployment lanes.
145
+ *
146
+ * Stable actor/server scope for an injected HTTP `commitOutbox`.
147
+ * Omit it to resolve `organizationId` and `participantId` from the authenticated
148
+ * `/auth/identity` endpoint. Supplying it avoids that one setup request in
149
+ * trusted worker environments; `namespace` distinguishes deployments or
150
+ * workflow lanes that share the same actor identity.
151
+ */
152
+ commitOutboxScope?: CommitOutboxScope;
111
153
  /**
112
154
  * Selects the transport. `'websocket'` (the default) is the live client: a
113
155
  * persistent socket, a local synced cache, and `onChange` subscriptions. `'http'`
@@ -118,8 +160,7 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
118
160
  * {@link AbloHttpClient}, so stateful-only capabilities such as `get`, `getAll`,
119
161
  * and `onChange` become compile errors instead of runtime gaps.
120
162
  *
121
- * Note: session minting through `sessions.create` runs on the default WebSocket
122
- * client, not the HTTP client.
163
+ * Session minting through `sessions.create` is available on both transports.
123
164
  *
124
165
  * @default 'websocket'
125
166
  */
@@ -222,9 +263,9 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
222
263
  /**
223
264
  * TypeScript schema defined with `defineSchema()`.
224
265
  *
225
- * The root `Ablo(...)` client is schema-first so consumers get typed
226
- * model clients such as `ablo.weatherReports.update(...)`. Omit `schema`
227
- * only for the advanced Model / Claim / Commit client.
266
+ * Required on every public `Ablo(...)` construction. It produces typed model
267
+ * clients such as `ablo.weatherReports.update(...)`; schema-agnostic routing
268
+ * exists only inside the private HTTP transport.
228
269
  */
229
270
  schema: Schema<S>;
230
271
  /**
@@ -280,6 +321,12 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
280
321
  * reload-surviving local cache in a browser.
281
322
  */
282
323
  persistence?: AbloPersistence;
324
+ /** Internal mirror of {@link AbloOptions.durableWrites}. */
325
+ durableWrites?: DurableWritesConfig;
326
+ /** @deprecated Internal mirror of {@link AbloOptions.commitOutbox}. */
327
+ commitOutbox?: DurableWriteStore;
328
+ /** @deprecated Internal mirror of {@link AbloOptions.commitOutboxScope}. */
329
+ commitOutboxScope?: CommitOutboxScope;
283
330
  /** @deprecated Use `persistence: 'indexeddb'` for durable browser storage. */
284
331
  offline?: boolean;
285
332
  /**
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * The option types for the {@link Ablo} client: the public {@link AbloOptions}
3
- * bag callers pass, and the fuller {@link InternalAbloOptions} construction
4
- * surface. This module holds only types and has no runtime imports.
3
+ * bag callers pass, and the fuller monorepo-only {@link InternalAbloOptions}
4
+ * construction surface exposed through `@abloatai/ablo/core`.
5
+ * This module holds only types and has no runtime imports.
5
6
  */
6
7
  export {};
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The shared resource types of the client: {@link ModelRead}, {@link ModelClient},
2
+ * Shared resource types for the typed clients and private HTTP transport.
3
3
  * the commit and claim shapes, the session-mint params and resource, and the
4
4
  * {@link HttpClaimApi} derivation. This module holds only types and has no runtime
5
5
  * imports.
@@ -10,8 +10,7 @@ export type { ModelTarget, ModelClaim };
10
10
  import type { SchemaRecord } from '../schema/schema.js';
11
11
  import type { SyncGroupInput } from '../schema/roles.js';
12
12
  import type { Claim, ClaimStream, ClaimWaitOptions, Duration, HeldClaim } from '../types/streams.js';
13
- import type { ModelUpdater, ContentionOptions } from './functionalUpdate.js';
14
- import type { ClaimOptions, ClaimParams, ClaimReadApi, AwaitedClaimMethod, ServerReadOptions } from './createModelProxy.js';
13
+ import type { ClaimOptions, ClaimParams, ClaimReadApi, AwaitedClaimMethod } from './createModelProxy.js';
15
14
  /**
16
15
  * The operations available on each model in the sync engine:
17
16
  * `retrieve({ id })` — an async single-row server read
@@ -23,24 +22,13 @@ import type { ClaimOptions, ClaimParams, ClaimReadApi, AwaitedClaimMethod, Serve
23
22
  export type { LocalCountOptions, LocalReadOptions, ModelListScope, ServerReadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, ModelOperations, } from './createModelProxy.js';
24
23
  export type ModelOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
25
24
  export type CommitWait = 'queued' | 'confirmed';
26
- export interface ModelRead<T = Record<string, unknown>> {
27
- /**
28
- * The row, or `undefined` when no row matched the id (or it's outside the
29
- * caller's scope). A miss is data-absence, not an error — `retrieve` never
30
- * throws "not found", mirroring the WebSocket client's `T | undefined`.
31
- * Branch on it: `const deal = (await ablo.deals.retrieve({ id })).data; if (!deal) …`.
32
- */
33
- readonly data: T | undefined;
34
- readonly stamp: number;
35
- readonly claims: readonly ModelClaim[];
36
- }
37
25
  export type IfClaimedPolicy = 'return' | 'fail';
38
26
  export interface ClaimedOptions {
39
27
  /**
40
- * What to do when another participant has claimed the target: `return`
41
- * includes active claim metadata in the response; `fail` throws
42
- * `AbloClaimedError`. Waiting for a claim to clear is a claim-side concern —
43
- * take `ablo.<model>.claim({ id })` (it queues fairly); reads never block.
28
+ * What to do when another participant has claimed the target: `return` lets
29
+ * the read proceed; `fail` throws `AbloClaimedError`. Inspect claim state via
30
+ * `ablo.<model>.claim.state({ id })`. Waiting is a claim-side concern — take
31
+ * `ablo.<model>.claim({ id })` (it queues fairly); reads never block.
44
32
  */
45
33
  readonly ifClaimed?: IfClaimedPolicy;
46
34
  }
@@ -73,8 +61,7 @@ export interface ClaimCreateOptions {
73
61
  export interface CommitOperationInput {
74
62
  readonly action: ModelOperationAction;
75
63
  /** The model name — matches `ablo.<model>` and the schema's `model()`. */
76
- readonly model?: string;
77
- readonly target?: ModelTarget;
64
+ readonly model: string;
78
65
  readonly id?: string | null;
79
66
  readonly data?: Record<string, unknown> | null;
80
67
  readonly transactionId?: string | null;
@@ -82,9 +69,6 @@ export interface CommitOperationInput {
82
69
  readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
83
70
  }
84
71
  export interface CommitCreateOptions {
85
- readonly claimRef?: string | {
86
- readonly id: string;
87
- } | null;
88
72
  readonly idempotencyKey?: string | null;
89
73
  readonly readAt?: number | null;
90
74
  readonly onStale?: 'reject' | 'overwrite' | 'notify' | null;
@@ -97,8 +81,8 @@ export interface CommitCreateOptions {
97
81
  * Explicit `readAt`/`onStale` on the options win.
98
82
  */
99
83
  readonly claim?: Claim | null;
100
- readonly operation?: CommitOperationInput;
101
- readonly operations?: readonly CommitOperationInput[];
84
+ /** One atomic batch. Use a one-element array for a single operation. */
85
+ readonly operations: readonly CommitOperationInput[];
102
86
  readonly wait?: CommitWait;
103
87
  /**
104
88
  * Batch-level read dependencies — the "did anything I looked at change?" guard.
@@ -167,66 +151,6 @@ export interface ModelMutationOptions extends ClaimedOptions {
167
151
  export type HttpClaimApi<T = Record<string, unknown>> = ((params: ClaimParams<T>) => Promise<HeldClaim<T>>) & {
168
152
  [K in keyof ClaimReadApi<T>]: AwaitedClaimMethod<ClaimReadApi<T>[K]>;
169
153
  };
170
- export interface ModelClient<T = Record<string, unknown>> {
171
- /**
172
- * Single-row read over HTTP. **Returns an envelope, not the bare row** — the
173
- * row is on `.data`, alongside the `.stamp` watermark (for stale-context
174
- * guards on the following write) and any active `.claims`. A stateless HTTP
175
- * client can't synthesize the watermark from a local snapshot, so the
176
- * envelope is load-bearing here (the WebSocket client's `retrieve` returns
177
- * `T | undefined` because it reads from its local cache).
178
- *
179
- * ```ts
180
- * const deal = await ablo.deals.retrieve({ id });
181
- * deal.data?.recommendation; // ← the row is on .data
182
- * deal.stamp; // watermark — pass to the next write's readAt
183
- * ```
184
- */
185
- retrieve(params: ModelReadOptions & {
186
- readonly id: string;
187
- }): Promise<ModelRead<T>>;
188
- /**
189
- * Collection read over HTTP (server round-trip). Equality `where`, `orderBy`,
190
- * `limit`. Present on the stateless protocol client; the store-backed
191
- * `.model(name)` accessor omits it (use the typed `ablo.<model>.list` there).
192
- */
193
- list?(options?: ServerReadOptions<T>): Promise<T[]>;
194
- /**
195
- * Creates a row and returns the confirmed server row, including framework
196
- * defaults such as `createdAt` and `createdBy`. Matches the stateful client's
197
- * `create`. Passing an id that already exists is idempotent: the existing row is
198
- * returned, not the input.
199
- */
200
- create(params: ModelMutationOptions & {
201
- readonly data: Record<string, unknown>;
202
- readonly id?: string | null;
203
- }): Promise<T>;
204
- update(params: ModelMutationOptions & {
205
- readonly id: string;
206
- readonly data: Record<string, unknown>;
207
- }): Promise<CommitReceipt>;
208
- /**
209
- * Update under contention with a function of the latest state —
210
- * `update(id, current => next)`, the `setState(prev => next)` of the data
211
- * layer. The SDK reads the freshest row, runs your updater, writes it as a
212
- * compare-and-swap against the row's watermark, and re-reads + re-runs on any
213
- * concurrent write. No claim, no identity, no conflict codes surface: the
214
- * write either lands or throws {@link AbloContentionError} once its reconcile
215
- * budget is spent. Return `null`/`undefined` from the updater to skip the
216
- * write (resolves to `undefined`).
217
- */
218
- update(id: string, updater: ModelUpdater<T>, options?: ContentionOptions): Promise<CommitReceipt | undefined>;
219
- delete(params: ModelMutationOptions & {
220
- readonly id: string;
221
- }): Promise<CommitReceipt>;
222
- /**
223
- * Durable lease + FIFO wait-line over HTTP — coordination without a socket.
224
- * Present on the stateless protocol client (`Ablo({ schema: null })` /
225
- * `createAbloHttpClient`); the store-backed `.model(name)` accessor omits it
226
- * (the typed `ablo.<model>.claim` proxy is the full reactive namespace there).
227
- */
228
- claim?: HttpClaimApi<T>;
229
- }
230
154
  /** A single data operation a scoped **agent** session may perform on a model. */
231
155
  export type SessionOperation = 'read' | 'create' | 'update' | 'delete';
232
156
  /** Parameters for minting an end-user session — full data authority within the
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The shared resource types of the client: {@link ModelRead}, {@link ModelClient},
2
+ * Shared resource types for the typed clients and private HTTP transport.
3
3
  * the commit and claim shapes, the session-mint params and resource, and the
4
4
  * {@link HttpClaimApi} derivation. This module holds only types and has no runtime
5
5
  * imports.
@@ -10,14 +10,13 @@ export interface MintSessionContext {
10
10
  readonly baseUrl: string;
11
11
  readonly fetch?: typeof fetch;
12
12
  /**
13
- * Maps each schema key to its wire type name. Only the schema-aware client
14
- * supplies this. A capability is scoped by the lowercased type name the
15
- * server checks, but `can` is keyed by schema key so
13
+ * Maps each schema key to its wire type name. Every public client supplies
14
+ * this; it stays optional only for isolated private-transport use. A
15
+ * capability is scoped by the lowercased type name the server checks, but
16
+ * `can` is keyed by schema key — so
16
17
  * `can: { documents: ['update'] }` on a model whose type name is overridden
17
18
  * to `Document` must mint `document.update`, not `documents.update`, or the
18
- * server denies the write with `capability_scope_denied`. The schemaless
19
- * client omits this map, because there the `can` key is already the wire
20
- * token and needs no translation.
19
+ * server denies the write with `capability_scope_denied`.
21
20
  */
22
21
  readonly modelTypenames?: Readonly<Record<string, string>>;
23
22
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Mints a session token. This is the single implementation behind
3
- * `sessions.create`, shared by the schema-aware client and the schemaless HTTP
4
- * client so the two never disagree on how a token is produced.
3
+ * `sessions.create`, shared by the WebSocket and HTTP clients so the two never
4
+ * disagree on how a token is produced.
5
5
  *
6
6
  * Minting is a plain control-plane HTTP call — no socket, no synced data. A
7
7
  * backend holding a secret key (`sk_`) exchanges it for a short-lived, scoped
@@ -60,9 +60,8 @@ export async function mintSession(params, ctx) {
60
60
  }
61
61
  const operations = Object.entries(params.can).flatMap(([model, ops]) => {
62
62
  // Translate the schema key the developer used in `can` into the wire type
63
- // name the server checks — see `modelTypenames` above. Falls back to the
64
- // key itself when no map is supplied (the schemaless client) or the key is
65
- // absent from it.
63
+ // name the server checks — see `modelTypenames` above. The key fallback is
64
+ // private transport tolerance; every public client supplies the schema map.
66
65
  const ns = ctx.modelTypenames?.[model] ?? model;
67
66
  return (ops ?? []).map((op) => `${ns.toLowerCase()}.${op}`);
68
67
  });
@@ -17,9 +17,9 @@ export function validateAbloOptions(input) {
17
17
  return new AbloValidationError('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
18
18
  `Ablo({ baseURL: 'wss://api.abloatai.com', schema, user })`, { code: 'base_url_missing' });
19
19
  }
20
- // Schema is optional for the model-first API:
21
- // Ablo({ apiKey }).model('clauses').retrieve(...)
22
- // Passing a schema only enables typed model sugar (`ablo.weatherReports.update(...)`).
20
+ if (!options.schema?.models) {
21
+ return new AbloValidationError('Ablo: `schema` is required. Define it once and access models as `ablo.<model>`.', { code: 'invalid_options', param: 'schema' });
22
+ }
23
23
  if (!configuredApiKey &&
24
24
  !configuredAuthToken &&
25
25
  !options.capabilityToken &&
@@ -17,8 +17,9 @@ import type { MutationExecutor, MutationOperation } from '../interfaces/index.js
17
17
  * the ready socket.
18
18
  *
19
19
  * When set, `options.idempotencyKey` is sent as the wire-level `clientTxId`, so
20
- * retrying a call with the same key is safe; otherwise the executor generates
21
- * one.
20
+ * retrying a call with the same key is safe. TransactionQueue always supplies
21
+ * this key before its first attempt and owns reuse across retries. The fallback
22
+ * generation below exists only for direct, one-shot executor consumers.
22
23
  */
23
24
  export declare function createDefaultMutationExecutor(getWs: () => {
24
25
  sendCommit?: (operations: readonly MutationOperation[], clientTxId: string, timeoutMs?: number, causedByTaskId?: string | null, reads?: readonly ReadDependency[] | null) => Promise<{
@@ -17,8 +17,9 @@ import { AbloError, AbloConnectionError } from '../errors.js';
17
17
  * the ready socket.
18
18
  *
19
19
  * When set, `options.idempotencyKey` is sent as the wire-level `clientTxId`, so
20
- * retrying a call with the same key is safe; otherwise the executor generates
21
- * one.
20
+ * retrying a call with the same key is safe. TransactionQueue always supplies
21
+ * this key before its first attempt and owns reuse across retries. The fallback
22
+ * generation below exists only for direct, one-shot executor consumers.
22
23
  */
23
24
  export function createDefaultMutationExecutor(getWs) {
24
25
  async function commit(operations, options) {
@@ -157,6 +157,9 @@ export type StaleNotification = z.infer<typeof staleNotificationSchema>;
157
157
  * • Group — `{ group, readAt }`: did anything in this sync group change?
158
158
  * `group` is a sync-group key such as `deck:abc` or `slide:s1`, the
159
159
  * same unit a participant watches and claims.
160
+ *
161
+ * See `packages/sync-engine/docs/concurrency-convention.md` (§4) for the
162
+ * governing convention and the receive → reconcile loop.
160
163
  */
161
164
  export declare const readDependencySchema: z.ZodUnion<readonly [z.ZodObject<{
162
165
  model: z.ZodString;
@@ -304,6 +307,33 @@ export declare const claimRejectionSchema: z.ZodObject<{
304
307
  policyReason: z.ZodOptional<z.ZodString>;
305
308
  }, z.core.$strip>;
306
309
  export type ClaimRejection = z.infer<typeof claimRejectionSchema>;
310
+ /**
311
+ * The point-to-point notification sent to a holder whose lease ended without
312
+ * a successful commit. This remains a wire-shaped target because it arrives
313
+ * directly from the WebSocket; the schema is the single validation boundary
314
+ * before the event reaches public `claims.onLost` listeners.
315
+ */
316
+ export declare const claimLostSchema: z.ZodObject<{
317
+ claimId: z.ZodString;
318
+ reason: z.ZodEnum<{
319
+ expired: "expired";
320
+ preempted: "preempted";
321
+ }>;
322
+ target: z.ZodObject<{
323
+ entityType: z.ZodString;
324
+ entityId: z.ZodString;
325
+ path: z.ZodOptional<z.ZodString>;
326
+ range: z.ZodOptional<z.ZodObject<{
327
+ startLine: z.ZodNumber;
328
+ endLine: z.ZodNumber;
329
+ startColumn: z.ZodOptional<z.ZodNumber>;
330
+ endColumn: z.ZodOptional<z.ZodNumber>;
331
+ }, z.core.$strip>>;
332
+ field: z.ZodOptional<z.ZodString>;
333
+ meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
334
+ }, z.core.$strip>;
335
+ }, z.core.$strip>;
336
+ export type ClaimLost = z.infer<typeof claimLostSchema>;
307
337
  /**
308
338
  * What a {@link ModelClaim} points at — the target locator as SDK callers see
309
339
  * it, keyed by `model` and `id` rather than the wire schema's `entityType` and
@@ -165,6 +165,9 @@ export const staleNotificationSchema = z.object({
165
165
  * • Group — `{ group, readAt }`: did anything in this sync group change?
166
166
  * `group` is a sync-group key such as `deck:abc` or `slide:s1`, the
167
167
  * same unit a participant watches and claims.
168
+ *
169
+ * See `packages/sync-engine/docs/concurrency-convention.md` (§4) for the
170
+ * governing convention and the receive → reconcile loop.
168
171
  */
169
172
  export const readDependencySchema = z.union([
170
173
  z.object({
@@ -249,6 +252,17 @@ export const claimRejectionSchema = z.object({
249
252
  heldByClaim: wireClaimSummarySchema.optional(),
250
253
  policyReason: z.string().optional(),
251
254
  });
255
+ /**
256
+ * The point-to-point notification sent to a holder whose lease ended without
257
+ * a successful commit. This remains a wire-shaped target because it arrives
258
+ * directly from the WebSocket; the schema is the single validation boundary
259
+ * before the event reaches public `claims.onLost` listeners.
260
+ */
261
+ export const claimLostSchema = z.object({
262
+ claimId: z.string(),
263
+ reason: z.enum(['expired', 'preempted']),
264
+ target: targetRefSchema,
265
+ });
252
266
  /**
253
267
  * What a {@link ModelClaim} points at — the target locator as SDK callers see
254
268
  * it, keyed by `model` and `id` rather than the wire schema's `entityType` and
@@ -21,6 +21,8 @@ import { LoadStrategy } from '../types/index.js';
21
21
  */
22
22
  export declare class StoreManager {
23
23
  private stores;
24
+ /** Strict-durability wrapper for the client write journal and commit outbox. */
25
+ private transactionStore;
24
26
  private syncactionStore;
25
27
  private db;
26
28
  private isInitialized;
@@ -22,6 +22,8 @@ import { AbloValidationError } from '../errors.js';
22
22
  */
23
23
  export class StoreManager {
24
24
  stores = new Map();
25
+ /** Strict-durability wrapper for the client write journal and commit outbox. */
26
+ transactionStore = null;
25
27
  syncactionStore = null;
26
28
  db = null;
27
29
  isInitialized = false;
@@ -46,6 +48,12 @@ export class StoreManager {
46
48
  for (const modelName of allModels) {
47
49
  await this.createStoreForModel(modelName);
48
50
  }
51
+ // Special stores are created during the IDB upgrade, but unlike model
52
+ // stores they have no registry metadata and therefore need an explicit
53
+ // runtime wrapper. Outbox acknowledgement must mean disk-backed, so this
54
+ // store opts into strict durability rather than the cache stores' relaxed
55
+ // mode.
56
+ this.transactionStore = new ObjectStore(this.db, '__transactions', '__transactions', { loadStrategy: LoadStrategy.instant }, 'strict');
49
57
  // Initialize SyncactionStore
50
58
  this.syncactionStore = new SyncActionStore(this.db);
51
59
  await this.syncactionStore.initialize();
@@ -142,6 +150,9 @@ export class StoreManager {
142
150
  * Get ObjectStore for a model
143
151
  */
144
152
  getStore(modelName) {
153
+ if (modelName === '__transactions') {
154
+ return this.transactionStore ?? undefined;
155
+ }
145
156
  return this.stores.get(modelName);
146
157
  }
147
158
  /**
@@ -295,6 +306,7 @@ export class StoreManager {
295
306
  for (const store of this.stores.values()) {
296
307
  store.markAsClosing();
297
308
  }
309
+ this.transactionStore?.markAsClosing();
298
310
  // SyncActionStore is a standalone store that does not extend ObjectStore
299
311
  // and has no markAsClosing equivalent, so it is intentionally skipped here.
300
312
  // If closing-state coordination is ever needed for sync actions, add it
@@ -21,7 +21,7 @@ export { Model } from '../Model.js';
21
21
  export { LazyReferenceCollection, type LazyCollectionOptions, } from '../LazyReferenceCollection.js';
22
22
  export { ModelRegistry, getActiveRegistry, } from '../ModelRegistry.js';
23
23
  export { postQuery, type PostQueryOptions } from '../query/client.js';
24
- export { computeFKDepthPriority, type InternalAbloOptions } from '../client/Ablo.js';
24
+ export { computeFKDepthPriority } from '../client/Ablo.js';
25
25
  export type { SyncLogger, SyncObservabilityProvider, MutationExecutor, SessionErrorDetector, OnlineStatusProvider, CommitResult, MutationOperation, } from '../interfaces/index.js';
26
26
  export { SyncWebSocket, type SyncDelta, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
27
27
  export { BootstrapFetcher } from '../sync/BootstrapFetcher.js';
@@ -112,7 +112,7 @@ export const ERROR_CODES = {
112
112
  jwt_invalid: wire('auth', 401, false, "The bearer JWT failed validation for a reason the server could not classify further. Check the token's issuer, signature, audience, and expiry."),
113
113
  jwt_malformed: wire('auth', 401, false, 'The bearer token is not a well-formed JWT and could not be decoded. Check that the full, unmodified token was sent.'),
114
114
  jwt_missing_issuer: wire('auth', 401, false, 'The bearer JWT has no `iss` (issuer) claim, so it cannot be routed to a trusted issuer.'),
115
- jwt_issuer_untrusted: wire('auth', 401, false, "The bearer JWT's `iss` is not a registered trusted issuer. Register it via POST /v1/trusted-issuers, or check the token's issuer claim."),
115
+ jwt_issuer_untrusted: wire('auth', 401, false, "The bearer JWT's `iss` is not a registered trusted issuer. Check the token's issuer claim, or register the issuer with your deployment before retrying."),
116
116
  jwt_signature_invalid: wire('auth', 401, false, "The bearer JWT's signature could not be verified against the issuer's JWKS (wrong key, rotated key, or forged token)."),
117
117
  jwt_audience_mismatch: wire('auth', 401, false, "The bearer JWT's `aud` (audience) claim does not match the audience this issuer is registered with."),
118
118
  jwt_missing_subject: wire('auth', 401, false, 'The bearer JWT has no `sub` (subject) claim to identify the user.'),
@@ -161,7 +161,11 @@ export const ERROR_CODES = {
161
161
  model_claim_not_configured: client('claim', 'Claiming requires the collaboration runtime, which the standard Ablo({ schema, apiKey }) client wires up for every model automatically — there is no per-model claim configuration to add. This appears only when a model proxy is constructed directly without that runtime (an internal/advanced path).'),
162
162
  model_watch_not_configured: client('claim', 'watch() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport).'),
163
163
  // ── stale context / idempotency (409) ──────────────────────────────
164
- stale_context: wire('conflict', 409, true, "The row changed after you read it — the write's `readAt` watermark is older than the current row version. Re-read the row and retry."),
164
+ // Not retryable at the transport: the rejected request carries its frozen
165
+ // `readAt`, so resending the identical payload can never succeed. Recovery
166
+ // is a caller-level re-read that produces a NEW request with a fresh
167
+ // watermark — the same shape as `claim_conflict`.
168
+ stale_context: wire('conflict', 409, false, "The row changed after you read it — the write's `readAt` watermark is older than the current row version. Re-read the row and retry."),
165
169
  // Raised by the functional `update(id, current => next)` form once its
166
170
  // internal reconcile budget is exhausted, because the row stayed continuously
167
171
  // contended. The SDK has already retried; the caller decides whether to back
package/dist/errors.d.ts CHANGED
@@ -131,10 +131,10 @@ export declare class AbloValidationError extends AbloError {
131
131
  }
132
132
  /**
133
133
  * An update or delete addressed a row that does not exist, or lies outside the
134
- * caller's organization (HTTP 404). Such targets are reported on
135
- * {@link CommitReceipt.missingIds}, and the typed resource methods raise this
136
- * error rather than returning a successful receipt for a write that quietly
137
- * matched zero rows. The absent ids are carried on {@link missingIds}.
134
+ * caller's organization (HTTP 404). Raw commit responses may report those
135
+ * targets as `missingIds`; typed model methods raise this error instead of
136
+ * returning a successful result for a write that quietly matched zero rows.
137
+ * The absent ids are carried on {@link missingIds}.
138
138
  */
139
139
  export declare class AbloNotFoundError extends AbloError {
140
140
  readonly type: "AbloNotFoundError";
@@ -292,42 +292,6 @@ export interface RequiredCapability {
292
292
  * capability. */
293
293
  readonly nonce?: string;
294
294
  }
295
- /**
296
- * The receipt returned for every commit, whether it succeeded or was rejected,
297
- * and regardless of how it was sent — the WebSocket `mutation_result` payload,
298
- * the HTTP `/v1/commits` response body, and an agent job's result all use this
299
- * one shape. It is a correlation receipt, carrying the ids and outcome needed to
300
- * reconcile a commit rather than a cryptographic proof.
301
- */
302
- export interface CommitReceipt {
303
- readonly object: 'commit_receipt';
304
- /** Client-issued idempotency handle. Echoed verbatim. */
305
- readonly clientTxId: string;
306
- /** Server-issued opaque commit id — typically `String(lastSyncId)`. */
307
- readonly serverTxId: string;
308
- /** A convenience boolean, equal to `status === 'confirmed'`. */
309
- readonly success: boolean;
310
- /** `'confirmed'` on apply, `'rejected'` on any failure. */
311
- readonly status: 'confirmed' | 'rejected';
312
- /** Last syncId visible after this commit (or high-water mark on
313
- * rejection). */
314
- readonly lastSyncId?: number;
315
- /** Number of operations metered. Reported on both success and
316
- * rejection so quota systems see attempted work. */
317
- readonly ops?: number;
318
- /** Ids of update or delete targets that matched no row. Present and non-empty
319
- * only when a write missed; the typed resource methods raise
320
- * {@link AbloNotFoundError} from it. */
321
- readonly missingIds?: readonly string[];
322
- /** Present on rejection. When set, `requiredCapability` carries the structured
323
- * hint describing what would let the request succeed on retry. */
324
- readonly error?: {
325
- readonly code: string;
326
- readonly message: string;
327
- readonly field?: string;
328
- readonly requiredCapability?: RequiredCapability;
329
- };
330
- }
331
295
  /**
332
296
  * A scoped credential was denied, either because the key is unknown, revoked, or
333
297
  * expired (`capability_invalid`), or because the connection's scope does not
package/dist/errors.js CHANGED
@@ -150,10 +150,10 @@ export class AbloValidationError extends AbloError {
150
150
  }
151
151
  /**
152
152
  * An update or delete addressed a row that does not exist, or lies outside the
153
- * caller's organization (HTTP 404). Such targets are reported on
154
- * {@link CommitReceipt.missingIds}, and the typed resource methods raise this
155
- * error rather than returning a successful receipt for a write that quietly
156
- * matched zero rows. The absent ids are carried on {@link missingIds}.
153
+ * caller's organization (HTTP 404). Raw commit responses may report those
154
+ * targets as `missingIds`; typed model methods raise this error instead of
155
+ * returning a successful result for a write that quietly matched zero rows.
156
+ * The absent ids are carried on {@link missingIds}.
157
157
  */
158
158
  export class AbloNotFoundError extends AbloError {
159
159
  type = 'AbloNotFoundError';
@@ -442,7 +442,7 @@ const ErrorFieldSchema = z
442
442
  const ErrorBodyShapeSchema = z
443
443
  .object({
444
444
  /** The `error` field may be a flat code string, as some endpoints return,
445
- * or a nested error object, as a {@link CommitReceipt} carries. */
445
+ * or a nested error object, as commit endpoints return on rejection. */
446
446
  error: ErrorFieldSchema,
447
447
  code: OptionalWireStringSchema,
448
448
  reason: OptionalWireStringSchema,