@abloatai/ablo 0.35.0 → 0.36.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 (215) hide show
  1. package/CHANGELOG.md +713 -629
  2. package/README.md +56 -519
  3. package/bin/ablo.cjs +39 -0
  4. package/dist/BaseSyncedStore.d.ts +24 -4
  5. package/dist/BaseSyncedStore.js +53 -37
  6. package/dist/Database.d.ts +8 -20
  7. package/dist/Database.js +61 -59
  8. package/dist/InstanceCache.d.ts +6 -2
  9. package/dist/InstanceCache.js +18 -16
  10. package/dist/Model.d.ts +17 -7
  11. package/dist/Model.js +17 -7
  12. package/dist/ModelRegistry.d.ts +4 -0
  13. package/dist/ModelRegistry.js +17 -15
  14. package/dist/NetworkMonitor.d.ts +3 -1
  15. package/dist/NetworkMonitor.js +7 -5
  16. package/dist/SyncClient.d.ts +5 -15
  17. package/dist/SyncClient.js +60 -57
  18. package/dist/client/Ablo.js +30 -19
  19. package/dist/client/createInternalComponents.d.ts +4 -0
  20. package/dist/client/createInternalComponents.js +8 -2
  21. package/dist/client/createModelProxy.d.ts +21 -1
  22. package/dist/client/createModelProxy.js +123 -57
  23. package/dist/client/humans.d.ts +30 -9
  24. package/dist/client/humans.js +45 -19
  25. package/dist/client/reactiveEngine.d.ts +16 -11
  26. package/dist/client/reactiveEngine.js +113 -335
  27. package/dist/client/storeCluster.d.ts +47 -0
  28. package/dist/client/storeCluster.js +118 -0
  29. package/dist/client/storeLifecycle.d.ts +61 -0
  30. package/dist/client/storeLifecycle.js +231 -0
  31. package/dist/context.d.ts +13 -0
  32. package/dist/context.js +23 -0
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/docs/catalog.js +6 -3
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +4 -2
  37. package/dist/query/client.d.ts +3 -0
  38. package/dist/query/client.js +6 -5
  39. package/dist/react/AbloProvider.d.ts +13 -1
  40. package/dist/react/AbloProvider.js +5 -2
  41. package/dist/react/context.d.ts +2 -2
  42. package/dist/react/createAbloReact.d.ts +56 -0
  43. package/dist/react/createAbloReact.js +51 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/useAblo.d.ts +9 -2
  47. package/dist/react/useAblo.js +25 -7
  48. package/dist/schema/coordination.js +5 -1
  49. package/dist/schema/index.d.ts +1 -0
  50. package/dist/schema/index.js +4 -0
  51. package/dist/schema/select.js +3 -0
  52. package/dist/schema/serialize.js +3 -0
  53. package/dist/source/adapter.d.ts +7 -5
  54. package/dist/source/adapter.js +7 -5
  55. package/dist/source/index.d.ts +1 -1
  56. package/dist/source/index.js +1 -1
  57. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
  58. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  59. package/dist/{core → stores}/DatabaseManager.js +13 -12
  60. package/dist/{core → stores}/StoreManager.d.ts +5 -3
  61. package/dist/{core → stores}/StoreManager.js +24 -22
  62. package/dist/stores/SyncActionStore.d.ts +3 -1
  63. package/dist/stores/SyncActionStore.js +9 -7
  64. package/dist/sync/BootstrapFetcher.d.ts +4 -0
  65. package/dist/sync/BootstrapFetcher.js +28 -26
  66. package/dist/sync/OnDemandLoader.d.ts +3 -0
  67. package/dist/sync/OnDemandLoader.js +1 -0
  68. package/dist/sync/bootstrapApply.d.ts +3 -0
  69. package/dist/sync/bootstrapApply.js +2 -2
  70. package/dist/sync/deltaPipeline.d.ts +13 -12
  71. package/dist/sync/deltaPipeline.js +21 -4
  72. package/dist/sync/groupChange.d.ts +3 -0
  73. package/dist/sync/groupChange.js +16 -14
  74. package/dist/sync/participants.d.ts +19 -2
  75. package/dist/sync/participants.js +3 -1
  76. package/dist/sync/schemas.d.ts +2 -1
  77. package/dist/sync/schemas.js +3 -3
  78. package/dist/syncLog/contract.d.ts +20 -0
  79. package/dist/syncLog/contract.js +19 -0
  80. package/dist/syncLog/index.d.ts +1 -0
  81. package/dist/syncLog/index.js +1 -0
  82. package/dist/transaction/auth/capability.d.ts +35 -0
  83. package/dist/transaction/auth/capability.js +25 -0
  84. package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
  85. package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
  86. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
  87. package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
  88. package/dist/transaction/coordination/index.d.ts +4 -4
  89. package/dist/transaction/coordination/index.js +4 -3
  90. package/dist/transaction/coordination/locator.d.ts +23 -2
  91. package/dist/transaction/coordination/locator.js +22 -2
  92. package/dist/transaction/coordination/schema.d.ts +125 -62
  93. package/dist/transaction/coordination/schema.js +228 -64
  94. package/dist/transaction/coordination/targetConflict.js +32 -28
  95. package/dist/transaction/errorCodes.d.ts +2 -2
  96. package/dist/transaction/errorCodes.js +13 -9
  97. package/dist/transaction/plugin.d.ts +95 -2
  98. package/dist/transaction/plugin.js +21 -2
  99. package/dist/transaction/resources/httpResources.d.ts +57 -2
  100. package/dist/transaction/resources/modelOperations.d.ts +148 -40
  101. package/dist/transaction/resources/where.d.ts +16 -0
  102. package/dist/transaction/resources/where.js +45 -0
  103. package/dist/transaction/schema/field.d.ts +12 -18
  104. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  105. package/dist/transaction/schema/fieldRef.js +11 -0
  106. package/dist/transaction/schema/openapi.d.ts +16 -15
  107. package/dist/transaction/schema/openapi.js +186 -25
  108. package/dist/transaction/schema/relation.d.ts +7 -2
  109. package/dist/transaction/schema/schema.d.ts +27 -0
  110. package/dist/transaction/schema/schema.js +20 -0
  111. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
  112. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
  113. package/dist/transaction/transport/httpClient.d.ts +9 -1
  114. package/dist/transaction/transport/httpClient.js +1 -0
  115. package/dist/transaction/transport/httpTransport.js +156 -44
  116. package/dist/transaction/transport/wsTransport.d.ts +2 -4
  117. package/dist/transaction/transport/wsTransport.js +16 -10
  118. package/dist/transaction/types/streams.d.ts +10 -0
  119. package/dist/transaction/utils/duration.d.ts +25 -0
  120. package/dist/transaction/utils/duration.js +32 -0
  121. package/dist/transaction/wire/accountResponses.d.ts +69 -0
  122. package/dist/transaction/wire/accountResponses.js +36 -1
  123. package/dist/transaction/wire/auth.d.ts +9 -2
  124. package/dist/transaction/wire/auth.js +7 -1
  125. package/dist/transaction/wire/claims.d.ts +164 -97
  126. package/dist/transaction/wire/claims.js +126 -28
  127. package/dist/transaction/wire/commit.d.ts +1 -1
  128. package/dist/transaction/wire/feedEvent.d.ts +27 -0
  129. package/dist/transaction/wire/feedEvent.js +27 -1
  130. package/dist/transaction/wire/frames.d.ts +2 -2
  131. package/dist/transaction/wire/inboundFrames.d.ts +12 -2
  132. package/dist/transaction/wire/index.d.ts +10 -6
  133. package/dist/transaction/wire/index.js +12 -3
  134. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  135. package/dist/transaction/wire/modelMutations.js +52 -0
  136. package/dist/transaction/wire/modelShape.d.ts +78 -0
  137. package/dist/transaction/wire/modelShape.js +74 -0
  138. package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
  139. package/dist/transactions/mutations/MutationQueue.js +55 -45
  140. package/dist/transactions/mutations/commitPayload.d.ts +3 -2
  141. package/dist/transactions/mutations/commitPayload.js +5 -5
  142. package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
  143. package/dist/transactions/mutations/deltaConfirmation.js +10 -8
  144. package/dist/transactions/mutations/replayValidation.d.ts +2 -1
  145. package/dist/transactions/mutations/replayValidation.js +3 -2
  146. package/dist/{core → views}/QueryView.d.ts +1 -1
  147. package/dist/{core → views}/QueryView.js +1 -1
  148. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  149. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  150. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  151. package/docs/agents.md +1 -1
  152. package/docs/api-keys.md +6 -6
  153. package/docs/api.md +5 -43
  154. package/docs/audit.md +4 -3
  155. package/docs/cli.md +11 -11
  156. package/docs/client-behavior.md +4 -4
  157. package/docs/concurrency-convention.md +28 -42
  158. package/docs/coordination.md +235 -83
  159. package/docs/data-sources.md +4 -4
  160. package/docs/debugging.md +34 -12
  161. package/docs/deployment.md +8 -8
  162. package/docs/examples/scoped-agent.md +3 -3
  163. package/docs/groups.md +57 -3
  164. package/docs/guarantees.md +37 -10
  165. package/docs/how-it-works.md +29 -5
  166. package/docs/idempotency.md +6 -6
  167. package/docs/identity.md +22 -23
  168. package/docs/index.md +8 -8
  169. package/docs/integration-guide.md +14 -3
  170. package/docs/mcp.md +7 -7
  171. package/docs/migration.md +34 -15
  172. package/docs/projects.md +1 -1
  173. package/docs/react.md +19 -8
  174. package/docs/sessions.md +1 -1
  175. package/docs/webhooks.md +9 -9
  176. package/llms.txt +4 -4
  177. package/package.json +13 -20
  178. package/dist/cli.cjs +0 -288600
  179. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  180. package/dist/testing/fixtures/bootstrap.js +0 -59
  181. package/dist/testing/fixtures/deltas.d.ts +0 -83
  182. package/dist/testing/fixtures/deltas.js +0 -136
  183. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  184. package/dist/testing/fixtures/httpResponses.js +0 -90
  185. package/dist/testing/fixtures/models.d.ts +0 -83
  186. package/dist/testing/fixtures/models.js +0 -272
  187. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  188. package/dist/testing/helpers/reactWrapper.js +0 -67
  189. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  190. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  191. package/dist/testing/helpers/wait.d.ts +0 -30
  192. package/dist/testing/helpers/wait.js +0 -49
  193. package/dist/testing/index.d.ts +0 -23
  194. package/dist/testing/index.js +0 -33
  195. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  196. package/dist/testing/mocks/FakeDatabase.js +0 -10
  197. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  198. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  199. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  200. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  201. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  202. package/dist/testing/mocks/MockSyncContext.js +0 -72
  203. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  204. package/dist/testing/mocks/MockSyncStore.js +0 -171
  205. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  206. package/dist/testing/mocks/MockWebSocket.js +0 -118
  207. package/docs/interaction-model.md +0 -99
  208. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  209. /package/dist/{core → query}/QueryProcessor.js +0 -0
  210. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  211. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  212. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  213. /package/dist/{source → transaction}/footprint.d.ts +0 -0
  214. /package/dist/{source → transaction}/footprint.js +0 -0
  215. /package/dist/{core → views}/ViewRegistry.js +0 -0
