@abloatai/ablo 0.5.0 → 0.6.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 (94) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +242 -135
  3. package/dist/BaseSyncedStore.d.ts +2 -2
  4. package/dist/BaseSyncedStore.js +2 -2
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +90 -93
  8. package/dist/client/Ablo.js +121 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +90 -87
  14. package/dist/client/createModelProxy.js +124 -127
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +3 -3
  18. package/dist/core/index.d.ts +2 -0
  19. package/dist/core/index.js +7 -0
  20. package/dist/errors.d.ts +8 -8
  21. package/dist/errors.js +18 -10
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +7 -11
  24. package/dist/interfaces/index.d.ts +2 -10
  25. package/dist/mutators/Transaction.d.ts +2 -2
  26. package/dist/mutators/Transaction.js +2 -2
  27. package/dist/mutators/mutateActions.d.ts +44 -0
  28. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  29. package/dist/mutators/readerActions.d.ts +32 -0
  30. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  31. package/dist/query/types.d.ts +1 -1
  32. package/dist/react/AbloProvider.d.ts +1 -1
  33. package/dist/react/AbloProvider.js +3 -3
  34. package/dist/react/context.d.ts +4 -4
  35. package/dist/react/index.d.ts +4 -5
  36. package/dist/react/index.js +3 -7
  37. package/dist/react/useAblo.d.ts +14 -14
  38. package/dist/react/useAblo.js +26 -26
  39. package/dist/react/useIntent.d.ts +2 -2
  40. package/dist/react/useIntent.js +2 -2
  41. package/dist/react/useMutators.d.ts +1 -1
  42. package/dist/react/usePresence.d.ts +3 -3
  43. package/dist/react/usePresence.js +4 -4
  44. package/dist/react/useUndoScope.d.ts +1 -1
  45. package/dist/schema/diff.d.ts +161 -0
  46. package/dist/schema/diff.js +262 -0
  47. package/dist/schema/generate.d.ts +19 -0
  48. package/dist/schema/generate.js +87 -0
  49. package/dist/schema/index.d.ts +4 -1
  50. package/dist/schema/index.js +7 -1
  51. package/dist/schema/schema.d.ts +83 -32
  52. package/dist/schema/schema.js +58 -12
  53. package/dist/schema/serialize.d.ts +92 -0
  54. package/dist/schema/serialize.js +227 -0
  55. package/dist/sync/SyncWebSocket.d.ts +17 -0
  56. package/dist/sync/SyncWebSocket.js +46 -1
  57. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  58. package/dist/sync/awaitIntentGrant.js +60 -0
  59. package/dist/sync/createIntentStream.js +43 -4
  60. package/dist/sync/createPresenceStream.js +1 -1
  61. package/dist/sync/participants.d.ts +2 -2
  62. package/dist/sync/participants.js +4 -4
  63. package/dist/types/global.d.ts +43 -52
  64. package/dist/types/global.js +16 -18
  65. package/dist/types/streams.d.ts +37 -9
  66. package/docs/api.md +68 -158
  67. package/docs/audit.md +5 -5
  68. package/docs/client-behavior.md +41 -42
  69. package/docs/coordination.md +294 -0
  70. package/docs/data-sources.md +14 -14
  71. package/docs/examples/agent-human.md +30 -32
  72. package/docs/examples/ai-sdk-tool.md +32 -33
  73. package/docs/examples/existing-python-backend.md +35 -33
  74. package/docs/examples/nextjs.md +24 -25
  75. package/docs/examples/server-agent.md +20 -61
  76. package/docs/guarantees.md +30 -55
  77. package/docs/identity.md +458 -0
  78. package/docs/index.md +12 -24
  79. package/docs/integration-guide.md +106 -116
  80. package/docs/interaction-model.md +29 -95
  81. package/docs/mcp/claude-code.md +3 -3
  82. package/docs/mcp/cursor.md +1 -1
  83. package/docs/mcp/windsurf.md +1 -1
  84. package/docs/mcp.md +11 -26
  85. package/docs/quickstart.md +43 -49
  86. package/docs/react.md +73 -23
  87. package/docs/roadmap.md +5 -7
  88. package/llms.txt +34 -39
  89. package/package.json +1 -1
  90. package/dist/react/useMutate.d.ts +0 -83
  91. package/dist/react/useQuery.d.ts +0 -123
  92. package/dist/react/useQuery.js +0 -145
  93. package/dist/react/useReader.d.ts +0 -69
  94. package/docs/capabilities.md +0 -163
