@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
|
@@ -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()`
|
package/dist/client/options.d.ts
CHANGED
|
@@ -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}
|
|
4
|
-
* surface
|
|
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
|
-
*
|
|
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
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* only
|
|
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
|
/**
|
package/dist/client/options.js
CHANGED
|
@@ -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}
|
|
4
|
-
* surface
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
42
|
-
* `
|
|
43
|
-
*
|
|
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
|
|
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
|
-
|
|
101
|
-
readonly operations
|
|
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
|
-
*
|
|
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.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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`.
|
|
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
|
|
4
|
-
*
|
|
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.
|
|
64
|
-
//
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
21
|
-
*
|
|
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
|
|
21
|
-
*
|
|
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
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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
|
|
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';
|
package/dist/errorCodes.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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).
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
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).
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
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
|
|
445
|
+
* or a nested error object, as commit endpoints return on rejection. */
|
|
446
446
|
error: ErrorFieldSchema,
|
|
447
447
|
code: OptionalWireStringSchema,
|
|
448
448
|
reason: OptionalWireStringSchema,
|