@@ -11,8 +11,8 @@
11
11
  */
12
12
  import type { ModelScope } from '../types/index.js';
13
13
  import type { ResolveClaimMeta } from '../types/global.js';
14
- import type { AbloClaimedError } from '../errors.js';
15
- import type { StaleNotification, TrackDependency } from '../coordination/schema.js';
14
+ import type { ClaimPart, StaleNotification, TrackDependency } from '../coordination/schema.js';
15
+ import type { ClaimHeartbeatPlan } from '../coordination/claimHeartbeatLoop.js';
16
16
  import type { Duration } from '../utils/duration.js';
17
17
  import type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease, TargetRange } from '../types/streams.js';
18
18
  import type { MutationOptions } from './mutationOptions.js';
@@ -99,24 +99,97 @@ export interface ServerReadOptions<T> {
99
99
  /** Options for the single-row async server read `retrieve({ id })`. A subset of
100
100
  * {@link ServerReadOptions} — `where`/`limit`/`orderBy` are fixed by the id. */
101
101
  export type ServerRetrieveOptions = Pick<ServerReadOptions<unknown>, 'type' | 'expand'>;
102
+ /**
103
+ * A claimable part of a row: one of the model's own fields, an app-defined name
104
+ * marked with {@link part}, or any other string.
105
+ *
106
+ * The third arm is the one that matters, and it is not a TypeScript
107
+ * limitation. Function parameters are contravariant, so a claim accepting
108
+ * `'title' | 'status'` is not assignable to one accepting `string` — and the
109
+ * legacy `useAblo<R>()` path erases a concrete schema to `SchemaRecord`
110
+ * (`AbloProvider.tsx`: `engine as Ablo<SchemaRecord>`) and restores it, which
111
+ * is exactly that assignment. While that path exists this union must stay open,
112
+ * and `fields: ['titel']` therefore compiles, is granted, excludes nobody, and
113
+ * leaves the write of `title` unguarded — the conflict rule compares names as
114
+ * opaque strings, so an invented one matches no other claim and nothing reports
115
+ * it.
116
+ *
117
+ * The replacement already exists: `createAbloReact(schema)` creates its context
118
+ * after the schema is known, so nothing is erased and nothing is restored — the
119
+ * same shape as `createTRPCReact<AppRouter>()`. What remains is retiring the
120
+ * generic provider in favour of it; the callers are enumerated in
121
+ * docs/plans/typed-react-binding.md. When they have moved, this arm goes, and
122
+ * `fields: ['titel']` stops compiling with no change at any call site.
123
+ */
124
+ export type ClaimField<T> = Extract<keyof T, string> | ClaimPart | (string & {});
125
+ /**
126
+ * The options on a claim, in four axes — each answers one question, and no
127
+ * member sits in two:
128
+ *
129
+ * - **what you claim** — `field` / `fields` / `path` / `range`, the target
130
+ * narrowed below the row;
131
+ * - **what others see** — `description` / `meta`, the presence half;
132
+ * - **how you wait** — `queue` / `maxQueueDepth` / `waitTimeoutMs` /
133
+ * `signal`, admission to the line;
134
+ * - **how long you hold** — `ttl` / `heartbeat`, the lease.
135
+ */
102
136
  export interface ClaimTargetOptions<T = Record<string, unknown>> {
103
- /** Peer-visible description of the work being performed — the sentence a
104
- * contending participant reads to decide whether to wait, work elsewhere, or
105
- * move on. Defaults to `'editing'`. The same field on every claim surface. */
106
- description?: string;
107
- /** Field-level target, for fine-grained claimed-state badges. */
108
- field?: string;
109
137
  /**
110
- * Several named parts of the row at once. Claims conflict where their sets
111
- * intersect, so two holders on disjoint parts do not wait for each other.
112
- * Prefer this to packing names into `field`, which compares as one opaque
113
- * string and lets overlapping sets both be granted.
138
+ * @deprecated Say it as a set: `fields: ['title']`. Removed in 0.37.0.
139
+ *
140
+ * One member for a set of one, beside another for a set of any size, is two
141
+ * ways to say one thing — and the singular is the one that misleads. A caller
142
+ * needing two parts reached for the member named `field` and packed
143
+ * `'b_3,b_7'` into it, which compares as a single unrelated name, so both
144
+ * writers were granted the same part and one update was lost. That spelling
145
+ * is refused now, and this member is going with it.
146
+ *
147
+ * The wire keeps reading `field` — it is frozen, and an older client emits it
148
+ * — so this is a change of what you write, not of what is understood.
149
+ */
150
+ field?: ClaimField<T>;
151
+ /**
152
+ * Narrow the claim to named parts of the row — one or several.
153
+ *
154
+ * Exclusion follows the target: claims on the same row conflict only where
155
+ * their sets intersect, so a holder on `['title']` and a holder on
156
+ * `['status']` proceed concurrently, while a whole-row claim (no target)
157
+ * conflicts with both. The per-field claimed-state badge is a consequence of
158
+ * the narrower lease, not its purpose.
159
+ */
160
+ fields?: readonly ClaimField<T>[];
161
+ /**
162
+ * @deprecated Claim the field the position lives in — `fields: ['content']`.
163
+ * Removed in 0.37.0.
164
+ *
165
+ * A claim may not be finer than the smallest thing the write path can
166
+ * address, and today that is a field: nothing writes part of a value. Two
167
+ * holders of `/content/3` and `/content/7` therefore contend, because either
168
+ * one committing takes the whole `content` field — so naming the position
169
+ * bought exclusion it could not deliver, and this member read as a peer of
170
+ * `fields` while being nothing of the kind.
171
+ *
172
+ * For a UI that draws a rail per block, put the block id in `meta`: that is
173
+ * the open bag for describing your work to peers, it renders exactly as well,
174
+ * and it promises nothing about exclusion. The wire keeps parsing `path`, so
175
+ * an older client is unaffected.
114
176
  */
115
- fields?: readonly string[];
116
- /** Optional path for document/file-like targets. */
117
177
  path?: string;
118
- /** Optional range for document/file-like targets. */
178
+ /**
179
+ * @deprecated Claim the field the span lives in — `fields: ['content']`.
180
+ * Removed in 0.37.0.
181
+ *
182
+ * Same rule as {@link path}: a span of a value is not separately writable, so
183
+ * two disjoint spans of one field contend. Unlike `path` this one has a
184
+ * future — a mergeable field makes an operation the write unit, at which
185
+ * point a span inside it is addressable and this becomes enforceable. It
186
+ * returns then, for code rather than prose, and it returns working.
187
+ */
119
188
  range?: TargetRange;
189
+ /** Peer-visible description of the work being performed — the sentence a
190
+ * contending participant reads to decide whether to wait, work elsewhere, or
191
+ * move on. Defaults to `'editing'`. The same field on every claim surface. */
192
+ description?: string;
120
193
  /**
121
194
  * App-defined structured metadata, carried verbatim to every participant
122
195
  * that observes the claim. Declare its shape once, on `Register`'s
@@ -124,8 +197,6 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
124
197
  * find — the write side and the read side are the same declaration.
125
198
  */
126
199
  meta?: ResolveClaimMeta;
127
- /** Crash-cleanup TTL — the claim auto-releases if the holder dies. */
128
- ttl?: Duration;
129
200
  /**
130
201
  * Behavior under contention. `true` (the default) queues behind the current
131
202
  * holder and resolves once the row is yours. `false` is fail-fast: if another
@@ -133,10 +204,6 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
133
204
  * {@link AbloClaimedError} instead of waiting. Use `false` to deduplicate
134
205
  * distributed work ("if someone else has this job, skip it"), where waiting
135
206
  * would mean double-processing.
136
- *
137
- * The high-level typed claim defaults this on because it serializes writers;
138
- * the low-level lease and the HTTP client default it off, since they resolve
139
- * immediately and cannot transparently wait for a grant.
140
207
  */
141
208
  queue?: boolean;
142
209
  /**
@@ -146,31 +213,36 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
146
213
  * Omit to wait however deep the queue is.
147
214
  */
148
215
  maxQueueDepth?: number;
216
+ /**
217
+ * Cap on how long a queued claim waits for its grant before rejecting with
218
+ * {@link AbloClaimedError} (`grant_timeout`). Omit to wait as long as the
219
+ * line takes. Same meaning on both transports; on the stateless HTTP client
220
+ * a timed-out wait also leaves the line, so the slot is not left to expire.
221
+ */
222
+ waitTimeoutMs?: number;
223
+ /**
224
+ * Abort a pending wait from outside — the same signal that cancels
225
+ * everything else in the program, so a cancelled agent task or an unmounted
226
+ * component takes its queued claim with it. Rejects with
227
+ * {@link AbloClaimedError} (`claim_wait_aborted`); over HTTP the abort also
228
+ * leaves the line. Ignored once the grant has arrived — a held lease is
229
+ * never torn down by a late abort; release it instead.
230
+ */
231
+ signal?: AbortSignal;
232
+ /** Crash-cleanup TTL — the claim auto-releases if the holder dies. */
233
+ ttl?: Duration;
149
234
  /**
150
235
  * Keep the lease alive for the duration of real work by beating on a
151
236
  * cadence — the pattern for background workers whose task outlives the
152
237
  * crash-cleanup TTL. `true` beats every third of the TTL (so two beats can
153
238
  * fail before the lease is at risk, and a crashed worker's lease still
154
239
  * lapses within one beat window); a duration such as `'2m'` sets the
155
- * cadence explicitly. The loop stops on release. A beat answered with a
156
- * definitive loss stops the loop and calls {@link onHeartbeatLost}; you can
157
- * also beat manually with `held.heartbeat()`.
158
- */
159
- heartbeat?: true | Duration;
160
- /**
161
- * Called once if the auto-heartbeat learns the lease is no longer yours
162
- * (expired and possibly granted onward). The loop has already stopped;
163
- * abandon the work or re-claim. Any write attempted under the old lease is
164
- * independently rejected by its `readAt` guard.
165
- */
166
- onHeartbeatLost?: (error: AbloClaimedError) => void;
167
- /**
168
- * Called after every successful beat (manual or auto) with the server's
169
- * answer — chiefly `queueDepth`, the number of participants waiting in
170
- * line behind this lease. A worker that can checkpoint may read pressure
171
- * here and release early when others wait.
240
+ * cadence explicitly; the structured {@link ClaimHeartbeatPlan} carries the
241
+ * cadence and both callbacks in one place
242
+ * `heartbeat: { every: '2m', onBeat, onLost }`. The loop stops on release.
243
+ * You can also beat manually with `held.heartbeat()`.
172
244
  */
173
- onHeartbeat?(beat: ClaimHeartbeat): void;
245
+ heartbeat?: true | Duration | ClaimHeartbeatPlan;
174
246
  }
175
247
  /** Options for `claim({ id, ... })`. */
176
248
  export interface ClaimParams<T = Record<string, unknown>> extends ClaimTargetOptions<T> {
@@ -178,7 +250,10 @@ export interface ClaimParams<T = Record<string, unknown>> extends ClaimTargetOpt
178
250
  }
179
251
  export interface ClaimLookupParams<T = Record<string, unknown>> {
180
252
  readonly id: string;
181
- readonly field?: string;
253
+ /** @deprecated Say it as a set: `fields: ['title']`. Removed in 0.37.0. */
254
+ readonly field?: ClaimField<T>;
255
+ /** Read the claim state of named parts of the row rather than the row. */
256
+ readonly fields?: readonly ClaimField<T>[];
182
257
  }
183
258
  export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLookupParams<T> {
184
259
  readonly order: readonly Claim[];
@@ -251,6 +326,24 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
251
326
  * carrying more than one meta shape occasionally needs.
252
327
  */
253
328
  state<M = ResolveClaimMeta>(params: ClaimLookupParams<T>): Claim<Record<string, unknown>, M> | null;
329
+ /**
330
+ * Every holder of a row, not just one.
331
+ *
332
+ * A claim that names a narrower target — a `path`, a `range`, a `field` or
333
+ * `fields` — excludes only what it overlaps, so several participants hold
334
+ * disjoint parts of one row at the same time and
335
+ * {@link ClaimReadApi.state} answers with one of them. This is the read
336
+ * behind a per-region UI: a rail for each claimed block, a chip for each
337
+ * participant. Synchronous and reactive off the same snapshot as `state`,
338
+ * so a render reads it inline.
339
+ *
340
+ * Own claim first when this client holds one, then peers. Takes the same
341
+ * `meta` parameter as {@link ClaimReadApi.state}.
342
+ */
343
+ list<M = ResolveClaimMeta>(params: ClaimLookupParams<T>): {
344
+ readonly object: 'list';
345
+ readonly data: readonly Claim<Record<string, unknown>, M>[];
346
+ };
254
347
  /**
255
348
  * FIFO wait line behind the current holder. Advanced: useful for operator
256
349
  * UIs and schedulers. Takes the same `meta` parameter as
@@ -274,6 +367,17 @@ export interface ClaimReadApi<T = Record<string, unknown>> {
274
367
  */
275
368
  export type AwaitedClaimMethod<F> = F extends (...args: infer A) => infer R ? R extends Promise<unknown> ? (...args: A) => R : (...args: A) => Promise<R> : F;
276
369
  export interface ClaimApi<T> extends ClaimReadApi<T> {
370
+ /**
371
+ * The try-claim: `queue: false` treats a held target as an expected outcome,
372
+ * not an error — it resolves `null`, so claim-or-skip dedup reads
373
+ * `if (!claim) return` with no try/catch. Who holds it, and why, stays
374
+ * readable through `claim.state({ id })`. (A write to a row someone else
375
+ * holds still rejects with `entity_claimed` — a failed write is an error;
376
+ * a declined try is not.)
377
+ */
378
+ (params: ClaimParams<T> & {
379
+ queue: false;
380
+ }): Promise<HeldClaim<T> | null>;
277
381
  /**
278
382
  * Takes a claim and returns an explicit held-work handle — a {@link HeldClaim}.
279
383
  * `data`, `release`, `revoke`, and the async disposer are always present (this
@@ -281,6 +385,10 @@ export interface ClaimApi<T> extends ClaimReadApi<T> {
281
385
  * directly and `await using` works without a guard.
282
386
  */
283
387
  (params: ClaimParams<T>): Promise<HeldClaim<T>>;
388
+ /** The row-free try-claim — `null` when the key is already held. */
389
+ (id: string, opts: ClaimOptions<T> & {
390
+ queue: false;
391
+ }): Promise<HeldLease | null>;
284
392
  /**
285
393
  * Takes a claim by id alone, for a row that lives only in the customer's own
286
394
  * database — Ablo has never seen it, so there is nothing to re-read. Returns a
@@ -55,6 +55,22 @@ export declare const WHERE_SCALAR_OPS: ReadonlySet<WhereOp>;
55
55
  export declare const WHERE_ARRAY_OPS: ReadonlySet<WhereOp>;
56
56
  export declare const WHERE_NULL_OPS: ReadonlySet<WhereOp>;
57
57
  export declare const WHERE_LIKE_OPS: ReadonlySet<WhereOp>;
58
+ /**
59
+ * Does this operator accept this operand? The rule the sets above imply, stated
60
+ * once and thrown once.
61
+ *
62
+ * The sets were collapsed here because two copies of the split let one plane
63
+ * accept a request the other refused. The check that consumes them stayed
64
+ * duplicated — the SQL compiler and the log-plane evaluator each carried their
65
+ * own arity tests and their own wording — which leaves the same gap one step
66
+ * further along: relax `IN` in one evaluator and a request succeeds against a
67
+ * hosted plane and fails against a connected one, with no test in a position to
68
+ * notice.
69
+ *
70
+ * Returns the classification the caller needs next, so the check and the branch
71
+ * are the same statement rather than two that can disagree.
72
+ */
73
+ export declare function classifyWhereOperand(op: WhereOp, value: unknown): 'null' | 'array' | 'scalar';
58
74
  /**
59
75
  * A single condition. Two supported shapes:
60
76
  *
@@ -13,6 +13,7 @@
13
13
  * so they live with the settlement core rather than the reactive consumer.
14
14
  */
15
15
  import { z } from 'zod';
16
+ import { AbloValidationError } from '../errors.js';
16
17
  /**
17
18
  * The comparison operators a {@link WhereClause} may use: equality and
18
19
  * inequality, ordering, set membership (`IN` / `NOT IN`), null checks
@@ -68,3 +69,47 @@ export const WHERE_LIKE_OPS = new Set([
68
69
  'ILIKE',
69
70
  'NOT ILIKE',
70
71
  ]);
72
+ /**
73
+ * Does this operator accept this operand? The rule the sets above imply, stated
74
+ * once and thrown once.
75
+ *
76
+ * The sets were collapsed here because two copies of the split let one plane
77
+ * accept a request the other refused. The check that consumes them stayed
78
+ * duplicated — the SQL compiler and the log-plane evaluator each carried their
79
+ * own arity tests and their own wording — which leaves the same gap one step
80
+ * further along: relax `IN` in one evaluator and a request succeeds against a
81
+ * hosted plane and fails against a connected one, with no test in a position to
82
+ * notice.
83
+ *
84
+ * Returns the classification the caller needs next, so the check and the branch
85
+ * are the same statement rather than two that can disagree.
86
+ */
87
+ export function classifyWhereOperand(op, value) {
88
+ if (WHERE_NULL_OPS.has(op)) {
89
+ if (value !== null) {
90
+ throw new AbloValidationError(`${op} only supports null RHS`, {
91
+ code: 'query_unsupported_operator',
92
+ });
93
+ }
94
+ return 'null';
95
+ }
96
+ if (WHERE_ARRAY_OPS.has(op)) {
97
+ if (!Array.isArray(value)) {
98
+ throw new AbloValidationError(`${op} requires an array RHS`, {
99
+ code: 'query_unsupported_operator',
100
+ });
101
+ }
102
+ return 'array';
103
+ }
104
+ if (WHERE_SCALAR_OPS.has(op)) {
105
+ if (Array.isArray(value)) {
106
+ throw new AbloValidationError(`${op} requires a scalar RHS`, {
107
+ code: 'query_unsupported_operator',
108
+ });
109
+ }
110
+ return 'scalar';
111
+ }
112
+ throw new AbloValidationError(`unsupported operator: ${op}`, {
113
+ code: 'query_unsupported_operator',
114
+ });
115
+ }
@@ -24,24 +24,18 @@
24
24
  * });
25
25
  */
26
26
  import { z } from 'zod';
27
- /** The sync-engine metadata describing one field, available at runtime through a
28
- * model's `fields` map. The {@link field} builders attach it, and the migration
29
- * planner, type generator, and OpenAPI generator all read it. */
30
- export interface FieldMeta {
31
- /** Sync-engine type tag, which maps to storage and serialization hints. */
32
- type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'json';
33
- /** Whether the field was marked optional via `.optional()` or `.nullable()`. */
34
- isOptional: boolean;
35
- /** Whether the field was marked indexed via `.indexed()`. */
36
- isIndexed: boolean;
37
- /**
38
- * Physical database column name override. When absent, SQL layers derive
39
- * the column from the field name using the active casing convention.
40
- */
41
- column?: string;
42
- /** For enums: the allowed values. */
43
- enumValues?: readonly string[];
44
- }
27
+ /**
28
+ * The sync-engine metadata describing one field, available at runtime through a
29
+ * model's `fields` map. The {@link field} builders attach it, and the migration
30
+ * planner, type generator, and OpenAPI generator all read it.
31
+ *
32
+ * Declared in `wire/modelShape.ts` and inferred here, not restated: this record
33
+ * crosses the wire twice serialized into the pushed artifact, and reported
34
+ * back by `GET /api/schema` — so a second declaration would be a copy that
35
+ * drifts in whichever direction nobody is looking.
36
+ */
37
+ export type { FieldMeta } from '../wire/modelShape.js';
38
+ import type { FieldMeta } from '../wire/modelShape.js';
45
39
  /**
46
40
  * Extract FieldMeta from a Zod schema. Returns null if no sync-engine
47
41
  * metadata is attached (e.g., raw `z.string()` usage).
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A reference to one field of one model — the field as a value rather than as
3
+ * a quoted name.
4
+ *
5
+ * A model is declared as Zod schemas keyed by name, so a field's name lives on
6
+ * the object key and never on the value: `model({ status: z.enum([...]) })`
7
+ * gives you nothing to point at, and every surface that needs to name a field
8
+ * has had to quote it. `fields: ['titel']` then compiles, is granted, excludes
9
+ * nobody, and leaves the write of `title` unguarded — the conflict rule
10
+ * compares names as opaque strings, so an invented one matches no other claim
11
+ * and nothing reports it.
12
+ *
13
+ * {@link Schema.fields} carries one of these per field, stamped from the key
14
+ * `defineSchema` is already holding. Referencing a field that does not exist
15
+ * stops compiling, and renaming one is a compile error at every use rather than
16
+ * a claim that quietly stops matching.
17
+ *
18
+ * An object rather than a branded string, for the reason `ClaimPart` is one: a
19
+ * brand makes a concrete schema's claim params mutually unassignable with the
20
+ * erased `SchemaRecord` view, and the react context boundary erases and
21
+ * restores exactly that way. Objects stay pairwise comparable across it, which
22
+ * a union of literal keys does not — function parameters are contravariant, so
23
+ * a claim accepting `'title' | 'status'` is not assignable to one accepting
24
+ * `string`.
25
+ *
26
+ * `model` rides along so a reference carries where it came from. Claiming
27
+ * `users.email` through `ablo.tasks` is a mistake nothing can currently see.
28
+ */
29
+ export interface FieldRef {
30
+ /** The schema key of the model this field belongs to. */
31
+ readonly model: string;
32
+ /** The field's own name — the key it was declared under. */
33
+ readonly field: string;
34
+ }
35
+ /** Whether a value is a {@link FieldRef}. */
36
+ export declare function isFieldRef(value: unknown): value is FieldRef;
37
+ /** Build the reference for one declared field. */
38
+ export declare function fieldRef(model: string, field: string): FieldRef;
@@ -0,0 +1,11 @@
1
+ /** Whether a value is a {@link FieldRef}. */
2
+ export function isFieldRef(value) {
3
+ return (typeof value === 'object' &&
4
+ value !== null &&
5
+ typeof value.field === 'string' &&
6
+ typeof value.model === 'string');
7
+ }
8
+ /** Build the reference for one declared field. */
9
+ export function fieldRef(model, field) {
10
+ return { model, field };
11
+ }
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Two OpenAPI 3.1 documents describing the same API, for two different readers.
3
3
  *
4
- * The server registers ONE parameterised route family — `/api/v1/models/:model`,
5
- * `/:id`, `/:id/claim`, `/claim/heartbeat`, `/claim/reorder`, plus `/v1/commits`.
6
- * Six routes, whatever a tenant's schema contains. Authentication is a single
7
- * Bearer scheme, your API key.
4
+ * The server registers ONE parameterised route family — `/api/v1/models/:model`
5
+ * and what hangs below it, plus the coordination, credential and commit
6
+ * resources. The count does not move when a tenant's schema does.
7
+ * Authentication is a single Bearer scheme, your API key.
8
8
  *
9
- * {@link abloOpenApi} publishes exactly those six. It takes no schema, so it
10
- * cannot grow with one: identical for every caller, stable across every push,
11
- * and the document a Python or Go client is generated from once.
9
+ * {@link abloOpenApi} publishes that family. It takes no schema, so it cannot
10
+ * grow with one: identical for every caller, stable across every push, and the
11
+ * document a Python or Go client is generated from once. `spec-covers-routes`
12
+ * holds it to the served surface in both directions, so this is the list of
13
+ * routes rather than a description of it.
12
14
  *
13
- * {@link schemaToOpenApi} expands the same six into one set per model, with
15
+ * {@link schemaToOpenApi} expands the model half into one set per model, with
14
16
  * payloads typed from each model's introspectable {@link FieldMeta}. Five paths
15
17
  * per model, regenerated on every push — worth it when you want generated types
16
18
  * for one schema, and the wrong thing to hand an agent.
@@ -30,15 +32,14 @@ export interface SchemaToOpenApiOptions {
30
32
  }
31
33
  type Json = Record<string, unknown>;
32
34
  /**
33
- * The protocol reference: the five route templates the server actually serves,
34
- * plus `/v1/commits`.
35
+ * The protocol reference: the route templates the server actually serves.
35
36
  *
36
37
  * This takes no schema, and that is the point. The server registers one
37
- * parameterised route family (`/api/v1/models/:model/...`), so the API is five
38
- * routes no matter how many models a tenant defines — and a spec that cannot see
39
- * a schema cannot grow with one. It is publishable once, identical for every
40
- * caller, and stable across every schema push: the document a Python or Go
41
- * client is generated from, and the surface an agent is handed.
38
+ * parameterised route family (`/api/v1/models/:model/...`), so the surface is
39
+ * the same size no matter how many models a tenant defines — and a spec that
40
+ * cannot see a schema cannot grow with one. It is publishable once, identical
41
+ * for every caller, and stable across every schema push: the document a Python
42
+ * or Go client is generated from, and the surface an agent is handed.
42
43
  *
43
44
  * Payload shapes are generic here by design. A caller that wants them typed
44
45
  * either reads the schema at runtime or generates the per-tenant expansion with