@@ -1,26 +1,26 @@
1
1
  /**
2
- * Per-model resource factory.
2
+ * Per-model client factory.
3
3
  *
4
- * Mirrors Anthropic SDK's `resources/messages.ts` / `resources/models.ts`
5
- * pattern: each resource has its own file, the client just instantiates
4
+ * Mirrors Anthropic SDK's per-endpoint module pattern: each model client
5
+ * has its own file, and the root client just instantiates
6
6
  * one per model. Extracted from `Ablo.ts` so the proxy logic is
7
7
  * testable in isolation and the constructor doesn't carry it.
8
8
  *
9
9
  * Each schema model gets one `ModelOperations<T, CreateInput>` —
10
10
  * exposes `retrieve`, `list`, `count`, `create`, `update`, `delete`,
11
- * `intent` (the coordination handle), `subscribe`, and `load`. The
12
- * factory returns a plain object; the client assembles the
11
+ * `claim`, `claimState`, `queue`, `release`, `subscribe`, and `load`.
12
+ * The factory returns a plain object; the client assembles the
13
13
  * `ablo.<model>` lookup table from these.
14
14
  */
15
15
  import { autorun } from 'mobx';
16
- import { AbloStaleContextError, AbloValidationError } from '../errors.js';
16
+ import { AbloClaimedError, AbloValidationError } from '../errors.js';
17
17
  import { Model, modelAsRow } from '../Model.js';
18
18
  import { ModelScope } from '../types/index.js';
19
- const modelResourceMeta = new WeakMap();
20
- export function getModelResourceMeta(resource) {
21
- if (typeof resource !== 'object' || resource === null)
19
+ const modelClientMeta = new WeakMap();
20
+ export function getModelClientMeta(modelClient) {
21
+ if (typeof modelClient !== 'object' || modelClient === null)
22
22
  return undefined;
23
- return modelResourceMeta.get(resource);
23
+ return modelClientMeta.get(modelClient);
24
24
  }
25
25
  export function createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, registry, hydration, collaboration) {
26
26
  const ModelClass = registry.getModelByName(registeredModelName);
@@ -42,6 +42,93 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
42
42
  await syncClient.syncNow();
43
43
  await syncClient.waitForConfirmation(model.getModelName(), model.id);
44
44
  };
