@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.
- package/CHANGELOG.md +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
|
@@ -6,9 +6,12 @@ import { ABLO_HOSTED_HTTP_BASE_URL } from '../auth/hostedEndpoints.js';
|
|
|
6
6
|
// enforced one.
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
import { commitRequestSchema, commitReceiptSchema } from '../wire/commit.js';
|
|
9
|
-
import { claimRequestSchema, claimHeartbeatRequestSchema, listQuerySchema, claimStateSchema, claimAcquiredResponseSchema, claimQueuedResponseSchema, claimHeartbeatReplySchema, } from '../wire/claims.js';
|
|
9
|
+
import { claimRequestSchema, claimHeartbeatRequestSchema, listQuerySchema, claimStateSchema, claimAcquiredResponseSchema, claimQueuedResponseSchema, claimHeartbeatReplySchema, claimHeartbeatBatchReplySchema, claimListQuerySchema, claimListResponseSchema, claimReorderRequestSchema, claimReorderReplySchema, claimReleaseReplySchema, } from '../wire/claims.js';
|
|
10
10
|
import { modelReadResponseSchema, modelListResponseSchema } from '../wire/modelResponses.js';
|
|
11
|
-
import {
|
|
11
|
+
import { modelMutationRequestSchema } from '../wire/modelMutations.js';
|
|
12
|
+
import { logListResponseSchema, logQuerySchema } from '../wire/feedEvent.js';
|
|
13
|
+
import { schemaReadResponseSchema } from '../wire/accountResponses.js';
|
|
14
|
+
import { ephemeralKeyRequestSchema, capabilityRequestSchema, capabilityMintResponseSchema, } from '../wire/auth.js';
|
|
12
15
|
import { EphemeralKeyResponseSchema } from '../auth/schemas.js';
|
|
13
16
|
function fieldSchema(f) {
|
|
14
17
|
switch (f.type) {
|
|
@@ -61,6 +64,16 @@ const modelParam = () => ({
|
|
|
61
64
|
description: 'A model name from your pushed schema, e.g. `task`.',
|
|
62
65
|
});
|
|
63
66
|
const genericRow = () => ({ type: 'object', additionalProperties: true });
|
|
67
|
+
/**
|
|
68
|
+
* The body of a model-scoped write, derived from the schema the server
|
|
69
|
+
* validates it against.
|
|
70
|
+
*
|
|
71
|
+
* It was `genericRow()` — "an object" — which described the record flat and
|
|
72
|
+
* omitted the envelope entirely. A client built from that document sent the
|
|
73
|
+
* row's fields at the top level, where the server looks for `data`, and every
|
|
74
|
+
* one of them went missing at once.
|
|
75
|
+
*/
|
|
76
|
+
const mutationBody = () => jsonBody(derive(modelMutationRequestSchema, 'input'));
|
|
64
77
|
/**
|
|
65
78
|
* Fill in `data` on a derived model-response schema.
|
|
66
79
|
*
|
|
@@ -81,11 +94,30 @@ function withGenericRows(derived) {
|
|
|
81
94
|
: genericRow();
|
|
82
95
|
return { ...derived, properties };
|
|
83
96
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Query parameters derived from the schema the route reads them with.
|
|
99
|
+
*
|
|
100
|
+
* Taking the schema as an argument rather than closing over one is what lets the
|
|
101
|
+
* claim listing publish its filters too: `GET /v1/claims` accepts six, and a
|
|
102
|
+
* hand-written parameter list beside them would be the drifting copy this file
|
|
103
|
+
* exists to avoid.
|
|
104
|
+
*/
|
|
105
|
+
function queryParams(schema) {
|
|
106
|
+
const props = (derive(schema, 'input').properties ?? {});
|
|
107
|
+
return Object.entries(props).map(([name, s]) => ({ name, in: 'query', schema: s }));
|
|
88
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Both release routes answer in one shape. `released` distinguishes "this call
|
|
111
|
+
* ended your lease" from "there was nothing of yours to end" — both success,
|
|
112
|
+
* and a retry after a lost response deserves to know which it got.
|
|
113
|
+
*/
|
|
114
|
+
const releaseResp = () => jsonResp('Released', derive(claimReleaseReplySchema, 'output'));
|
|
115
|
+
const claimIdParam = () => ({
|
|
116
|
+
name: 'claimId',
|
|
117
|
+
in: 'path',
|
|
118
|
+
required: true,
|
|
119
|
+
schema: { type: 'string' },
|
|
120
|
+
});
|
|
89
121
|
/**
|
|
90
122
|
* The `POST /v1/commits` body, derived from {@link commitRequestSchema} — the
|
|
91
123
|
* same schema the server validates the request against.
|
|
@@ -115,15 +147,14 @@ function envelope(options, description, paths, schemas) {
|
|
|
115
147
|
};
|
|
116
148
|
}
|
|
117
149
|
/**
|
|
118
|
-
* The protocol reference: the
|
|
119
|
-
* plus `/v1/commits`.
|
|
150
|
+
* The protocol reference: the route templates the server actually serves.
|
|
120
151
|
*
|
|
121
152
|
* This takes no schema, and that is the point. The server registers one
|
|
122
|
-
* parameterised route family (`/api/v1/models/:model/...`), so the
|
|
123
|
-
*
|
|
124
|
-
* a schema cannot grow with one. It is publishable once, identical
|
|
125
|
-
* caller, and stable across every schema push: the document a Python
|
|
126
|
-
* client is generated from, and the surface an agent is handed.
|
|
153
|
+
* parameterised route family (`/api/v1/models/:model/...`), so the surface is
|
|
154
|
+
* the same size no matter how many models a tenant defines — and a spec that
|
|
155
|
+
* cannot see a schema cannot grow with one. It is publishable once, identical
|
|
156
|
+
* for every caller, and stable across every schema push: the document a Python
|
|
157
|
+
* or Go client is generated from, and the surface an agent is handed.
|
|
127
158
|
*
|
|
128
159
|
* Payload shapes are generic here by design. A caller that wants them typed
|
|
129
160
|
* either reads the schema at runtime or generates the per-tenant expansion with
|
|
@@ -131,25 +162,46 @@ function envelope(options, description, paths, schemas) {
|
|
|
131
162
|
*/
|
|
132
163
|
export function abloOpenApi(options = {}) {
|
|
133
164
|
const rowResp = jsonResp('The row, with the watermark it was read at and who holds it.', withGenericRows(derive(modelReadResponseSchema, 'output')));
|
|
134
|
-
const writeBody = jsonBody(genericRow());
|
|
135
165
|
const tags = ['models'];
|
|
136
166
|
const paths = {
|
|
137
167
|
'/v1/models/{model}': {
|
|
138
168
|
get: {
|
|
139
169
|
tags,
|
|
140
170
|
summary: 'List rows of a model',
|
|
141
|
-
parameters: [modelParam(), ...
|
|
171
|
+
parameters: [modelParam(), ...queryParams(listQuerySchema)],
|
|
142
172
|
responses: {
|
|
143
173
|
'200': jsonResp('A page of rows. `next_cursor` feeds `starting_after` on the next ' +
|
|
144
174
|
'call; `stamp` is the watermark the page was read at.', withGenericRows(derive(modelListResponseSchema, 'output'))),
|
|
145
175
|
},
|
|
146
176
|
},
|
|
147
|
-
post: {
|
|
177
|
+
post: {
|
|
178
|
+
tags,
|
|
179
|
+
summary: 'Create a row',
|
|
180
|
+
description: 'The record travels in `data`; `id` is yours to choose — see the ' +
|
|
181
|
+
'field for the derivation that makes a retry idempotent.',
|
|
182
|
+
parameters: [modelParam()],
|
|
183
|
+
requestBody: mutationBody(),
|
|
184
|
+
responses: { '200': commitReceipt() },
|
|
185
|
+
},
|
|
148
186
|
},
|
|
149
187
|
'/v1/models/{model}/{id}': {
|
|
150
188
|
get: { tags, summary: 'Retrieve a row', parameters: [modelParam(), idParam()], responses: { '200': rowResp } },
|
|
151
|
-
patch: {
|
|
152
|
-
|
|
189
|
+
patch: {
|
|
190
|
+
tags,
|
|
191
|
+
summary: 'Update a row',
|
|
192
|
+
description: 'Pass `claim` and `readAt` together: the lease says nobody else is ' +
|
|
193
|
+
'writing, the watermark says the row has not moved since you read it.',
|
|
194
|
+
parameters: [modelParam(), idParam()],
|
|
195
|
+
requestBody: mutationBody(),
|
|
196
|
+
responses: { '200': commitReceipt() },
|
|
197
|
+
},
|
|
198
|
+
delete: {
|
|
199
|
+
tags,
|
|
200
|
+
summary: 'Delete a row',
|
|
201
|
+
parameters: [modelParam(), idParam()],
|
|
202
|
+
requestBody: { ...mutationBody(), required: false },
|
|
203
|
+
responses: { '200': commitReceipt() },
|
|
204
|
+
},
|
|
153
205
|
},
|
|
154
206
|
'/v1/models/{model}/{id}/claim': {
|
|
155
207
|
post: {
|
|
@@ -165,7 +217,12 @@ export function abloOpenApi(options = {}) {
|
|
|
165
217
|
'`GET /v1/claims/{claimId}` for the grant.', derive(claimQueuedResponseSchema, 'output')),
|
|
166
218
|
},
|
|
167
219
|
},
|
|
168
|
-
delete: {
|
|
220
|
+
delete: {
|
|
221
|
+
tags: ['claims'],
|
|
222
|
+
summary: 'Release a claim',
|
|
223
|
+
parameters: [modelParam(), idParam()],
|
|
224
|
+
responses: { '200': releaseResp() },
|
|
225
|
+
},
|
|
169
226
|
},
|
|
170
227
|
'/v1/models/{model}/{id}/claim/heartbeat': {
|
|
171
228
|
post: {
|
|
@@ -179,7 +236,17 @@ export function abloOpenApi(options = {}) {
|
|
|
179
236
|
},
|
|
180
237
|
},
|
|
181
238
|
'/v1/models/{model}/{id}/claim/reorder': {
|
|
182
|
-
post: {
|
|
239
|
+
post: {
|
|
240
|
+
tags: ['claims'],
|
|
241
|
+
summary: 'Reorder the wait-line (privileged)',
|
|
242
|
+
description: 'Name the waiters you want at the front, in the order you want them. ' +
|
|
243
|
+
'Waiters you leave out keep their relative places behind them.',
|
|
244
|
+
parameters: [modelParam(), idParam()],
|
|
245
|
+
requestBody: optionalJsonBody(derive(claimReorderRequestSchema, 'input')),
|
|
246
|
+
responses: {
|
|
247
|
+
'200': jsonResp('Reordered', derive(claimReorderReplySchema, 'output')),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
183
250
|
},
|
|
184
251
|
'/v1/ephemeral_keys': {
|
|
185
252
|
post: {
|
|
@@ -194,6 +261,44 @@ export function abloOpenApi(options = {}) {
|
|
|
194
261
|
responses: { '200': jsonResp('The minted credential', derive(EphemeralKeyResponseSchema, 'output')) },
|
|
195
262
|
},
|
|
196
263
|
},
|
|
264
|
+
'/v1/claims': {
|
|
265
|
+
get: {
|
|
266
|
+
tags: ['claims'],
|
|
267
|
+
summary: 'List who holds what, and who waits',
|
|
268
|
+
description: 'The coordination view: scope to a row with `model` and `id`, to a ' +
|
|
269
|
+
'participant with `actorId`, `actorKind`, `onBehalfOfId` or ' +
|
|
270
|
+
'`capabilityId`, or combine them. `queue` is populated only when the ' +
|
|
271
|
+
'request names both `model` and `id` — a wait line belongs to one row.',
|
|
272
|
+
parameters: queryParams(claimListQuerySchema),
|
|
273
|
+
responses: {
|
|
274
|
+
'200': jsonResp('Live claims, and the wait line behind the named row.', derive(claimListResponseSchema, 'output')),
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
post: {
|
|
278
|
+
tags: ['claims'],
|
|
279
|
+
summary: 'Claim a row named in the body',
|
|
280
|
+
description: 'The same operation as `POST /v1/models/{model}/{id}/claim`, with the ' +
|
|
281
|
+
'row in `target` instead of the URL. Answers identically.',
|
|
282
|
+
requestBody: jsonBody(derive(claimRequestSchema, 'input')),
|
|
283
|
+
responses: {
|
|
284
|
+
'201': jsonResp('The lease is yours.', derive(claimAcquiredResponseSchema, 'output')),
|
|
285
|
+
'202': jsonResp('Already held, and you asked to queue. You are in line at `position`.', derive(claimQueuedResponseSchema, 'output')),
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
'/v1/claims/heartbeat': {
|
|
290
|
+
post: {
|
|
291
|
+
tags: ['claims'],
|
|
292
|
+
summary: 'Heartbeat every lease you hold, in one request',
|
|
293
|
+
description: 'One round trip per cadence for a worker holding many rows, instead of ' +
|
|
294
|
+
'one per row. Takes only `ttl`; the leases are whichever ones your ' +
|
|
295
|
+
'credential holds on this plane.',
|
|
296
|
+
requestBody: optionalJsonBody(derive(claimHeartbeatRequestSchema, 'input')),
|
|
297
|
+
responses: {
|
|
298
|
+
'200': jsonResp('One ack per lease extended.', derive(claimHeartbeatBatchReplySchema, 'output')),
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
},
|
|
197
302
|
'/v1/claims/{claimId}': {
|
|
198
303
|
get: {
|
|
199
304
|
tags: ['claims'],
|
|
@@ -201,7 +306,7 @@ export function abloOpenApi(options = {}) {
|
|
|
201
306
|
description: 'How a caller without a persistent connection learns its queued claim ' +
|
|
202
307
|
'was granted. `position` is advisory — a privileged reorder can move it ' +
|
|
203
308
|
'up — so branch on `status`, never on position.',
|
|
204
|
-
parameters: [
|
|
309
|
+
parameters: [claimIdParam()],
|
|
205
310
|
responses: { '200': jsonResp('The claim state', derive(claimStateSchema, 'output')) },
|
|
206
311
|
},
|
|
207
312
|
delete: {
|
|
@@ -210,8 +315,24 @@ export function abloOpenApi(options = {}) {
|
|
|
210
315
|
description: 'The same call for both: releasing a held lease and abandoning a queued ' +
|
|
211
316
|
'position are one operation, because a queue entry is a lease in a ' +
|
|
212
317
|
'different state.',
|
|
213
|
-
parameters: [
|
|
214
|
-
responses: { '200':
|
|
318
|
+
parameters: [claimIdParam()],
|
|
319
|
+
responses: { '200': releaseResp() },
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
'/v1/claims/{claimId}/heartbeat': {
|
|
323
|
+
post: {
|
|
324
|
+
tags: ['claims'],
|
|
325
|
+
summary: 'Heartbeat a claim by id — held or queued',
|
|
326
|
+
description: 'The beat a waiter needs: a queued caller holds nothing but the ' +
|
|
327
|
+
'`claimId` it was handed at enqueue, and an entry that stops beating ' +
|
|
328
|
+
'drops out of the line on TTL. The reply doubles as the wait poll — ' +
|
|
329
|
+
'`queued` means still in line, `held` means the grant landed, at which ' +
|
|
330
|
+
'point `GET /v1/claims/{claimId}` carries the fence token.',
|
|
331
|
+
parameters: [claimIdParam()],
|
|
332
|
+
requestBody: optionalJsonBody(derive(claimHeartbeatRequestSchema, 'input')),
|
|
333
|
+
responses: {
|
|
334
|
+
'200': jsonResp('Lease extended, or queued slot refreshed.', derive(claimHeartbeatReplySchema, 'output')),
|
|
335
|
+
},
|
|
215
336
|
},
|
|
216
337
|
},
|
|
217
338
|
'/v1/capabilities': {
|
|
@@ -221,7 +342,14 @@ export function abloOpenApi(options = {}) {
|
|
|
221
342
|
description: 'A scoped, revocable grant. Narrow by default: an agent or system ' +
|
|
222
343
|
'capability must name its `syncGroups` and `operations`.',
|
|
223
344
|
requestBody: jsonBody(derive(capabilityRequestSchema, 'input')),
|
|
224
|
-
responses: {
|
|
345
|
+
responses: {
|
|
346
|
+
// 201, not 200. The reference said 200 while the route has always
|
|
347
|
+
// answered 201, so a generated client checking the documented code
|
|
348
|
+
// failed on every successful mint.
|
|
349
|
+
'201': jsonResp('The minted capability. `token` is the credential — carry it as the ' +
|
|
350
|
+
'Bearer token on every other call. `scope` is what was minted, ' +
|
|
351
|
+
'which is not always what was asked for.', derive(capabilityMintResponseSchema, 'output')),
|
|
352
|
+
},
|
|
225
353
|
},
|
|
226
354
|
},
|
|
227
355
|
'/v1/capabilities/{id}': {
|
|
@@ -246,6 +374,39 @@ export function abloOpenApi(options = {}) {
|
|
|
246
374
|
responses: { '200': jsonResp('The rotated capability', { type: 'object' }) },
|
|
247
375
|
},
|
|
248
376
|
},
|
|
377
|
+
'/v1/schema': {
|
|
378
|
+
get: {
|
|
379
|
+
tags: ['schema'],
|
|
380
|
+
summary: 'What the models look like',
|
|
381
|
+
description: "The schema deployed on your credential's plane: every model with its " +
|
|
382
|
+
'fields, their types, and its relations. Read this when you have no ' +
|
|
383
|
+
'local schema declaration to read types from — it is what makes a ' +
|
|
384
|
+
'field typo a local check rather than a rejected write. Each model ' +
|
|
385
|
+
'carries a content `hash` that moves only when its shape does, so read ' +
|
|
386
|
+
'the shape once and poll the hashes after: a refetch only ever answers ' +
|
|
387
|
+
'a push.',
|
|
388
|
+
responses: {
|
|
389
|
+
'200': jsonResp('The deployed schema, or `active: false` when nothing is pushed.', derive(schemaReadResponseSchema, 'output')),
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
'/v1/logs': {
|
|
394
|
+
get: {
|
|
395
|
+
tags: ['logs'],
|
|
396
|
+
summary: 'Tail what changed',
|
|
397
|
+
description: 'How a caller without a socket learns what its peers did. Omit ' +
|
|
398
|
+
'`after` for the most recent entries, then copy each page\'s ' +
|
|
399
|
+
'`next_cursor` back as `after` to walk forward. Scope is taken from ' +
|
|
400
|
+
'your key — organization, plane and sync groups — so a caller cannot ' +
|
|
401
|
+
'widen what it sees by asking.',
|
|
402
|
+
parameters: queryParams(logQuerySchema),
|
|
403
|
+
responses: {
|
|
404
|
+
'200': jsonResp('A page of the feed, oldest first. Entries are discriminated on ' +
|
|
405
|
+
'`object`, so a reader that meets an entry kind it does not know ' +
|
|
406
|
+
'can skip it and keep paging.', derive(logListResponseSchema, 'output')),
|
|
407
|
+
},
|
|
408
|
+
},
|
|
409
|
+
},
|
|
249
410
|
'/v1/commits': {
|
|
250
411
|
post: {
|
|
251
412
|
tags: ['commits'],
|
|
@@ -290,7 +451,7 @@ export function schemaToOpenApi(schema, options = {}) {
|
|
|
290
451
|
get: {
|
|
291
452
|
tags: [key],
|
|
292
453
|
summary: `List ${key}`,
|
|
293
|
-
parameters:
|
|
454
|
+
parameters: queryParams(listQuerySchema),
|
|
294
455
|
responses: {
|
|
295
456
|
'200': jsonResp('List of rows', {
|
|
296
457
|
type: 'object',
|
|
@@ -96,7 +96,13 @@ export interface BelongsToOptions {
|
|
|
96
96
|
declare const __relationType: unique symbol;
|
|
97
97
|
declare const __relationTarget: unique symbol;
|
|
98
98
|
declare const __relationField: unique symbol;
|
|
99
|
-
|
|
99
|
+
/**
|
|
100
|
+
* The three edges the engine resolves. Inferred from `wire/modelShape.ts`
|
|
101
|
+
* rather than restated: the set is reported to callers by `GET /api/schema`,
|
|
102
|
+
* so it is protocol, and protocol has one definition.
|
|
103
|
+
*/
|
|
104
|
+
export type { RelationType } from '../wire/modelShape.js';
|
|
105
|
+
import type { RelationType } from '../wire/modelShape.js';
|
|
100
106
|
/**
|
|
101
107
|
* A relation definition, carrying its type information at both the type and
|
|
102
108
|
* runtime level.
|
|
@@ -196,4 +202,3 @@ export declare const relation: {
|
|
|
196
202
|
*/
|
|
197
203
|
readonly hasOne: <Target extends string, Field extends string>(target: Target, foreignKey: Field) => RelationDef<"hasOne", Target, Field>;
|
|
198
204
|
};
|
|
199
|
-
export {};
|
|
@@ -23,6 +23,7 @@ import { z } from 'zod';
|
|
|
23
23
|
import type { ModelDef, RelationRecord } from './model.js';
|
|
24
24
|
import type { RelationDef } from './relation.js';
|
|
25
25
|
import type { IdentityRole } from './roles.js';
|
|
26
|
+
import { type FieldRef } from './fieldRef.js';
|
|
26
27
|
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
|
|
27
28
|
/** The set of built-in casing conventions supported by `defineSchema`. */
|
|
28
29
|
export type CasingConvention = 'snake_case' | 'camelCase';
|
|
@@ -150,6 +151,23 @@ export interface BaseModelMethods {
|
|
|
150
151
|
export interface Schema<S extends SchemaRecord = SchemaRecord> {
|
|
151
152
|
/** The raw model definitions */
|
|
152
153
|
readonly models: S;
|
|
154
|
+
/**
|
|
155
|
+
* Every declared field as a {@link FieldRef} — the field as a value rather
|
|
156
|
+
* than a quoted name: `schema.fields.tasks.status`.
|
|
157
|
+
*
|
|
158
|
+
* A surface that names a field takes one of these instead of a string, so a
|
|
159
|
+
* name that does not exist stops compiling and a rename is a compile error at
|
|
160
|
+
* every use. Claims are the first caller; anything else that has to say
|
|
161
|
+
* "which field" should take a reference for the same reason.
|
|
162
|
+
*
|
|
163
|
+
* Base fields (`id`, `createdAt`, …) are not here: the schema does not
|
|
164
|
+
* declare them, and a caller naming one means the row, not a part of it.
|
|
165
|
+
*/
|
|
166
|
+
readonly fields: {
|
|
167
|
+
readonly [K in keyof S]: S[K] extends ModelDef<infer Shape> ? {
|
|
168
|
+
readonly [F in Extract<keyof Shape, string>]: FieldRef;
|
|
169
|
+
} : never;
|
|
170
|
+
};
|
|
153
171
|
/** Zod schemas with base fields merged in */
|
|
154
172
|
readonly validators: {
|
|
155
173
|
readonly [K in keyof S]: S[K] extends ModelDef<infer Shape> ? z.ZodObject<Shape & typeof baseFieldsSchema.shape> : never;
|
|
@@ -321,4 +339,13 @@ export type UpdateValue<S extends Schema, ModelName extends keyof S['models']> =
|
|
|
321
339
|
export interface DeleteId<S extends Schema, ModelName extends keyof S['models']> {
|
|
322
340
|
id: string;
|
|
323
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* One {@link FieldRef} per declared field, keyed by model then field.
|
|
344
|
+
*
|
|
345
|
+
* Every construction of a {@link Schema} goes through here — `defineSchema`,
|
|
346
|
+
* the `selectModels`/`omitModels` projections, and the deserializer — so a
|
|
347
|
+
* projection carries references for exactly the models it kept and a schema
|
|
348
|
+
* rebuilt from JSON carries them at all.
|
|
349
|
+
*/
|
|
350
|
+
export declare function buildFieldRefs(models: Record<string, ModelDef>): Record<string, Record<string, FieldRef>>;
|
|
324
351
|
export declare function defineSchema<const S extends SchemaRecord>(models: S, options?: DefineSchemaOptions): Schema<S>;
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { z } from 'zod';
|
|
23
23
|
import { AbloValidationError } from '../errors.js';
|
|
24
|
+
import { fieldRef } from './fieldRef.js';
|
|
24
25
|
import { scopeSchema, grantsRefSchema } from './roles.js';
|
|
25
26
|
// Sync-group roles (identity and entity) are defined in `./roles.js` and
|
|
26
27
|
// re-exported here so they can also be imported from this module. See
|
|
@@ -153,6 +154,24 @@ function assertRoundTrippableCamelCase(modelName, fieldName) {
|
|
|
153
154
|
}
|
|
154
155
|
}
|
|
155
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* One {@link FieldRef} per declared field, keyed by model then field.
|
|
159
|
+
*
|
|
160
|
+
* Every construction of a {@link Schema} goes through here — `defineSchema`,
|
|
161
|
+
* the `selectModels`/`omitModels` projections, and the deserializer — so a
|
|
162
|
+
* projection carries references for exactly the models it kept and a schema
|
|
163
|
+
* rebuilt from JSON carries them at all.
|
|
164
|
+
*/
|
|
165
|
+
export function buildFieldRefs(models) {
|
|
166
|
+
const refs = {};
|
|
167
|
+
for (const [model, def] of Object.entries(models)) {
|
|
168
|
+
const perField = {};
|
|
169
|
+
for (const field of Object.keys(def.shape))
|
|
170
|
+
perField[field] = fieldRef(model, field);
|
|
171
|
+
refs[model] = perField;
|
|
172
|
+
}
|
|
173
|
+
return refs;
|
|
174
|
+
}
|
|
156
175
|
export function defineSchema(models, options) {
|
|
157
176
|
// Build validators with base fields merged in, and resolve defaults for
|
|
158
177
|
// `typename` and `persist.store` so downstream code (the loader and the
|
|
@@ -228,6 +247,7 @@ export function defineSchema(models, options) {
|
|
|
228
247
|
// already part of ModelDef, so the shape is structurally unchanged.
|
|
229
248
|
models: resolvedModels,
|
|
230
249
|
validators: validators,
|
|
250
|
+
fields: buildFieldRefs(resolvedModels),
|
|
231
251
|
identityRoles: options?.identityRoles ?? [],
|
|
232
252
|
sessionSettings: options?.sessionSettings ?? {},
|
|
233
253
|
};
|
|
@@ -111,8 +111,8 @@ export declare const durableCommitEnvelopeSchema: z.ZodObject<{
|
|
|
111
111
|
}>>;
|
|
112
112
|
}, z.core.$strip>]>>>>;
|
|
113
113
|
track: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
114
|
-
model: z.ZodString;
|
|
115
114
|
id: z.ZodString;
|
|
115
|
+
model: z.ZodString;
|
|
116
116
|
readAt: z.ZodOptional<z.ZodNumber>;
|
|
117
117
|
}, z.core.$strip>, z.ZodObject<{
|
|
118
118
|
group: z.ZodString;
|
|
@@ -67,8 +67,8 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
67
67
|
}>>;
|
|
68
68
|
}, z.core.$strip>]>>>>;
|
|
69
69
|
track: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
70
|
-
model: z.ZodString;
|
|
71
70
|
id: z.ZodString;
|
|
71
|
+
model: z.ZodString;
|
|
72
72
|
readAt: z.ZodOptional<z.ZodNumber>;
|
|
73
73
|
}, z.core.$strip>, z.ZodObject<{
|
|
74
74
|
group: z.ZodString;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HttpClientConfig } from './httpOptions.js';
|
|
2
|
-
import type { CommitResource, HttpClaimApi, ModelReadOptions, CreateSessionParams, AbloSession } from '../resources/httpResources.js';
|
|
2
|
+
import type { CommitResource, HttpClaimApi, HttpClaimsResource, ModelReadOptions, CreateSessionParams, AbloSession } from '../resources/httpResources.js';
|
|
3
3
|
import type { ModelCreateParams, ModelDeleteParams, ServerReadOptions, ModelRetrieveParams, ModelTrackParams, ModelTrackResult, ModelUpdateParams } from '../resources/modelOperations.js';
|
|
4
4
|
import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
|
|
5
5
|
import type { ModelUpdater, ContentionOptions } from '../resources/functionalUpdate.js';
|
|
@@ -108,6 +108,14 @@ export type AbloHttpClient<S extends SchemaRecord> = {
|
|
|
108
108
|
/** Replays every pending durable HTTP write in seal order and waits for settlement. */
|
|
109
109
|
waitForFlush(): Promise<void>;
|
|
110
110
|
readonly commits: CommitResource;
|
|
111
|
+
/**
|
|
112
|
+
* Claim-ticket operations keyed by `claimId`: `retrieve` polls a queued
|
|
113
|
+
* ticket to its grant, `heartbeat` keeps one lease (held or queued) alive,
|
|
114
|
+
* `heartbeatAll` beats every lease this identity holds in one round trip.
|
|
115
|
+
* The id comes from `AbloClaimedError('claim_queued')`, which carries it on
|
|
116
|
+
* `error.claims`.
|
|
117
|
+
*/
|
|
118
|
+
readonly claims: HttpClaimsResource;
|
|
111
119
|
dispose(): Promise<void>;
|
|
112
120
|
/** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
|
|
113
121
|
getAuthToken(): Promise<string | null>;
|