@abloatai/ablo 0.5.1 → 0.7.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 (129) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +248 -124
  3. package/dist/BaseSyncedStore.d.ts +3 -3
  4. package/dist/BaseSyncedStore.js +3 -3
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +91 -93
  8. package/dist/client/Ablo.js +122 -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 +116 -90
  14. package/dist/client/createModelProxy.js +128 -128
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +5 -5
  18. package/dist/coordination/index.d.ts +6 -0
  19. package/dist/coordination/index.js +6 -0
  20. package/dist/coordination/schema.d.ts +329 -0
  21. package/dist/coordination/schema.js +209 -0
  22. package/dist/core/QueryView.d.ts +4 -1
  23. package/dist/core/QueryView.js +1 -1
  24. package/dist/core/index.d.ts +2 -0
  25. package/dist/core/index.js +7 -0
  26. package/dist/core/query-utils.d.ts +7 -10
  27. package/dist/core/query-utils.js +2 -3
  28. package/dist/errorCodes.d.ts +264 -0
  29. package/dist/errorCodes.js +251 -0
  30. package/dist/errors.d.ts +59 -14
  31. package/dist/errors.js +73 -12
  32. package/dist/index.d.ts +11 -9
  33. package/dist/index.js +8 -12
  34. package/dist/interfaces/index.d.ts +2 -10
  35. package/dist/mutators/Transaction.d.ts +2 -2
  36. package/dist/mutators/Transaction.js +2 -2
  37. package/dist/mutators/mutateActions.d.ts +44 -0
  38. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  39. package/dist/mutators/readerActions.d.ts +32 -0
  40. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  41. package/dist/policy/index.d.ts +1 -1
  42. package/dist/policy/index.js +1 -1
  43. package/dist/policy/types.d.ts +31 -0
  44. package/dist/policy/types.js +15 -0
  45. package/dist/query/types.d.ts +1 -1
  46. package/dist/react/AbloProvider.d.ts +13 -1
  47. package/dist/react/AbloProvider.js +14 -6
  48. package/dist/react/context.d.ts +4 -4
  49. package/dist/react/index.d.ts +4 -5
  50. package/dist/react/index.js +3 -7
  51. package/dist/react/useAblo.d.ts +14 -14
  52. package/dist/react/useAblo.js +26 -26
  53. package/dist/react/useIntent.d.ts +2 -2
  54. package/dist/react/useIntent.js +2 -2
  55. package/dist/react/useMutators.d.ts +1 -1
  56. package/dist/react/usePresence.d.ts +3 -3
  57. package/dist/react/usePresence.js +4 -4
  58. package/dist/react/useUndoScope.d.ts +1 -1
  59. package/dist/schema/ddl.d.ts +62 -0
  60. package/dist/schema/ddl.js +317 -0
  61. package/dist/schema/diff.d.ts +167 -0
  62. package/dist/schema/diff.js +280 -0
  63. package/dist/schema/field.d.ts +16 -19
  64. package/dist/schema/field.js +30 -17
  65. package/dist/schema/generate.d.ts +19 -0
  66. package/dist/schema/generate.js +87 -0
  67. package/dist/schema/index.d.ts +9 -3
  68. package/dist/schema/index.js +14 -2
  69. package/dist/schema/model.d.ts +87 -25
  70. package/dist/schema/model.js +33 -3
  71. package/dist/schema/relation.d.ts +17 -0
  72. package/dist/schema/roles.d.ts +148 -0
  73. package/dist/schema/roles.js +149 -0
  74. package/dist/schema/schema.d.ts +10 -69
  75. package/dist/schema/schema.js +58 -24
  76. package/dist/schema/select.d.ts +25 -0
  77. package/dist/schema/select.js +55 -0
  78. package/dist/schema/serialize.d.ts +96 -0
  79. package/dist/schema/serialize.js +231 -0
  80. package/dist/schema/sugar.d.ts +20 -3
  81. package/dist/schema/sugar.js +5 -1
  82. package/dist/schema/tenancy.d.ts +66 -0
  83. package/dist/schema/tenancy.js +58 -0
  84. package/dist/sync/HydrationCoordinator.d.ts +2 -0
  85. package/dist/sync/HydrationCoordinator.js +23 -17
  86. package/dist/sync/SyncWebSocket.d.ts +17 -0
  87. package/dist/sync/SyncWebSocket.js +46 -1
  88. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  89. package/dist/sync/awaitIntentGrant.js +60 -0
  90. package/dist/sync/createIntentStream.d.ts +2 -1
  91. package/dist/sync/createIntentStream.js +89 -5
  92. package/dist/sync/createPresenceStream.js +1 -1
  93. package/dist/sync/participants.d.ts +2 -2
  94. package/dist/sync/participants.js +9 -18
  95. package/dist/types/global.d.ts +43 -52
  96. package/dist/types/global.js +16 -18
  97. package/dist/types/streams.d.ts +90 -42
  98. package/docs/api-keys.md +44 -0
  99. package/docs/api.md +72 -173
  100. package/docs/audit.md +5 -5
  101. package/docs/cli.md +212 -0
  102. package/docs/client-behavior.md +42 -43
  103. package/docs/coordination.md +343 -0
  104. package/docs/data-sources.md +16 -16
  105. package/docs/examples/agent-human.md +30 -32
  106. package/docs/examples/ai-sdk-tool.md +32 -33
  107. package/docs/examples/existing-python-backend.md +38 -36
  108. package/docs/examples/nextjs.md +24 -25
  109. package/docs/examples/scoped-agent.md +78 -0
  110. package/docs/examples/server-agent.md +20 -61
  111. package/docs/guarantees.md +34 -56
  112. package/docs/identity.md +529 -0
  113. package/docs/index.md +18 -24
  114. package/docs/integration-guide.md +130 -144
  115. package/docs/interaction-model.md +32 -95
  116. package/docs/mcp/claude-code.md +3 -3
  117. package/docs/mcp/cursor.md +1 -1
  118. package/docs/mcp/windsurf.md +1 -1
  119. package/docs/mcp.md +11 -26
  120. package/docs/quickstart.md +43 -49
  121. package/docs/react.md +74 -24
  122. package/docs/roadmap.md +17 -7
  123. package/llms.txt +34 -39
  124. package/package.json +8 -1
  125. package/dist/react/useMutate.d.ts +0 -83
  126. package/dist/react/useQuery.d.ts +0 -123
  127. package/dist/react/useQuery.js +0 -145
  128. package/dist/react/useReader.d.ts +0 -69
  129. package/docs/capabilities.md +0 -163