45
+ // Claims this proxy currently holds, keyed by entity id. Lets the flat
46
+ // `release(id)` and `update(id)` find the lease + snapshot a `claim(id)`
47
+ // took — no per-call handle. Released on dispose, explicit release, or TTL.
48
+ const activeClaims = new Map();
49
+ const releaseClaim = async (id) => {
50
+ const held = activeClaims.get(id);
51
+ if (!held)
52
+ return;
53
+ activeClaims.delete(id);
54
+ await held.lease.release();
55
+ };
56
+ const takeClaim = async (id, options) => {
57
+ if (!collaboration) {
58
+ throw new AbloValidationError(`Model "${schemaKey}" cannot claim a row without collaboration wiring.`, { code: 'model_claim_not_configured' });
59
+ }
60
+ // Is someone ELSE already on this target? Read the local coordination
61
+ // snapshot up front — it decides whether we'll need to re-read after the
62
+ // claim (a free / already-mine target can't have changed under us).
63
+ const held = collaboration.observe({ model: schemaKey, id });
64
+ const contended = !!held && held.heldBy !== collaboration.selfParticipantId;
65
+ const failFast = options?.wait === false;
66
+ // Fail-fast (`wait: false`): if another participant already holds it,
67
+ // reject now instead of queuing. Best-effort at the client (a racing
68
+ // claim not yet synced into our snapshot slips through here) — the
69
+ // commit-time intent guard is the authoritative backstop that rejects
70
+ // the loser's first write. For work-distribution dedup that's exactly
71
+ // right: don't wait (that would double-process), skip.
72
+ if (failFast && contended) {
73
+ throw new AbloClaimedError(`${registeredModelName}/${id} is held by ${held?.heldBy ?? 'another participant'}.`, { code: 'entity_claimed' });
74
+ }
75
+ // Ensure the row exists locally before claiming.
76
+ let model = objectPool.get(id);
77
+ if (!model) {
78
+ await load({ where: { id } });
79
+ model = objectPool.get(id);
80
+ }
81
+ if (!model) {
82
+ throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
83
+ }
84
+ // Acquire the lease. Default (`wait` !== false) goes through the server's
85
+ // fair FIFO queue — `queue: true` resolves only once the lease is genuinely
86
+ // ours, blocking behind any current holder, with no TOCTOU gap (the server
87
+ // orders contenders). Fail-fast skips the queue: we already rejected an
88
+ // observed conflict above, so this just records our lease.
89
+ const lease = await collaboration.createIntent({
90
+ target: {
91
+ model: schemaKey,
92
+ id,
93
+ ...(options?.field ? { field: options.field } : {}),
94
+ },
95
+ action: options?.action ?? 'editing',
96
+ ttl: options?.ttl,
97
+ queue: !failFast,
98
+ maxQueueDepth: options?.maxQueueDepth,
99
+ });
100
+ // Only when we actually waited behind another holder can the row have
101
+ // changed underneath us — re-read so the claimed snapshot reflects what
102
+ // they committed before releasing.
103
+ if (contended && !failFast) {
104
+ await load({ where: { id } });
105
+ model = objectPool.get(id) ?? model;
106
+ }
107
+ const snapshot = collaboration.createSnapshot(schemaKey, id);
108
+ activeClaims.set(id, { lease, snapshot });
109
+ const row = modelAsRow(model);
110
+ // `await using` calls this on scope exit; releases the claim.
111
+ Object.defineProperty(row, Symbol.asyncDispose, {
112
+ value: () => releaseClaim(id),
113
+ enumerable: false,
114
+ configurable: true,
115
+ });
116
+ return row;
117
+ };
118
+ function claim(id, a, b) {
119
+ if (typeof a === 'function') {
120
+ return (async () => {
121
+ const row = await takeClaim(id, b);
122
+ try {
123
+ return await a(row);
124
+ }
125
+ finally {
126
+ await releaseClaim(id);
127
+ }
128
+ })();
129
+ }
130
+ return takeClaim(id, a);
131
+ }
45
132
  const operations = {
46
133
  retrieve(id) {
47
134
  return objectPool.get(id);
@@ -99,9 +186,21 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
99
186
  const model = objectPool.get(id);
100
187
  if (!model)
101
188
  throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
189
+ // If we hold a claim on this row, guard the write with its snapshot
190
+ // watermark + lease so it's stale-rejected and attributed to the claim.
191
+ const claimed = activeClaims.get(id);
192
+ const effective = claimed
193
+ ? {
194
+ wait: 'confirmed',
195
+ readAt: claimed.snapshot.stamp,
196
+ onStale: 'reject',
197
+ intent: claimed.lease,
198
+ ...options,
199
+ }
200
+ : options;
102
201
  model.updateFromData(data);
103
- syncClient.update(model, options);
104
- await waitForMutation(model, options);
202
+ syncClient.update(model, effective);
203
+ await waitForMutation(model, effective);
105
204
  return modelAsRow(model);
106
205
  },
107
206
  async delete(id, options) {
@@ -111,122 +210,20 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
111
210
  syncClient.delete(model, options);
112
211
  await waitForMutation(model, options);
113
212
  },
114
- intent(id) {
115
- const target = { resource: schemaKey, id };
116
- let acquired = null;
117
- let snapshot = null;
118
- let released = false;
119
- let acquireWait;
120
- // Public `cancel()` — drop the claim without committing. Calls the
121
- // lower-level lease handle's `revoke()` (a different API; leave it).
122
- const cancel = () => {
123
- if (released)
124
- return;
125
- released = true;
126
- if (snapshot)
127
- snapshot.signal.removeEventListener('abort', cancel);
128
- acquired?.revoke();
129
- };
130
- // Public `finish()` — give the claim back after committing. Calls the
131
- // lower-level lease handle's `release()` (a different API; leave it).
132
- const finish = async () => {
133
- if (released)
134
- return;
135
- released = true;
136
- if (snapshot)
137
- snapshot.signal.removeEventListener('abort', cancel);
138
- await acquired?.release();
139
- };
140
- const whenFree = async (options) => {
141
- if (!collaboration)
142
- return;
143
- await collaboration.waitFor(target, options);
144
- };
145
- const claim = async (options) => {
146
- if (!collaboration) {
147
- throw new AbloValidationError(`Model "${schemaKey}" cannot claim an intent without collaboration wiring.`, { code: 'model_intent_not_configured' });
148
- }
149
- if (acquired)
150
- return;
151
- acquireWait = options?.wait;
152
- // Load the row so update() has a snapshot to guard against.
153
- let model = objectPool.get(id);
154
- if (!model) {
155
- await load({ where: { id } });
156
- model = objectPool.get(id);
157
- }
158
- if (!model) {
159
- throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
160
- }
161
- const snap = collaboration.createSnapshot(schemaKey, id);
162
- snap.signal.addEventListener('abort', cancel, { once: true });
163
- snapshot = snap;
164
- released = false;
165
- acquired = await collaboration.createIntent({
166
- target: {
167
- resource: schemaKey,
168
- id,
169
- ...(options?.field ? { field: options.field } : {}),
170
- },
171
- action: options?.action ?? 'editing',
172
- ttl: options?.ttl,
173
- });
174
- };
175
- const claimOrWait = async (options) => {
176
- if (!collaboration) {
177
- throw new AbloValidationError(`Model "${schemaKey}" cannot claim an intent without collaboration wiring.`, { code: 'model_intent_not_configured' });
178
- }
179
- const held = collaboration.observe(target);
180
- // A foreign holder: wait for them to finish, then re-read before
181
- // claiming. Our own claim (or a free target) goes straight to claim.
182
- if (held && held.heldBy !== collaboration.selfParticipantId) {
183
- await whenFree();
184
- await load({ where: { id } });
185
- }
186
- await claim(options);
187
- };
188
- const handle = {
189
- id,
190
- get current() {
191
- return collaboration?.observe(target) ?? null;
192
- },
193
- get status() {
194
- return collaboration?.observe(target)?.status ?? 'idle';
195
- },
196
- claim,
197
- claimOrWait,
198
- async update(data, updateOptions) {
199
- if (!acquired || !snapshot) {
200
- throw new AbloValidationError(`Call claim() before update() on ablo.${schemaKey}.intent(${id}).`, { code: 'intent_not_acquired' });
201
- }
202
- if (snapshot.signal.aborted) {
203
- throw new AbloStaleContextError(`Intent context is stale for ${schemaKey}/${id}. Re-read the row and retry.`, {
204
- code: 'edit_context_stale',
205
- readAt: snapshot.stamp,
206
- cause: snapshot.signal.reason,
207
- });
208
- }
209
- try {
210
- return await operations.update(id, data, {
211
- wait: acquireWait ?? 'confirmed',
212
- readAt: snapshot.stamp,
213
- onStale: 'reject',
214
- ...updateOptions,
215
- intent: acquired,
216
- });
217
- }
218
- finally {
219
- await finish();
220
- }
221
- },
222
- finish,
223
- whenFree,
224
- cancel,
225
- [Symbol.asyncDispose]: finish,
213
+ claim,
214
+ claimState(id) {
215
+ return collaboration?.observe({ model: schemaKey, id }) ?? null;
216
+ },
217
+ queue(id) {
218
+ return {
219
+ object: 'list',
220
+ data: collaboration?.queue({ model: schemaKey, id }) ?? [],
226
221
  };
227
- return handle;
228
222
  },
229
- subscribe(callback, options) {
223
+ release(id) {
224
+ return releaseClaim(id);
225
+ },
226
+ onChange(callback, options) {
230
227
  return autorun(() => {
231
228
  const entities = this.list(options);
232
229
  callback(entities);
@@ -234,7 +231,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
234
231
  },
235
232
  load,
236
233
  };
237
- modelResourceMeta.set(operations, {
234
+ modelClientMeta.set(operations, {
238
235
  key: schemaKey,
239
236
  typename: registeredModelName,
240
237
  });
@@ -15,22 +15,21 @@
15
15
  * apiKey: process.env.ABLO_API_KEY,
16
16
  * });
17
17
  *
18
- * const tasks = ablo.tasks.list({ where: { status: 'todo' } });
19
- * await ablo.tasks.create({ title: 'New task' });
18
+ * const reports = ablo.weatherReports.list({ where: { status: 'pending' } });
19
+ * await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
20
20
  * ```
21
21
  *
22
- * For headless agents (workers, bots), pass `kind: 'agent'` plus a
23
- * restricted (`rk_`) API key as `capabilityToken`:
22
+ * For headless agents (workers, bots), pass the same schema and an API key
23
+ * scoped for that server runtime:
24
24
  *
25
25
  * ```ts
26
26
  * const bot = Ablo({
27
27
  * schema,
28
28
  * apiKey: process.env.ABLO_API_KEY,
29
- * kind: 'agent',
30
29
  * });
31
30
  * ```
32
31
  */
33
- export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type BusyOptions, type BusyPolicy, type IntentWaitOptions, type ModelCountOptions, type ModelListOptions, type ModelListScope, type ModelLoadOptions, type ModelOperations, type ResourceReadOptions, } from './Ablo.js';
32
+ export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type ClaimedOptions, type IfClaimedPolicy, type IntentWaitOptions, type ModelCountOptions, type ModelListOptions, type ModelListScope, type ModelLoadOptions, type ModelOperations, type ModelReadOptions, } from './Ablo.js';
34
33
  export type { AbloPersistence } from './persistence.js';
35
- export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions, AgentResourceClient, AgentResourceReadOptions, AgentResourceMutationOptions, AgentRunContext, AgentRunDone, AgentRunFailed, AgentRunCancelled, AgentRunOptions, AgentRunResult, AgentRunStatus, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, Task, TaskCloseOptions, TaskCloseResult, TaskCreateOptions, TaskResource, } from './ApiClient.js';
34
+ export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions, AgentModelClient, AgentModelReadOptions, AgentModelMutationOptions, AgentRunContext, AgentRunDone, AgentRunFailed, AgentRunCancelled, AgentRunOptions, AgentRunResult, AgentRunStatus, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, Task, TaskCloseOptions, TaskCloseResult, TaskCreateOptions, TaskResource, } from './ApiClient.js';
36
35
  export type { EngineParticipant, JoinedParticipant, ParticipantJoinOptions, ParticipantManager, ParticipantScope, ParticipantStatus, ScopedIntents, ScopedPresence, } from '../sync/participants.js';
@@ -15,18 +15,17 @@
15
15
  * apiKey: process.env.ABLO_API_KEY,
16
16
  * });
17
17
  *
18
- * const tasks = ablo.tasks.list({ where: { status: 'todo' } });
19
- * await ablo.tasks.create({ title: 'New task' });
18
+ * const reports = ablo.weatherReports.list({ where: { status: 'pending' } });
19
+ * await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
20
20
  * ```
21
21
  *
22
- * For headless agents (workers, bots), pass `kind: 'agent'` plus a
23
- * restricted (`rk_`) API key as `capabilityToken`:
22
+ * For headless agents (workers, bots), pass the same schema and an API key
23
+ * scoped for that server runtime:
24
24
  *
25
25
  * ```ts
26
26
  * const bot = Ablo({
27
27
  * schema,
28
28
  * apiKey: process.env.ABLO_API_KEY,
29
- * kind: 'agent',
30
29
  * });
31
30
  * ```
32
31
  */
@@ -17,9 +17,9 @@ export function validateAbloOptions(input) {
17
17
  return new Error('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
18
18
  `Ablo({ baseURL: 'wss://sync.ablo.dev', schema, user })`);
19
19
  }
20
- // Schema is optional for the resource-first API:
21
- // Ablo({ apiKey }).resource('clauses').retrieve(...)
22
- // Passing a schema only enables typed model sugar (`ablo.tasks.update(...)`).
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(...)`).
23
23
  if (!configuredApiKey &&
24
24
  !configuredAuthToken &&
25
25
  !options.capabilityToken &&
@@ -28,6 +28,8 @@ export { ObjectStore } from '../stores/ObjectStore.js';
28
28
  export { NetworkMonitor } from '../NetworkMonitor.js';
29
29
  export { SyncWebSocket, type SyncDelta, type VersionVector, type BootstrapHint, type SyncGroupChangePayload, type BootstrapDataEvent, type PresenceUpdateEvent, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
30
30
  export { BootstrapHelper, type BootstrapData, type BootstrapOptions, type BootstrapFetchResult } from '../sync/BootstrapHelper.js';
31
+ export { createIntentStream, type AttachableIntentStream, type IntentStreamConfig, } from '../sync/createIntentStream.js';
32
+ export { awaitIntentGrant, type GrantTransport, } from '../sync/awaitIntentGrant.js';
31
33
  export { OfflineTransactionStore, offlineTxStore, Priority } from '../sync/OfflineTransactionStore.js';
32
34
  export { PropertyType, LoadStrategy, MutationOperationType } from '../types/index.js';
33
35
  export type { PropertyMetadata, ReferenceMetadata, ModelMetadata, SyncAction, DeltaPacket, BootstrapMetadata, DatabaseMetadata, } from '../types/index.js';
@@ -49,6 +49,13 @@ export { NetworkMonitor } from '../NetworkMonitor.js';
49
49
  // Sync layer
50
50
  export { SyncWebSocket, } from '../sync/SyncWebSocket.js';
51
51
  export { BootstrapHelper } from '../sync/BootstrapHelper.js';
52
+ // Intent coordination primitives (the lower-level pieces behind the
53
+ // consumer-facing `ablo.<model>.claim`). The stream factory builds the
54
+ // announce/await machinery on a SyncWebSocket; `awaitIntentGrant` is the
55
+ // fair-queue grant coordinator. Exposed on /core for framework-level
56
+ // orchestration and e2e harnesses — NOT on the consumer `.` root.
57
+ export { createIntentStream, } from '../sync/createIntentStream.js';
58
+ export { awaitIntentGrant, } from '../sync/awaitIntentGrant.js';
52
59
  // Offline transaction queue — moved out of the main barrel in the headless
53
60
  // audit cleanup (see docs/headless-audit.md §4.1 Task 23). The class
54
61
  // touches indexedDB + crypto.subtle and therefore cannot live on the main
package/dist/errors.d.ts CHANGED
@@ -111,21 +111,21 @@ export declare class AbloStaleContextError extends AbloError {
111
111
  });
112
112
  }
113
113
  /**
114
- * The target entity currently has an active intent held by another
115
- * participant and the caller asked the SDK not to return immediately.
114
+ * The target entity is currently claimed by another participant and the caller
115
+ * asked the SDK not to read/write through that claim.
116
116
  *
117
- * Use `ifBusy: 'wait'` to wait for the intent stream to clear, or
118
- * `ifBusy: 'return'` to inspect the active intents yourself.
117
+ * Use `ifClaimed: 'wait'` to wait for the claim to clear, or
118
+ * `ifClaimed: 'return'` to inspect active claims yourself.
119
119
  */
120
- export declare class AbloBusyError extends AbloError {
121
- readonly type: "AbloBusyError";
122
- readonly intents?: ReadonlyArray<unknown>;
120
+ export declare class AbloClaimedError extends AbloError {
121
+ readonly type: "AbloClaimedError";
122
+ readonly claims?: ReadonlyArray<unknown>;
123
123
  constructor(message: string, options?: {
124
124
  code?: string;
125
125
  httpStatus?: number;
126
126
  requestId?: string;
127
127
  cause?: unknown;
128
- intents?: ReadonlyArray<unknown>;
128
+ claims?: ReadonlyArray<unknown>;
129
129
  });
130
130
  }
131
131
  /**
package/dist/errors.js CHANGED
@@ -109,19 +109,19 @@ export class AbloStaleContextError extends AbloError {
109
109
  }
110
110
  }
111
111
  /**
112
- * The target entity currently has an active intent held by another
113
- * participant and the caller asked the SDK not to return immediately.
112
+ * The target entity is currently claimed by another participant and the caller
113
+ * asked the SDK not to read/write through that claim.
114
114
  *
115
- * Use `ifBusy: 'wait'` to wait for the intent stream to clear, or
116
- * `ifBusy: 'return'` to inspect the active intents yourself.
115
+ * Use `ifClaimed: 'wait'` to wait for the claim to clear, or
116
+ * `ifClaimed: 'return'` to inspect active claims yourself.
117
117
  */
118
- export class AbloBusyError extends AbloError {
119
- type = 'AbloBusyError';
120
- intents;
118
+ export class AbloClaimedError extends AbloError {
119
+ type = 'AbloClaimedError';
120
+ claims;
121
121
  constructor(message, options) {
122
122
  super(message, options);
123
- if (options?.intents !== undefined)
124
- this.intents = options.intents;
123
+ if (options?.claims !== undefined)
124
+ this.claims = options.claims;
125
125
  }
126
126
  }
127
127
  /**
@@ -224,7 +224,8 @@ export function translateHttpError(status, body, requestId) {
224
224
  flatError ??
225
225
  (typeof body === 'string' ? body : `HTTP ${status}`);
226
226
  const requiredCapability = nested?.requiredCapability ?? parsed.requiredCapability;
227
- const baseOpts = { code, httpStatus: status, requestId };
227
+ const publicCode = code === 'intent_conflict' ? 'claim_conflict' : code;
228
+ const baseOpts = { code: publicCode, httpStatus: status, requestId };
228
229
  if (status === 401)
229
230
  return new AbloAuthenticationError(message, baseOpts);
230
231
  if (status === 403 || code === 'capability_scope_denied' || code === 'capability_invalid') {
@@ -233,6 +234,13 @@ export function translateHttpError(status, body, requestId) {
233
234
  }
234
235
  return new AbloPermissionError(message, baseOpts);
235
236
  }
237
+ // Claim enforcement also rides 409 (a commit blocked by a foreign claim).
238
+ // Discriminate on the code BEFORE the generic idempotency mapping so a
239
+ // claim rejection surfaces as AbloClaimedError, not AbloIdempotencyError —
240
+ // same typed error the WebSocket commit path yields for these codes.
241
+ if (code === 'intent_conflict' || code === 'claim_conflict' || code === 'entity_claimed') {
242
+ return new AbloClaimedError(message, baseOpts);
243
+ }
236
244
  if (status === 409)
237
245
  return new AbloIdempotencyError(message, baseOpts);
238
246
  if (status === 422 || status === 400)
package/dist/index.d.ts CHANGED
@@ -5,17 +5,17 @@
5
5
  * import Ablo from '@abloatai/ablo';
6
6
  *
7
7
  * const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
8
- * await ablo.tasks.load({ where: { id: 'task_123' } });
9
- * await ablo.tasks.update('task_123', { title: 'Fix bug' });
8
+ * await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
9
+ * await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
10
10
  *
11
11
  * type Entry = Ablo.Peer;
12
12
  * ```
13
13
  *
14
- * `Ablo({ schema, apiKey })` gives typed model resources. `Ablo({ apiKey })`
15
- * gives the lower-level Resource / Intent / Commit client for agents,
16
- * MCP routes, and custom runtimes.
14
+ * `Ablo({ schema, apiKey })` gives typed model clients. `Ablo({ apiKey })`
15
+ * gives the HTTP model/commit client for agents, MCP routes, and custom
16
+ * runtimes.
17
17
  *
18
- * Stripe / Anthropic / OpenAI all do this: one import, resources
18
+ * Stripe / Anthropic / OpenAI all do this: one import, model clients
19
19
  * reached via dot-access on the engine, types via namespace dots.
20
20
  *
21
21
  * Public subpaths:
@@ -42,14 +42,15 @@
42
42
  * If you don't recognize one, you don't need it — the default path covers you.
43
43
  */
44
44
  export { Ablo } from './client/Ablo.js';
45
- export type { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelIntentHandle, ModelIntentAcquireOptions, ModelOperations, } from './client/Ablo.js';
45
+ export type { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ClaimOptions, ClaimedRow, ModelOperations, } from './client/Ablo.js';
46
46
  export type { AbloPersistence } from './client/persistence.js';
47
47
  export { session, agent } from './principal.js';
48
48
  import { Ablo } from './client/Ablo.js';
49
49
  export default Ablo;
50
50
  export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
51
51
  export { defaultPolicy } from './policy/index.js';
52
- export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloBusyError, CapabilityError, translateHttpError, } from './errors.js';
52
+ export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, } from './errors.js';
53
53
  export type { CommitReceipt, RequiredCapability } from './errors.js';
54
+ export type { Register, DefaultSyncShape } from './types/global.js';
54
55
  export { defineMutators } from './mutators/defineMutators.js';
55
56
  export { createTransaction, type Transaction } from './mutators/Transaction.js';
package/dist/index.js CHANGED
@@ -5,17 +5,17 @@
5
5
  * import Ablo from '@abloatai/ablo';
6
6
  *
7
7
  * const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
8
- * await ablo.tasks.load({ where: { id: 'task_123' } });
9
- * await ablo.tasks.update('task_123', { title: 'Fix bug' });
8
+ * await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
9
+ * await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
10
10
  *
11
11
  * type Entry = Ablo.Peer;
12
12
  * ```
13
13
  *
14
- * `Ablo({ schema, apiKey })` gives typed model resources. `Ablo({ apiKey })`
15
- * gives the lower-level Resource / Intent / Commit client for agents,
16
- * MCP routes, and custom runtimes.
14
+ * `Ablo({ schema, apiKey })` gives typed model clients. `Ablo({ apiKey })`
15
+ * gives the HTTP model/commit client for agents, MCP routes, and custom
16
+ * runtimes.
17
17
  *
18
- * Stripe / Anthropic / OpenAI all do this: one import, resources
18
+ * Stripe / Anthropic / OpenAI all do this: one import, model clients
19
19
  * reached via dot-access on the engine, types via namespace dots.
20
20
  *
21
21
  * Public subpaths:
@@ -78,11 +78,7 @@ export { defaultPolicy } from './policy/index.js';
78
78
  // Typed error hierarchy — Stripe-style. One import gets every class
79
79
  // consumers need to discriminate failures (`e instanceof AbloX` or
80
80
  // `e.type === 'AbloX'`) plus the HTTP-response translator.
81
- export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloBusyError, CapabilityError, translateHttpError, } from './errors.js';
82
- // Typed-global augmentation point. Consumers declare their Schema/Presence/
83
- // Intents/UserMeta once in a `.d.ts` via `declare global { interface AbloSync
84
- // { ... } }`. Resolver types live under the `Ablo` namespace —
85
- // `Ablo.ResolveSchema`, `Ablo.ResolvePresence`, etc. — pure type-level.
81
+ export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, } from './errors.js';
86
82
  // Advanced — most apps never import this. Custom (Zero-style) mutators:
87
83
  // `ablo.<model>.create/update/delete` already covers normal writes. Reach
88
84
  // for `defineMutators` only when you need a named, multi-step mutation with
@@ -151,18 +151,11 @@ export interface CommitResult {
151
151
  * When omitted, the SDK auto-generates a UUIDv4 per mutation so every
152
152
  * call is retry-safe by default. Opt out with `{ idempotencyKey: null }`
153
153
  * if you genuinely want retry-unsafe writes (rare).
154
- * - `timeout` — abort the request if it takes longer than this many ms.
155
- * No default (uses underlying transport's timeout).
156
- * - `maxNetworkRetries` — retry with exponential backoff on 5xx / 429 /
157
- * network errors. The same `idempotencyKey` is reused across retries
158
- * so the server dedupes correctly. Default: 0.
159
154
  * - `label` — human-readable audit tag. Flows to `mutation_log.label`
160
155
  * server-side for operator debugging ("nightly cleanup", "user click").
161
156
  */
162
157
  export interface MutationOptions {
163
158
  idempotencyKey?: string | null;
164
- timeout?: number;
165
- maxNetworkRetries?: number;
166
159
  label?: string;
167
160
  wait?: 'queued' | 'confirmed';
168
161
  readAt?: number | null;
@@ -205,9 +198,8 @@ export interface MutationOperation {
205
198
  /**
206
199
  * Per-op idempotency + audit metadata. `idempotencyKey` doubles as
207
200
  * the `mutation_log.client_tx_id` cache key; `label` is persisted to
208
- * `mutation_log.label` for debugging. Client-only fields (`timeout`,
209
- * `maxNetworkRetries`) are handled at the transport layer and are
210
- * NOT sent over the wire.
201
+ * `mutation_log.label` for debugging. These are the only `MutationOptions`
202
+ * fields carried over the wire.
211
203
  */
212
204
  options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
213
205
  }
@@ -21,8 +21,8 @@
21
21
  */
22
22
  import type { Schema } from '../schema/schema.js';
23
23
  import type { SyncStoreContract } from '../react/context.js';
24
- import { type MutateActions } from '../react/useMutate.js';
25
- import { type ReaderActions, type ReaderFindOptions } from '../react/useReader.js';
24
+ import { type MutateActions } from './mutateActions.js';
25
+ import { type ReaderActions, type ReaderFindOptions } from './readerActions.js';
26
26
  /**
27
27
  * The full transaction surface. `tx.mutations.<key>.*` for writes,
28
28
  * `tx.read.<key>.*` for imperative reads. Re-exports the base read options
@@ -19,8 +19,8 @@
19
19
  * microtask coalescer in `TransactionQueue` collapses N pushes into one
20
20
  * wire commit. Same shape Zero uses: no `insertMany`, just an array map.
21
21
  */
22
- import { createMutateActions } from '../react/useMutate.js';
23
- import { createReaderActions } from '../react/useReader.js';
22
+ import { createMutateActions } from './mutateActions.js';
23
+ import { createReaderActions } from './readerActions.js';
24
24
  import { AbloValidationError } from '../errors.js';
25
25
  /**
26
26
  * Build a Transaction for a single mutator invocation. The returned object