package/dist/errors.d.ts CHANGED
@@ -18,27 +18,71 @@
18
18
  *
19
19
  * Both work on every subclass.
20
20
  */
21
+ import type { ErrorCode } from './errorCodes.js';
22
+ export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec } from './errorCodes.js';
23
+ export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode } from './errorCodes.js';
21
24
  /** Common shape for all errors thrown by this SDK. */
22
25
  export declare class AbloError extends Error {
23
26
  /** Discriminator string — matches the class name. Lets consumers
24
27
  * switch on `e.type` without `instanceof` checks across package
25
28
  * boundaries (matches Stripe's `err.type` pattern). */
26
29
  readonly type: string;
27
- /** Stable short identifier for logs + metrics.
28
- * E.g. `'apikey_invalid'`, `'capability_scope_denied'`. */
30
+ /** Stable short identifier for logs + metrics, drawn from the closed
31
+ * {@link ErrorCode} registry — e.g. `'apikey_invalid'`,
32
+ * `'capability_scope_denied'`. Stored as a plain `string` (not
33
+ * `ErrorCode`) so an older SDK still surfaces a newer server's code it
34
+ * doesn't recognise yet; producers are constrained at the constructor
35
+ * param instead. */
29
36
  readonly code?: string;
30
37
  /** HTTP status code when the error originated from an HTTP response. */
31
38
  readonly httpStatus?: number;
32
39
  /** Correlation id for ops — present when the server sent one on
33
40
  * `x-request-id`. Include in support tickets. */
34
41
  readonly requestId?: string;
42
+ /** Which input caused the error — a model/field path like
43
+ * `'dataroomMember.grants.subject'`. Mirrors Stripe's `error.param`;
44
+ * lets tooling point at the exact offending declaration. */
45
+ readonly param?: string;
46
+ /** Link to the docs for this `code`. Mirrors Stripe's `error.doc_url`.
47
+ * Defaults from `code` via {@link docUrlForCode} when omitted. */
48
+ readonly docUrl?: string;
49
+ /** Domain-specific structured payload merged into the wire envelope —
50
+ * e.g. a schema push's `{ warnings, unexecutable }`, a stale write's
51
+ * conflicting rows. Mirrors how Stripe attaches type-specific fields
52
+ * (`decline_code`, `payment_intent`) alongside the standard ones, so a
53
+ * structured error keeps its detail through `toJSON` instead of being
54
+ * flattened to a bare message. */
55
+ readonly details?: Readonly<Record<string, unknown>>;
35
56
  constructor(message: string, options?: {
36
- code?: string;
57
+ code?: ErrorCode;
37
58
  httpStatus?: number;
38
59
  requestId?: string;
39
60
  cause?: unknown;
61
+ param?: string;
62
+ docUrl?: string;
63
+ details?: Readonly<Record<string, unknown>>;
40
64
  });
65
+ /**
66
+ * Serialize to Stripe's error-object shape: `{ type, code, param, message,
67
+ * doc_url, request_id }`. One JSON shape across HTTP bodies, WS frames, and
68
+ * logs — so consumers parse Ablo errors the way they already parse Stripe's.
69
+ */
70
+ toJSON(): {
71
+ type: string;
72
+ code?: string;
73
+ param?: string;
74
+ message: string;
75
+ doc_url?: string;
76
+ request_id?: string;
77
+ [key: string]: unknown;
78
+ };
41
79
  }
80
+ /**
81
+ * Map a stable error `code` to its docs URL — the one place the convention
82
+ * lives, so every error carrying a code gets a `doc_url` for free (Stripe
83
+ * ships a link on every error).
84
+ */
85
+ export declare function docUrlForCode(code: ErrorCode): string;
42
86
  /** 401 — invalid/missing/expired credentials. */
43
87
  export declare class AbloAuthenticationError extends AbloError {
44
88
  readonly type: "AbloAuthenticationError";
@@ -53,11 +97,12 @@ export declare class AbloRateLimitError extends AbloError {
53
97
  readonly type: "AbloRateLimitError";
54
98
  readonly retryAfterSeconds?: number;
55
99
  constructor(message: string, options?: {
56
- code?: string;
100
+ code?: ErrorCode;
57
101
  httpStatus?: number;
58
102
  requestId?: string;
59
103
  cause?: unknown;
60
104
  retryAfterSeconds?: number;
105
+ details?: Readonly<Record<string, unknown>>;
61
106
  });
62
107
  }
63
108
  /** 409 — same `Idempotency-Key` reused with a different request body. */
@@ -98,7 +143,7 @@ export declare class AbloStaleContextError extends AbloError {
98
143
  readonly observedSyncId: number;
99
144
  }>;
100
145
  constructor(message: string, options?: {
101
- code?: string;
146
+ code?: ErrorCode;
102
147
  httpStatus?: number;
103
148
  requestId?: string;
104
149
  cause?: unknown;
@@ -111,21 +156,21 @@ export declare class AbloStaleContextError extends AbloError {
111
156
  });
112
157
  }
113
158
  /**
114
- * The target entity currently has an active intent held by another
115
- * participant and the caller asked the SDK not to return immediately.
159
+ * The target entity is currently claimed by another participant and the caller
160
+ * asked the SDK not to read/write through that claim.
116
161
  *
117
- * Use `ifBusy: 'wait'` to wait for the intent stream to clear, or
118
- * `ifBusy: 'return'` to inspect the active intents yourself.
162
+ * Use `ifClaimed: 'wait'` to wait for the claim to clear, or
163
+ * `ifClaimed: 'return'` to inspect active claims yourself.
119
164
  */
120
- export declare class AbloBusyError extends AbloError {
121
- readonly type: "AbloBusyError";
122
- readonly intents?: ReadonlyArray<unknown>;
165
+ export declare class AbloClaimedError extends AbloError {
166
+ readonly type: "AbloClaimedError";
167
+ readonly claims?: ReadonlyArray<unknown>;
123
168
  constructor(message: string, options?: {
124
- code?: string;
169
+ code?: ErrorCode;
125
170
  httpStatus?: number;
126
171
  requestId?: string;
127
172
  cause?: unknown;
128
- intents?: ReadonlyArray<unknown>;
173
+ claims?: ReadonlyArray<unknown>;
129
174
  });
130
175
  }
131
176
  /**
package/dist/errors.js CHANGED
@@ -18,6 +18,7 @@
18
18
  *
19
19
  * Both work on every subclass.
20
20
  */
21
+ export { ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode } from './errorCodes.js';
21
22
  // ── AbloError hierarchy — the typed error surface ────────────────────
22
23
  /** Common shape for all errors thrown by this SDK. */
23
24
  export class AbloError extends Error {
@@ -25,14 +26,32 @@ export class AbloError extends Error {
25
26
  * switch on `e.type` without `instanceof` checks across package
26
27
  * boundaries (matches Stripe's `err.type` pattern). */
27
28
  type = 'AbloError';
28
- /** Stable short identifier for logs + metrics.
29
- * E.g. `'apikey_invalid'`, `'capability_scope_denied'`. */
29
+ /** Stable short identifier for logs + metrics, drawn from the closed
30
+ * {@link ErrorCode} registry — e.g. `'apikey_invalid'`,
31
+ * `'capability_scope_denied'`. Stored as a plain `string` (not
32
+ * `ErrorCode`) so an older SDK still surfaces a newer server's code it
33
+ * doesn't recognise yet; producers are constrained at the constructor
34
+ * param instead. */
30
35
  code;
31
36
  /** HTTP status code when the error originated from an HTTP response. */
32
37
  httpStatus;
33
38
  /** Correlation id for ops — present when the server sent one on
34
39
  * `x-request-id`. Include in support tickets. */
35
40
  requestId;
41
+ /** Which input caused the error — a model/field path like
42
+ * `'dataroomMember.grants.subject'`. Mirrors Stripe's `error.param`;
43
+ * lets tooling point at the exact offending declaration. */
44
+ param;
45
+ /** Link to the docs for this `code`. Mirrors Stripe's `error.doc_url`.
46
+ * Defaults from `code` via {@link docUrlForCode} when omitted. */
47
+ docUrl;
48
+ /** Domain-specific structured payload merged into the wire envelope —
49
+ * e.g. a schema push's `{ warnings, unexecutable }`, a stale write's
50
+ * conflicting rows. Mirrors how Stripe attaches type-specific fields
51
+ * (`decline_code`, `payment_intent`) alongside the standard ones, so a
52
+ * structured error keeps its detail through `toJSON` instead of being
53
+ * flattened to a bare message. */
54
+ details;
36
55
  constructor(message, options) {
37
56
  super(message);
38
57
  this.name = this.constructor.name;
@@ -42,10 +61,41 @@ export class AbloError extends Error {
42
61
  this.httpStatus = options.httpStatus;
43
62
  if (options?.requestId !== undefined)
44
63
  this.requestId = options.requestId;
64
+ if (options?.param !== undefined)
65
+ this.param = options.param;
66
+ if (options?.details !== undefined)
67
+ this.details = options.details;
68
+ const docUrl = options?.docUrl ?? (options?.code ? docUrlForCode(options.code) : undefined);
69
+ if (docUrl !== undefined)
70
+ this.docUrl = docUrl;
45
71
  if (options?.cause !== undefined) {
46
72
  Object.defineProperty(this, 'cause', { value: options.cause, enumerable: false });
47
73
  }
48
74
  }
75
+ /**
76
+ * Serialize to Stripe's error-object shape: `{ type, code, param, message,
77
+ * doc_url, request_id }`. One JSON shape across HTTP bodies, WS frames, and
78
+ * logs — so consumers parse Ablo errors the way they already parse Stripe's.
79
+ */
80
+ toJSON() {
81
+ return {
82
+ type: this.type,
83
+ ...(this.code !== undefined ? { code: this.code } : {}),
84
+ ...(this.param !== undefined ? { param: this.param } : {}),
85
+ message: this.message,
86
+ ...(this.docUrl !== undefined ? { doc_url: this.docUrl } : {}),
87
+ ...(this.requestId !== undefined ? { request_id: this.requestId } : {}),
88
+ ...(this.details ?? {}),
89
+ };
90
+ }
91
+ }
92
+ /**
93
+ * Map a stable error `code` to its docs URL — the one place the convention
94
+ * lives, so every error carrying a code gets a `doc_url` for free (Stripe
95
+ * ships a link on every error).
96
+ */
97
+ export function docUrlForCode(code) {
98
+ return `https://docs.abloatai.com/errors#${code}`;
49
99
  }
50
100
  /** 401 — invalid/missing/expired credentials. */
51
101
  export class AbloAuthenticationError extends AbloError {
@@ -109,19 +159,19 @@ export class AbloStaleContextError extends AbloError {
109
159
  }
110
160
  }
111
161
  /**
112
- * The target entity currently has an active intent held by another
113
- * participant and the caller asked the SDK not to return immediately.
162
+ * The target entity is currently claimed by another participant and the caller
163
+ * asked the SDK not to read/write through that claim.
114
164
  *
115
- * Use `ifBusy: 'wait'` to wait for the intent stream to clear, or
116
- * `ifBusy: 'return'` to inspect the active intents yourself.
165
+ * Use `ifClaimed: 'wait'` to wait for the claim to clear, or
166
+ * `ifClaimed: 'return'` to inspect active claims yourself.
117
167
  */
118
- export class AbloBusyError extends AbloError {
119
- type = 'AbloBusyError';
120
- intents;
168
+ export class AbloClaimedError extends AbloError {
169
+ type = 'AbloClaimedError';
170
+ claims;
121
171
  constructor(message, options) {
122
172
  super(message, options);
123
- if (options?.intents !== undefined)
124
- this.intents = options.intents;
173
+ if (options?.claims !== undefined)
174
+ this.claims = options.claims;
125
175
  }
126
176
  }
127
177
  /**
@@ -224,7 +274,11 @@ export function translateHttpError(status, body, requestId) {
224
274
  flatError ??
225
275
  (typeof body === 'string' ? body : `HTTP ${status}`);
226
276
  const requiredCapability = nested?.requiredCapability ?? parsed.requiredCapability;
227
- const baseOpts = { code, httpStatus: status, requestId };
277
+ // Wire boundary: an incoming code is an arbitrary string (a newer server
278
+ // may send a code this SDK predates). Cast to ErrorCode here — the one
279
+ // sanctioned crossing — so internal producers stay statically checked.
280
+ const publicCode = (code === 'intent_conflict' ? 'claim_conflict' : code);
281
+ const baseOpts = { code: publicCode, httpStatus: status, requestId };
228
282
  if (status === 401)
229
283
  return new AbloAuthenticationError(message, baseOpts);
230
284
  if (status === 403 || code === 'capability_scope_denied' || code === 'capability_invalid') {
@@ -233,6 +287,13 @@ export function translateHttpError(status, body, requestId) {
233
287
  }
234
288
  return new AbloPermissionError(message, baseOpts);
235
289
  }
290
+ // Claim enforcement also rides 409 (a commit blocked by a foreign claim).
291
+ // Discriminate on the code BEFORE the generic idempotency mapping so a
292
+ // claim rejection surfaces as AbloClaimedError, not AbloIdempotencyError —
293
+ // same typed error the WebSocket commit path yields for these codes.
294
+ if (code === 'intent_conflict' || code === 'claim_conflict' || code === 'entity_claimed') {
295
+ return new AbloClaimedError(message, baseOpts);
296
+ }
236
297
  if (status === 409)
237
298
  return new AbloIdempotencyError(message, baseOpts);
238
299
  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,16 @@
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
- export { defaultPolicy } from './policy/index.js';
52
- export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloBusyError, CapabilityError, translateHttpError, } from './errors.js';
51
+ export { defaultPolicy, capabilityPreemptPolicy } from './policy/index.js';
52
+ export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, } from './errors.js';
53
53
  export type { CommitReceipt, RequiredCapability } from './errors.js';
54
+ export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec } from './errors.js';
55
+ export type { Register, DefaultSyncShape } from './types/global.js';
54
56
  export { defineMutators } from './mutators/defineMutators.js';
55
57
  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:
@@ -74,15 +74,11 @@ export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest,
74
74
  // (reject-on-stale) is already applied server-side, so you only import it
75
75
  // to COMPOSE a custom policy. Leave it alone and stale writes are rejected
76
76
  // safely by default. Type counterparts live under `Ablo.Conflict.*`.
77
- export { defaultPolicy } from './policy/index.js';
77
+ export { defaultPolicy, capabilityPreemptPolicy } 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, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, } 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
@@ -0,0 +1,44 @@
1
+ import type { Schema, InferModel, InferCreate } from '../schema/schema.js';
2
+ import type { SyncStoreContract } from '../react/context.js';
3
+ /**
4
+ * `create` / `update` / `delete` are overloaded: pass one row or an array
5
+ * (Drizzle/Prisma `values(rowOrRows)` shape). Every entry in an array call
6
+ * lands in the same synchronous tick (`Promise.all`), so the microtask
7
+ * coalescer in `TransactionQueue` collapses N pushes into one wire commit.
8
+ *
9
+ * This module is the React-free core of CRUD staging. The transaction system
10
+ * (`Transaction` / `RecordingTransaction`) and `BaseSyncedStore` build on it;
11
+ * there is no React hook here (the legacy `useMutate` hook was removed —
12
+ * callers use `ablo.<model>.create/update/delete`).
13
+ */
14
+ type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> = {
15
+ id: string;
16
+ } & Partial<InferModel<S, K>>;
17
+ export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
18
+ /**
19
+ * Create one entity, or an array of entities in a single tick. ID,
20
+ * createdAt, updatedAt, organizationId default automatically per row.
21
+ */
22
+ create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
23
+ create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
24
+ /**
25
+ * Update one row, or an array of rows in a single tick. Each patch is
26
+ * `{ id, ...changes }` — missing ids throw. Schema-generated models are
27
+ * MobX-observable, so direct assignment fires reactivity.
28
+ */
29
+ update(patch: UpdatePatch<S, K>): Promise<InferModel<S, K>>;
30
+ update(patches: UpdatePatch<S, K>[]): Promise<InferModel<S, K>[]>;
31
+ /**
32
+ * Delete one row by id, or an array of ids in a single tick. Missing ids
33
+ * are silently ignored.
34
+ */
35
+ delete(id: string): Promise<void>;
36
+ delete(ids: string[]): Promise<void>;
37
+ /** Soft-archive by ID. */
38
+ archive: (id: string) => Promise<void>;
39
+ /** Restore an archived entity by ID. */
40
+ unarchive: (id: string) => Promise<void>;
41
+ }
42
+ /** Pure factory — builds CRUD actions over a store for one model. React-free. */
43
+ export declare function createMutateActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract, organizationId: string): MutateActions<S, K>;
44
+ export {};
@@ -1,12 +1,6 @@
1
- 'use client';
2
- import { useMemo } from 'react';
3
1
  import { Model, modelAsRow } from '../Model.js';
4
2
  import { AbloValidationError } from '../errors.js';
5
- import { useSyncContext } from './context.js';
6
- /**
7
- * Pure factory — testable without React. The hook just wraps this in
8
- * useMemo with the React context.
9
- */
3
+ /** Pure factory builds CRUD actions over a store for one model. React-free. */
10
4
  export function createMutateActions(schema, modelKey, store, organizationId) {
11
5
  const modelDef = schema.models[modelKey];
12
6
  const typename = modelDef?.typename ?? modelKey;
@@ -25,7 +19,7 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
25
19
  };
26
20
  const model = store.pool.createFromData(fullData);
27
21
  if (!model) {
28
- throw new AbloValidationError(`useMutate: failed to create ${typename} — no constructor in registry`, { code: 'mutate_create_unknown_model' });
22
+ throw new AbloValidationError(`createMutateActions: failed to create ${typename} — no constructor in registry`, { code: 'mutate_create_unknown_model' });
29
23
  }
30
24
  return model;
31
25
  };
@@ -34,14 +28,13 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
34
28
  const { id, ...changes } = patch;
35
29
  const model = store.pool.get(id);
36
30
  if (!model) {
37
- throw new AbloValidationError(`useMutate: ${typename} with id "${id}" not found in pool`, { code: 'mutate_update_entity_not_found' });
31
+ throw new AbloValidationError(`createMutateActions: ${typename} with id "${id}" not found in pool`, { code: 'mutate_update_entity_not_found' });
38
32
  }
39
- // Schema-derived patch keys are validated at the call-site type
40
- // signature (`UpdatePatch<S, K>`); writes here are dynamic-class
41
- // field assignments. `Reflect.set` is the typed bridge — Model
42
- // doesn't carry an index signature for arbitrary string keys, but
43
- // the dynamic field installation in `createDynamicModelClass`
44
- // guarantees these keys resolve at runtime.
33
+ // Schema-derived patch keys are validated at the call-site type signature
34
+ // (`UpdatePatch<S, K>`); writes here are dynamic-class field assignments.
35
+ // `Reflect.set` is the typed bridge — Model carries no index signature, but
36
+ // the dynamic field installation in `createDynamicModelClass` guarantees
37
+ // these keys resolve at runtime.
45
38
  for (const [fieldName, value] of Object.entries(changes)) {
46
39
  Reflect.set(model, fieldName, value);
47
40
  }
@@ -49,9 +42,9 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
49
42
  return model;
50
43
  };
51
44
  return {
52
- // Overloaded — runtime check on `Array.isArray` decides shape. Both
53
- // branches stage via `Promise.all` so the microtask coalescer in
54
- // `TransactionQueue` collapses N pushes into one wire commit.
45
+ // Overloaded — runtime `Array.isArray` decides shape. Both branches stage
46
+ // via `Promise.all` so the microtask coalescer collapses N pushes into one
47
+ // wire commit.
55
48
  create: (async (data) => {
56
49
  const now = new Date();
57
50
  if (Array.isArray(data)) {
@@ -110,13 +103,3 @@ export function createMutateActions(schema, modelKey, store, organizationId) {
110
103
  },
111
104
  };
112
105
  }
113
- export function useMutate(schemaOrKey, maybeKey) {
114
- const { store, organizationId, schema: ctxSchema } = useSyncContext();
115
- const resolvedSchema = typeof schemaOrKey === 'string' ? ctxSchema : schemaOrKey;
116
- const resolvedKey = typeof schemaOrKey === 'string' ? schemaOrKey : maybeKey;
117
- if (!resolvedSchema) {
118
- throw new AbloValidationError('useMutate: no schema available. Pass the schema as the first arg ' +
119
- 'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'mutate_schema_missing' });
120
- }
121
- return useMemo(() => createMutateActions(resolvedSchema, resolvedKey, store, organizationId), [store, organizationId, resolvedSchema, resolvedKey]);
122
- }
@@ -0,0 +1,32 @@
1
+ import type { Schema, InferModel } from '../schema/schema.js';
2
+ import type { SyncStoreContract } from '../react/context.js';
3
+ /**
4
+ * React-free imperative reads over a store: one-off `retrieve`/`list`/`count`
5
+ * snapshots that do NOT subscribe to changes. Used by the transaction system
6
+ * and `BaseSyncedStore`. For reactive reads in components use
7
+ * `useAblo((ablo) => ablo.<model>.retrieve(id) / .list(opts))`.
8
+ */
9
+ export interface ReaderFindOptions<T> {
10
+ /** Equality filter — uses FK index when the field is registered. */
11
+ where?: Partial<T>;
12
+ /** Predicate applied AFTER `where` filtering. */
13
+ filter?: (entity: T) => boolean;
14
+ /** Sort field. */
15
+ orderBy?: keyof T & string;
16
+ /** Sort direction. Default: 'asc'. */
17
+ order?: 'asc' | 'desc';
18
+ /** Max results. */
19
+ limit?: number;
20
+ /** Skip N results. */
21
+ offset?: number;
22
+ }
23
+ export interface ReaderActions<S extends Schema, K extends keyof S['models'] & string> {
24
+ /** Get a single entity by id. Returns undefined if not in pool. */
25
+ retrieve: (id: string) => InferModel<S, K> | undefined;
26
+ /** Read a collection with optional filters. Snapshot — not reactive. */
27
+ list: (options?: ReaderFindOptions<InferModel<S, K>>) => InferModel<S, K>[];
28
+ /** Count entities matching the options. */
29
+ count: (options?: ReaderFindOptions<InferModel<S, K>>) => number;
30
+ }
31
+ /** Pure factory — builds imperative read actions over a store for one model. */
32
+ export declare function createReaderActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract): ReaderActions<S, K>;
@@ -1,15 +1,9 @@
1
- 'use client';
2
- import { useMemo } from 'react';
3
- import { useSyncContext } from './context.js';
4
- import { AbloValidationError } from '../errors.js';
5
- /**
6
- * Pure factory — testable without React. `useReader` wraps this in useMemo.
7
- */
1
+ /** Pure factory — builds imperative read actions over a store for one model. */
8
2
  export function createReaderActions(schema, modelKey, store) {
9
3
  const modelDef = schema.models[modelKey];
10
4
  const typename = modelDef?.typename ?? modelKey;
11
5
  function read(options) {
12
- // FK index fast path: single-field `where` on a registered FK index → O(1) lookup.
6
+ // FK index fast path: single-field `where` on a registered FK index → O(1).
13
7
  let candidates;
14
8
  const whereEntries = options?.where ? Object.entries(options.where) : [];
15
9
  const singleWhere = whereEntries.length === 1 ? whereEntries[0] : undefined;
@@ -61,13 +55,3 @@ export function createReaderActions(schema, modelKey, store) {
61
55
  count: (options) => read(options).length,
62
56
  };
63
57
  }
64
- export function useReader(schemaOrKey, maybeKey) {
65
- const { store, schema: ctxSchema } = useSyncContext();
66
- const resolvedSchema = typeof schemaOrKey === 'string' ? ctxSchema : schemaOrKey;
67
- const resolvedKey = typeof schemaOrKey === 'string' ? schemaOrKey : maybeKey;
68
- if (!resolvedSchema) {
69
- throw new AbloValidationError('useReader: no schema available. Pass the schema as the first arg ' +
70
- 'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'reader_schema_missing' });
71
- }
72
- return useMemo(() => createReaderActions(resolvedSchema, resolvedKey, store), [store, resolvedSchema, resolvedKey]);
73
- }
@@ -16,4 +16,4 @@
16
16
  * ```
17
17
  */
18
18
  export type { Conflict, ConflictDecision, ConflictKind, ConflictOperation, ConflictPolicy, StaleContextConflict, IntentHeldConflict, } from './types.js';
19
- export { defaultPolicy } from './types.js';
19
+ export { defaultPolicy, capabilityPreemptPolicy } from './types.js';
@@ -15,4 +15,4 @@
15
15
  * };
16
16
  * ```
17
17
  */
18
- export { defaultPolicy } from './types.js';
18
+ export { defaultPolicy, capabilityPreemptPolicy } from './types.js';