@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
|
@@ -12,10 +12,10 @@ import { reconcileFunctionalUpdate, } from '../resources/functionalUpdate.js';
|
|
|
12
12
|
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, rejectRemovedDatabaseUrlOption, warnIfCliKeyMismatch, } from '../auth/apiKey.js';
|
|
13
13
|
import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
|
|
14
14
|
import { commitReceiptSchema } from '../wire/commit.js';
|
|
15
|
-
import { claimAcquireResponseSchema, claimHeartbeatReplySchema, claimListResponseSchema, } from '../wire/claims.js';
|
|
15
|
+
import { claimAcquireResponseSchema, claimHeartbeatBatchReplySchema, claimHeartbeatReplySchema, claimListResponseSchema, claimStateSchema, } from '../wire/claims.js';
|
|
16
16
|
import { modelListResponseSchema, modelReadResponseSchema, } from '../wire/modelResponses.js';
|
|
17
17
|
import { toMs } from '../utils/duration.js';
|
|
18
|
-
import { heartbeatCadenceMs, resolveHeartbeatOptions, startClaimHeartbeatLoop, } from '../coordination/claimHeartbeatLoop.js';
|
|
18
|
+
import { heartbeatCadenceMs, resolveHeartbeatOptions, resolveHeartbeatPlan, startClaimHeartbeatLoop, } from '../coordination/claimHeartbeatLoop.js';
|
|
19
19
|
import { mintSession } from '../auth/sessionMint.js';
|
|
20
20
|
import { parseIdentityResolveResponse } from '../auth/schemas.js';
|
|
21
21
|
/**
|
|
@@ -34,8 +34,8 @@ function heldHeartbeatReply(reply, label) {
|
|
|
34
34
|
}
|
|
35
35
|
throw new AbloClaimedError(`The lease behind ${label} is no longer held — it expired or was granted onward. Re-acquire the claim and retry; a write attempted under the old lease is rejected by its \`readAt\` guard.`, { code: 'claim_lost' });
|
|
36
36
|
}
|
|
37
|
-
import { claimDescription } from '../coordination/schema.js';
|
|
38
|
-
import { subTarget, streamTarget } from '../coordination/locator.js';
|
|
37
|
+
import { claimDescription, partName } from '../coordination/schema.js';
|
|
38
|
+
import { subTarget, streamTarget, batchFence, fenceTokenFor, } from '../coordination/locator.js';
|
|
39
39
|
import { declaredMeta, wireMeta } from '../coordination/claimMeta.js';
|
|
40
40
|
import { assertWriteOptions } from '../resources/writeOptionsSchema.js';
|
|
41
41
|
import { createDurableHttpCommitEnvelope, canonicalHttpCommitBody, durableHttpCommitEnvelopeSchema, httpCommitEnvelopeRecordId, isHttpCommitReplayExpired, } from '../transactions/settlement/httpCommitEnvelope.js';
|
|
@@ -698,7 +698,7 @@ export function createHttpTransport(options) {
|
|
|
698
698
|
? crypto.randomUUID()
|
|
699
699
|
: `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
700
700
|
}
|
|
701
|
-
function normalizeCommitOperation(op, defaults,
|
|
701
|
+
function normalizeCommitOperation(op, defaults, fence) {
|
|
702
702
|
return {
|
|
703
703
|
action: op.action,
|
|
704
704
|
model: op.model,
|
|
@@ -707,18 +707,16 @@ export function createHttpTransport(options) {
|
|
|
707
707
|
transactionId: op.transactionId ?? null,
|
|
708
708
|
readAt: op.readAt ?? defaults.readAt ?? null,
|
|
709
709
|
onStale: op.onStale ?? defaults.onStale ?? null,
|
|
710
|
-
|
|
711
|
-
// how it supplies the batch `readAt`.
|
|
712
|
-
fenceToken: op.fenceToken ?? fenceToken ?? null,
|
|
710
|
+
fenceToken: op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null),
|
|
713
711
|
};
|
|
714
712
|
}
|
|
715
|
-
function normalizeCommitOperations(commitOptions,
|
|
713
|
+
function normalizeCommitOperations(commitOptions, fence) {
|
|
716
714
|
if (commitOptions.operations.length === 0) {
|
|
717
715
|
throw new AbloValidationError('Commit requires a non-empty `operations` array.', {
|
|
718
716
|
code: 'commit_operation_required',
|
|
719
717
|
});
|
|
720
718
|
}
|
|
721
|
-
return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions,
|
|
719
|
+
return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fence));
|
|
722
720
|
}
|
|
723
721
|
async function listClaimState(target) {
|
|
724
722
|
const params = new URLSearchParams();
|
|
@@ -730,7 +728,105 @@ export function createHttpTransport(options) {
|
|
|
730
728
|
params.set('field', target.field);
|
|
731
729
|
const suffix = params.toString();
|
|
732
730
|
const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' }, claimListResponseSchema);
|
|
733
|
-
|
|
731
|
+
// One list, one resource in two states: holders and waiters are told
|
|
732
|
+
// apart by each entry's `status`, not by bespoke envelope members.
|
|
733
|
+
return {
|
|
734
|
+
active: body.data.filter((row) => row.status !== 'queued'),
|
|
735
|
+
queue: body.data.filter((row) => row.status === 'queued'),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
// The claim-ticket surface: everything a caller does holding only a
|
|
739
|
+
// `claimId` — which is all a queued acquire leaves in its hand. Each method
|
|
740
|
+
// is a thin cast of its route; the reply schemas are the wire's own, so the
|
|
741
|
+
// surface cannot describe a response the server does not send.
|
|
742
|
+
const claims = {
|
|
743
|
+
retrieve({ claimId }) {
|
|
744
|
+
return requestJson(`/v1/claims/${encodeURIComponent(claimId)}`, { method: 'GET' }, claimStateSchema);
|
|
745
|
+
},
|
|
746
|
+
heartbeat({ claimId, ttl }) {
|
|
747
|
+
return requestJson(`/v1/claims/${encodeURIComponent(claimId)}/heartbeat`, {
|
|
748
|
+
method: 'POST',
|
|
749
|
+
body: JSON.stringify(ttl !== undefined ? { ttl } : {}),
|
|
750
|
+
}, claimHeartbeatReplySchema);
|
|
751
|
+
},
|
|
752
|
+
async heartbeatAll(options) {
|
|
753
|
+
const reply = await requestJson('/v1/claims/heartbeat', {
|
|
754
|
+
method: 'POST',
|
|
755
|
+
body: JSON.stringify(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
756
|
+
}, claimHeartbeatBatchReplySchema);
|
|
757
|
+
return reply.results;
|
|
758
|
+
},
|
|
759
|
+
async release({ claimId }) {
|
|
760
|
+
await requestRaw(`/v1/claims/${encodeURIComponent(claimId)}`, {
|
|
761
|
+
method: 'DELETE',
|
|
762
|
+
});
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
// How the stateless client waits its turn. The queued slot is real server
|
|
766
|
+
// state, so one heartbeat per tick does both jobs: it refreshes this
|
|
767
|
+
// waiter's slot and reports the line's answer — `queued` (still waiting) or
|
|
768
|
+
// `held` (granted). The first check comes quickly because most holds are a
|
|
769
|
+
// short claim→write→release; after that the cadence relaxes, with jitter so
|
|
770
|
+
// a fleet of waiters doesn't beat in step.
|
|
771
|
+
const GRANT_POLL_FIRST_MS = 250;
|
|
772
|
+
const GRANT_POLL_INTERVAL_MS = 1_000;
|
|
773
|
+
// An abort cuts the sleep short so the wait ends within a tick of the
|
|
774
|
+
// signal, not at the next scheduled beat.
|
|
775
|
+
const sleep = (ms, signal) => new Promise((resolve) => {
|
|
776
|
+
const done = () => {
|
|
777
|
+
clearTimeout(timer);
|
|
778
|
+
signal?.removeEventListener('abort', done);
|
|
779
|
+
resolve();
|
|
780
|
+
};
|
|
781
|
+
const timer = setTimeout(done, ms);
|
|
782
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
783
|
+
});
|
|
784
|
+
async function awaitGrantOverHttp(targetLabel, queued, options) {
|
|
785
|
+
// The queued reply is a claim resource in its waiting state, so the
|
|
786
|
+
// handle is its `id` — same rule as the 201 and the poll.
|
|
787
|
+
const claimId = queued.id;
|
|
788
|
+
const { signal } = options;
|
|
789
|
+
// Leave the line before rejecting: an abandoned slot would otherwise sit
|
|
790
|
+
// in the queue until its TTL lapses, stalling every waiter behind it.
|
|
791
|
+
const rejectAndLeave = async (error) => {
|
|
792
|
+
await claims.release({ claimId }).catch(() => { });
|
|
793
|
+
throw error;
|
|
794
|
+
};
|
|
795
|
+
if (options.maxQueueDepth !== undefined && queued.position >= options.maxQueueDepth) {
|
|
796
|
+
return rejectAndLeave(new AbloClaimedError(`Claim queue for ${targetLabel} is ${queued.position} deep (max ${options.maxQueueDepth}).`, { code: 'queue_too_deep' }));
|
|
797
|
+
}
|
|
798
|
+
const deadline = options.waitTimeoutMs !== undefined ? Date.now() + options.waitTimeoutMs : undefined;
|
|
799
|
+
let delay = GRANT_POLL_FIRST_MS;
|
|
800
|
+
for (;;) {
|
|
801
|
+
if (signal?.aborted) {
|
|
802
|
+
return rejectAndLeave(new AbloClaimedError(`The wait for the claim on ${targetLabel} was aborted before the grant arrived.`, { code: 'claim_wait_aborted' }));
|
|
803
|
+
}
|
|
804
|
+
if (deadline !== undefined && Date.now() >= deadline) {
|
|
805
|
+
return rejectAndLeave(new AbloClaimedError(`Timed out after ${options.waitTimeoutMs}ms waiting for the queue grant on ${targetLabel}.`, { code: 'grant_timeout' }));
|
|
806
|
+
}
|
|
807
|
+
await sleep(deadline !== undefined ? Math.min(delay, Math.max(0, deadline - Date.now())) : delay, signal);
|
|
808
|
+
if (signal?.aborted) {
|
|
809
|
+
return rejectAndLeave(new AbloClaimedError(`The wait for the claim on ${targetLabel} was aborted before the grant arrived.`, { code: 'claim_wait_aborted' }));
|
|
810
|
+
}
|
|
811
|
+
delay = GRANT_POLL_INTERVAL_MS * (0.85 + Math.random() * 0.3);
|
|
812
|
+
// A lease that ended answers the beat with 409 `claim_lost`, which the
|
|
813
|
+
// wire error mapping raises as AbloClaimedError before this reads
|
|
814
|
+
// anything — the wait fails with the loss, as the socket wait does.
|
|
815
|
+
const beat = await claims.heartbeat({ claimId });
|
|
816
|
+
if (beat.status !== 'held')
|
|
817
|
+
continue;
|
|
818
|
+
// Granted. The heartbeat ack does not carry the fence token — the claim
|
|
819
|
+
// state does, server-stamped at grant.
|
|
820
|
+
const state = await claims.retrieve({ claimId });
|
|
821
|
+
if (state.status !== 'active') {
|
|
822
|
+
return rejectAndLeave(new AbloClaimedError(`Claim lost while queued for ${targetLabel}.`, {
|
|
823
|
+
code: 'claim_lost',
|
|
824
|
+
}));
|
|
825
|
+
}
|
|
826
|
+
return state.fenceToken !== undefined
|
|
827
|
+
? { id: claimId, fenceToken: state.fenceToken }
|
|
828
|
+
: { id: claimId };
|
|
829
|
+
}
|
|
734
830
|
}
|
|
735
831
|
async function applyClaimedPolicy(target, options, defaultPolicy = 'return') {
|
|
736
832
|
const policy = options?.ifClaimed ?? defaultPolicy;
|
|
@@ -760,7 +856,7 @@ export function createHttpTransport(options) {
|
|
|
760
856
|
...commitOptions,
|
|
761
857
|
readAt: commitOptions.readAt ?? claim?.readAt ?? null,
|
|
762
858
|
onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
763
|
-
}, claim?.fenceToken
|
|
859
|
+
}, batchFence(claim?.target, claim?.fenceToken));
|
|
764
860
|
const requestBody = {
|
|
765
861
|
operations,
|
|
766
862
|
reads: commitOptions.reads,
|
|
@@ -953,30 +1049,21 @@ export function createHttpTransport(options) {
|
|
|
953
1049
|
queue: params.queue ?? true,
|
|
954
1050
|
};
|
|
955
1051
|
const body = await requestJson(claimPath(params.id), { method: 'POST', body: JSON.stringify(request) }, claimAcquireResponseSchema);
|
|
956
|
-
//
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
//
|
|
961
|
-
//
|
|
962
|
-
//
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
// shape callers build against, and the better it worked the harder it
|
|
966
|
-
// would be to replace with the status this should be.
|
|
967
|
-
//
|
|
968
|
-
// A caller that must wait today can poll `GET /v1/claims/{claimId}`, which
|
|
969
|
-
// is served and published. That primitive is safe to build on; a blessed
|
|
970
|
-
// wait algorithm here is not.
|
|
971
|
-
// The two arms are told apart by `status`, which only the queued reply
|
|
972
|
-
// carries — see `claimAcquireResponseSchema` for why they cannot be a
|
|
973
|
-
// discriminated union.
|
|
974
|
-
if ('status' in body) {
|
|
975
|
-
throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position}. ` +
|
|
976
|
-
`Poll \`GET /v1/claims/{claimId}\` for the grant — the HTTP client does not await it.`, { code: 'claim_queued' });
|
|
1052
|
+
// One resource, two states, discriminated by `status`. The queued arm
|
|
1053
|
+
// WAITS, exactly as the socket client does: `claim({ id })` means
|
|
1054
|
+
// "serialize me behind the holder" on every transport, and the grant
|
|
1055
|
+
// machinery is the SDK's to own, not a loop each caller re-derives.
|
|
1056
|
+
// (Being queued is still not an error — ADR 0018 — which is precisely
|
|
1057
|
+
// why it no longer surfaces as one here. The `claims` namespace remains
|
|
1058
|
+
// the manual ticket surface.)
|
|
1059
|
+
if (body.status === 'queued') {
|
|
1060
|
+
return awaitGrantOverHttp(`${name}/${params.id}`, body, params);
|
|
977
1061
|
}
|
|
978
|
-
|
|
979
|
-
|
|
1062
|
+
// The lease's own fields are mirrored at the top level, the same place
|
|
1063
|
+
// the poll puts them — one reader for both answers.
|
|
1064
|
+
return body.fenceToken !== undefined
|
|
1065
|
+
? { id: body.id, fenceToken: body.fenceToken }
|
|
1066
|
+
: { id: body.id };
|
|
980
1067
|
};
|
|
981
1068
|
const releaseClaim = (params) => requestRaw(claimPath(isClaimHandle(params) ? params.target.id : params.id), {
|
|
982
1069
|
method: 'DELETE',
|
|
@@ -996,13 +1083,30 @@ export function createHttpTransport(options) {
|
|
|
996
1083
|
return heldHeartbeatReply(reply, `claim ${claimId} on ${name}/${id}`);
|
|
997
1084
|
};
|
|
998
1085
|
async function claimImpl(params) {
|
|
999
|
-
|
|
1086
|
+
let acquired;
|
|
1087
|
+
try {
|
|
1088
|
+
acquired = await acquireClaim(params);
|
|
1089
|
+
}
|
|
1090
|
+
catch (error) {
|
|
1091
|
+
// The try-claim: a held target is an expected outcome of `queue:
|
|
1092
|
+
// false`, not an error — resolve `null` and let the caller move on.
|
|
1093
|
+
// Every other failure (auth, validation, network) stays a rejection,
|
|
1094
|
+
// and the write-site claim path calls `acquireClaim` directly, so a
|
|
1095
|
+
// write that could not claim still fails loudly.
|
|
1096
|
+
if (params.queue === false &&
|
|
1097
|
+
error instanceof AbloClaimedError &&
|
|
1098
|
+
(error.code === 'entity_claimed' || error.code === 'claim_conflict')) {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
throw error;
|
|
1102
|
+
}
|
|
1103
|
+
const { id: claimId, fenceToken } = acquired;
|
|
1000
1104
|
observability?.captureClaim({
|
|
1001
1105
|
phase: 'acquired',
|
|
1002
1106
|
claimId,
|
|
1003
1107
|
model: name,
|
|
1004
1108
|
id: params.id,
|
|
1005
|
-
...(params.field ? { field: params.field } : {}),
|
|
1109
|
+
...(params.field ? { field: partName(params.field) } : {}),
|
|
1006
1110
|
description: claimDescription(params),
|
|
1007
1111
|
});
|
|
1008
1112
|
const { data, stamp } = await retrieveModel(name, { id: params.id });
|
|
@@ -1012,23 +1116,27 @@ export function createHttpTransport(options) {
|
|
|
1012
1116
|
if (data === undefined) {
|
|
1013
1117
|
throw new AbloNotFoundError(`Cannot claim ${name}/${params.id}: it does not exist (or is outside this credential's scope).`, [params.id]);
|
|
1014
1118
|
}
|
|
1119
|
+
// One reading of the heartbeat options — cadence and callbacks from
|
|
1120
|
+
// whichever spelling the caller used (plan object, shorthand, or the
|
|
1121
|
+
// deprecated flat callbacks).
|
|
1122
|
+
const plan = resolveHeartbeatPlan(params);
|
|
1015
1123
|
const heartbeat = async (beatOptions) => {
|
|
1016
1124
|
const resolved = resolveHeartbeatOptions(beatOptions);
|
|
1017
1125
|
const beat = await heartbeatClaim(params.id, claimId, {
|
|
1018
1126
|
ttl: resolved.ttl ?? params.ttl,
|
|
1019
1127
|
...(resolved.details !== undefined ? { details: resolved.details } : {}),
|
|
1020
1128
|
});
|
|
1021
|
-
|
|
1129
|
+
plan.onBeat?.(beat);
|
|
1022
1130
|
return beat;
|
|
1023
1131
|
};
|
|
1024
1132
|
// Opt-in auto-heartbeat — the background-worker cadence. The stateless
|
|
1025
1133
|
// HTTP claim defaults to the server's acquire window when no TTL
|
|
1026
1134
|
// was requested, so the default cadence lands at 20s beats.
|
|
1027
|
-
const stopHeartbeatLoop =
|
|
1135
|
+
const stopHeartbeatLoop = plan.loop
|
|
1028
1136
|
? startClaimHeartbeatLoop({
|
|
1029
1137
|
beat: () => heartbeat(),
|
|
1030
|
-
intervalMs: heartbeatCadenceMs(params.ttl !== undefined ? toMs(params.ttl) : DEFAULT_CLAIM_TTL_MS,
|
|
1031
|
-
...(
|
|
1138
|
+
intervalMs: heartbeatCadenceMs(params.ttl !== undefined ? toMs(params.ttl) : DEFAULT_CLAIM_TTL_MS, plan.cadence),
|
|
1139
|
+
...(plan.onLost ? { onLost: plan.onLost } : {}),
|
|
1032
1140
|
})
|
|
1033
1141
|
: undefined;
|
|
1034
1142
|
const release = () => {
|
|
@@ -1060,19 +1168,22 @@ export function createHttpTransport(options) {
|
|
|
1060
1168
|
[Symbol.asyncDispose]: release,
|
|
1061
1169
|
};
|
|
1062
1170
|
}
|
|
1063
|
-
const claimsForEntity = (params) => requestJson(`/v1/claims?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' }, claimListResponseSchema);
|
|
1171
|
+
const claimsForEntity = (params) => requestJson(`/v1/claims?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(partName(params.field))}` : ''}`, { method: 'GET' }, claimListResponseSchema);
|
|
1064
1172
|
const claim = Object.assign(claimImpl, {
|
|
1065
1173
|
release: releaseClaim,
|
|
1066
1174
|
state: async (params) => {
|
|
1067
1175
|
const res = await claimsForEntity(params);
|
|
1068
|
-
|
|
1176
|
+
// Holders come first in the one list; a `queued` entry is a waiter.
|
|
1177
|
+
const first = res.data.find((row) => row.status !== 'queued');
|
|
1069
1178
|
return first ? claimFromModelClaim(first) : null;
|
|
1070
1179
|
},
|
|
1071
1180
|
queue: async (params) => {
|
|
1072
1181
|
const res = await claimsForEntity(params);
|
|
1073
1182
|
return {
|
|
1074
1183
|
object: 'list',
|
|
1075
|
-
data:
|
|
1184
|
+
data: res.data
|
|
1185
|
+
.filter((row) => row.status === 'queued')
|
|
1186
|
+
.map(claimFromModelClaim),
|
|
1076
1187
|
};
|
|
1077
1188
|
},
|
|
1078
1189
|
reorder: async (params) => {
|
|
@@ -1224,6 +1335,7 @@ export function createHttpTransport(options) {
|
|
|
1224
1335
|
async dispose() { },
|
|
1225
1336
|
async purge() { },
|
|
1226
1337
|
commits,
|
|
1338
|
+
claims,
|
|
1227
1339
|
model,
|
|
1228
1340
|
sessions: {
|
|
1229
1341
|
async create(params) {
|
|
@@ -21,7 +21,7 @@ import { EventEmitter } from 'events';
|
|
|
21
21
|
import type { ParticipantKind } from '../types/participant.js';
|
|
22
22
|
import type { BootstrapReason } from '../wire/bootstrapReason.js';
|
|
23
23
|
import type { ClientSyncDelta } from '../wire/delta.js';
|
|
24
|
-
import type { ClaimAcquired, PresenceUpdate, ClaimExpired, ClaimGranted, ClaimHeartbeatAckPayload, ClaimLost, ClaimQueue, ClaimQueued, ClaimRejection, StaleNotification, ReadDependency, TrackDependency } from '../coordination/schema.js';
|
|
24
|
+
import type { ClaimAcquired, PresenceUpdate, ClaimExpired, ClaimGranted, ClaimHeartbeatAckPayload, ClaimLost, ClaimQueue, ClaimQueued, ClaimRejection, ParticipantClaimPayload, StaleNotification, ReadDependency, TrackDependency } from '../coordination/schema.js';
|
|
25
25
|
import { type AuthTokenGetter } from '../auth/credentialSource.js';
|
|
26
26
|
import { type CommitAck, type CommitFrameOperation } from './commitFrames.js';
|
|
27
27
|
import { type Logger } from '../logger.js';
|
|
@@ -444,9 +444,7 @@ export declare class WsTransport<TCollaboration extends EventMap<TCollaboration>
|
|
|
444
444
|
* and effective `ttlSeconds` once `claim_ack` arrives, or rejects with a typed
|
|
445
445
|
* error on a failed ack, a timeout, or a disconnect.
|
|
446
446
|
*/
|
|
447
|
-
sendClaim(claimId: string, syncGroups: readonly string[], options?: {
|
|
448
|
-
capabilityToken?: string;
|
|
449
|
-
ttlSeconds?: number;
|
|
447
|
+
sendClaim(claimId: string, syncGroups: readonly string[], options?: Pick<ParticipantClaimPayload, 'capabilityToken' | 'ttlSeconds'> & {
|
|
450
448
|
timeoutMs?: number;
|
|
451
449
|
}): Promise<{
|
|
452
450
|
syncGroups: string[];
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { EventEmitter } from 'events';
|
|
21
21
|
import { AbloConnectionError, AbloError, AbloSessionError, AbloValidationError, toAbloError, } from '../errors.js';
|
|
22
|
-
import { updateSubscriptionPayloadSchema } from '../coordination/schema.js';
|
|
22
|
+
import { participantClaimPayloadSchema, updateSubscriptionPayloadSchema, } from '../coordination/schema.js';
|
|
23
23
|
import { PROTOCOL_VERSION, WS_CLOSE_PROTOCOL_VERSION } from '../wire/protocolVersion.js';
|
|
24
24
|
import { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL, } from '../auth/credentialSource.js';
|
|
25
25
|
import { buildCommitFrame } from './commitFrames.js';
|
|
@@ -685,6 +685,20 @@ export class WsTransport extends EventEmitter {
|
|
|
685
685
|
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
686
686
|
return Promise.reject(this.notConnectedError('claim'));
|
|
687
687
|
}
|
|
688
|
+
// Checked against the schema the server ingests it with, for the same
|
|
689
|
+
// reason `updateSubscription` below is: the two frames name their scopes
|
|
690
|
+
// identically, so a group that would be refused there is refused here, at
|
|
691
|
+
// the call that asked for it, rather than coming back as a failed ack a
|
|
692
|
+
// round trip later with nothing to point at.
|
|
693
|
+
const payload = participantClaimPayloadSchema.safeParse({
|
|
694
|
+
claimId,
|
|
695
|
+
syncGroups: [...syncGroups],
|
|
696
|
+
capabilityToken: options?.capabilityToken,
|
|
697
|
+
ttlSeconds: options?.ttlSeconds,
|
|
698
|
+
});
|
|
699
|
+
if (!payload.success) {
|
|
700
|
+
return Promise.reject(new AbloValidationError(`join was given a sync group the protocol does not accept: ${payload.error.issues[0]?.message ?? 'unreadable'}. A group is 'default' or 'kind:id'.`, { code: 'malformed_claim' }));
|
|
701
|
+
}
|
|
688
702
|
const timeoutMs = options?.timeoutMs ?? 15_000;
|
|
689
703
|
return new Promise((resolve, reject) => {
|
|
690
704
|
const timeout = setTimeout(() => {
|
|
@@ -695,15 +709,7 @@ export class WsTransport extends EventEmitter {
|
|
|
695
709
|
}, timeoutMs);
|
|
696
710
|
this.pendingClaims.set(claimId, { resolve, reject, timeout });
|
|
697
711
|
try {
|
|
698
|
-
this.ws.send(JSON.stringify({
|
|
699
|
-
type: 'claim',
|
|
700
|
-
payload: {
|
|
701
|
-
claimId,
|
|
702
|
-
syncGroups: [...syncGroups],
|
|
703
|
-
capabilityToken: options?.capabilityToken,
|
|
704
|
-
ttlSeconds: options?.ttlSeconds,
|
|
705
|
-
},
|
|
706
|
-
}));
|
|
712
|
+
this.ws.send(JSON.stringify({ type: 'claim', payload: payload.data }));
|
|
707
713
|
}
|
|
708
714
|
catch (error) {
|
|
709
715
|
clearTimeout(timeout);
|
|
@@ -446,6 +446,16 @@ export interface Claim<T = Record<string, unknown>, M = ResolveClaimMeta> {
|
|
|
446
446
|
* claiming.
|
|
447
447
|
*/
|
|
448
448
|
readonly ttlSeconds?: number;
|
|
449
|
+
/**
|
|
450
|
+
* The claim's open metadata bag, as it stands on the wire.
|
|
451
|
+
*
|
|
452
|
+
* A heartbeat's `details` land here under `progress` — last beat wins — so
|
|
453
|
+
* an observer can read what a long hold is doing without waiting for the
|
|
454
|
+
* holder to release. Deliberately the open record rather than the declared
|
|
455
|
+
* `ClaimMeta`: a shape the program declared has no member for a key the
|
|
456
|
+
* coordinator wrote, and `target.meta` beside it is that declared shape.
|
|
457
|
+
*/
|
|
458
|
+
readonly meta?: Record<string, unknown>;
|
|
449
459
|
/**
|
|
450
460
|
* 0-based place in the FIFO line — present only when `status: 'queued'`
|
|
451
461
|
* (`0` = next behind the holder). Absent for the active holder.
|
|
@@ -12,14 +12,39 @@
|
|
|
12
12
|
* caller can pass the same field interchangeably. {@link toMs} returns
|
|
13
13
|
* milliseconds; {@link toSeconds} returns whole seconds.
|
|
14
14
|
*/
|
|
15
|
+
import { z } from 'zod';
|
|
15
16
|
export type Duration = number | `${number}ms` | `${number}s` | `${number}m` | `${number}h`;
|
|
17
|
+
/**
|
|
18
|
+
* The same grammar as a boundary schema, so a duration crossing the wire is
|
|
19
|
+
* checked against the parser that will read it rather than against a second
|
|
20
|
+
* description of it.
|
|
21
|
+
*
|
|
22
|
+
* The regex is shared with {@link toMs} deliberately: `z.toJSONSchema` emits it
|
|
23
|
+
* as a `pattern`, which is how a non-TypeScript caller learns the grammar at
|
|
24
|
+
* all. A union of `number | string` — which is what this used to be on the
|
|
25
|
+
* claim bodies — publishes as "some number or some string" and leaves the units
|
|
26
|
+
* to be guessed.
|
|
27
|
+
*/
|
|
28
|
+
export declare const durationSchema: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
|
16
29
|
/**
|
|
17
30
|
* Parses a duration and returns the equivalent number of milliseconds. Accepts
|
|
18
31
|
* either a bare number, interpreted as seconds, or a unit-suffixed string such
|
|
19
32
|
* as `'500ms'`, `'30s'`, `'3m'`, or `'24h'`. Throws an
|
|
20
33
|
* {@link AbloValidationError} with code `duration_invalid` when a string does
|
|
21
34
|
* not match a supported unit.
|
|
35
|
+
*
|
|
36
|
+
* Two doors, one implementation. {@link toMs} takes the narrow {@link Duration}
|
|
37
|
+
* template type, so an SDK caller writing a literal gets the unit checked at
|
|
38
|
+
* compile time. {@link parseDurationMs} takes the widened `string | number` that
|
|
39
|
+
* {@link durationSchema} infers — the shape a value has after it crossed the
|
|
40
|
+
* wire and was validated at the boundary. Neither is a cast of the other, and
|
|
41
|
+
* neither carries its own copy of the grammar.
|
|
22
42
|
*/
|
|
23
43
|
export declare function toMs(input: Duration): number;
|
|
44
|
+
/**
|
|
45
|
+
* {@link toMs} for a value that arrived over the wire, where the type system
|
|
46
|
+
* knows only `string | number` because that is what the boundary schema infers.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseDurationMs(input: string | number): number;
|
|
24
49
|
/** Parses a duration like {@link toMs} but returns whole seconds, rounding down. */
|
|
25
50
|
export declare function toSeconds(input: Duration): number;
|
|
@@ -12,8 +12,26 @@
|
|
|
12
12
|
* caller can pass the same field interchangeably. {@link toMs} returns
|
|
13
13
|
* milliseconds; {@link toSeconds} returns whole seconds.
|
|
14
14
|
*/
|
|
15
|
+
import { z } from 'zod';
|
|
15
16
|
import { AbloValidationError } from '../errors.js';
|
|
16
17
|
const PATTERN = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/;
|
|
18
|
+
/**
|
|
19
|
+
* The same grammar as a boundary schema, so a duration crossing the wire is
|
|
20
|
+
* checked against the parser that will read it rather than against a second
|
|
21
|
+
* description of it.
|
|
22
|
+
*
|
|
23
|
+
* The regex is shared with {@link toMs} deliberately: `z.toJSONSchema` emits it
|
|
24
|
+
* as a `pattern`, which is how a non-TypeScript caller learns the grammar at
|
|
25
|
+
* all. A union of `number | string` — which is what this used to be on the
|
|
26
|
+
* claim bodies — publishes as "some number or some string" and leaves the units
|
|
27
|
+
* to be guessed.
|
|
28
|
+
*/
|
|
29
|
+
export const durationSchema = z.union([
|
|
30
|
+
z.number().positive(),
|
|
31
|
+
z.string().regex(PATTERN, {
|
|
32
|
+
message: 'expected a duration such as "500ms", "30s", "3m" or "24h"',
|
|
33
|
+
}),
|
|
34
|
+
]);
|
|
17
35
|
const UNIT_MS = {
|
|
18
36
|
ms: 1,
|
|
19
37
|
s: 1_000,
|
|
@@ -26,8 +44,22 @@ const UNIT_MS = {
|
|
|
26
44
|
* as `'500ms'`, `'30s'`, `'3m'`, or `'24h'`. Throws an
|
|
27
45
|
* {@link AbloValidationError} with code `duration_invalid` when a string does
|
|
28
46
|
* not match a supported unit.
|
|
47
|
+
*
|
|
48
|
+
* Two doors, one implementation. {@link toMs} takes the narrow {@link Duration}
|
|
49
|
+
* template type, so an SDK caller writing a literal gets the unit checked at
|
|
50
|
+
* compile time. {@link parseDurationMs} takes the widened `string | number` that
|
|
51
|
+
* {@link durationSchema} infers — the shape a value has after it crossed the
|
|
52
|
+
* wire and was validated at the boundary. Neither is a cast of the other, and
|
|
53
|
+
* neither carries its own copy of the grammar.
|
|
29
54
|
*/
|
|
30
55
|
export function toMs(input) {
|
|
56
|
+
return parseDurationMs(input);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* {@link toMs} for a value that arrived over the wire, where the type system
|
|
60
|
+
* knows only `string | number` because that is what the boundary schema infers.
|
|
61
|
+
*/
|
|
62
|
+
export function parseDurationMs(input) {
|
|
31
63
|
if (typeof input === 'number')
|
|
32
64
|
return input * 1_000;
|
|
33
65
|
const match = PATTERN.exec(input);
|
|
@@ -131,6 +131,29 @@ export declare const schemaModelResponseSchema: z.ZodObject<{
|
|
|
131
131
|
}>>;
|
|
132
132
|
}, z.core.$strip>>;
|
|
133
133
|
hash: z.ZodString;
|
|
134
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
135
|
+
type: z.ZodEnum<{
|
|
136
|
+
string: "string";
|
|
137
|
+
number: "number";
|
|
138
|
+
boolean: "boolean";
|
|
139
|
+
date: "date";
|
|
140
|
+
enum: "enum";
|
|
141
|
+
json: "json";
|
|
142
|
+
}>;
|
|
143
|
+
isOptional: z.ZodBoolean;
|
|
144
|
+
isIndexed: z.ZodBoolean;
|
|
145
|
+
column: z.ZodOptional<z.ZodString>;
|
|
146
|
+
enumValues: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
147
|
+
}, z.core.$strip>>>;
|
|
148
|
+
relations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
149
|
+
type: z.ZodEnum<{
|
|
150
|
+
belongsTo: "belongsTo";
|
|
151
|
+
hasMany: "hasMany";
|
|
152
|
+
hasOne: "hasOne";
|
|
153
|
+
}>;
|
|
154
|
+
target: z.ZodString;
|
|
155
|
+
foreignKey: z.ZodString;
|
|
156
|
+
}, z.core.$strip>>>;
|
|
134
157
|
}, z.core.$strip>;
|
|
135
158
|
export type SchemaModelResponse = z.infer<typeof schemaModelResponseSchema>;
|
|
136
159
|
/**
|
|
@@ -168,6 +191,29 @@ export declare const schemaReadResponseSchema: z.ZodDiscriminatedUnion<[z.ZodObj
|
|
|
168
191
|
}>>;
|
|
169
192
|
}, z.core.$strip>>;
|
|
170
193
|
hash: z.ZodString;
|
|
194
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
195
|
+
type: z.ZodEnum<{
|
|
196
|
+
string: "string";
|
|
197
|
+
number: "number";
|
|
198
|
+
boolean: "boolean";
|
|
199
|
+
date: "date";
|
|
200
|
+
enum: "enum";
|
|
201
|
+
json: "json";
|
|
202
|
+
}>;
|
|
203
|
+
isOptional: z.ZodBoolean;
|
|
204
|
+
isIndexed: z.ZodBoolean;
|
|
205
|
+
column: z.ZodOptional<z.ZodString>;
|
|
206
|
+
enumValues: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
207
|
+
}, z.core.$strip>>>;
|
|
208
|
+
relations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
209
|
+
type: z.ZodEnum<{
|
|
210
|
+
belongsTo: "belongsTo";
|
|
211
|
+
hasMany: "hasMany";
|
|
212
|
+
hasOne: "hasOne";
|
|
213
|
+
}>;
|
|
214
|
+
target: z.ZodString;
|
|
215
|
+
foreignKey: z.ZodString;
|
|
216
|
+
}, z.core.$strip>>>;
|
|
171
217
|
}, z.core.$strip>>>;
|
|
172
218
|
}, z.core.$strip>, z.ZodObject<{
|
|
173
219
|
active: z.ZodLiteral<true>;
|
|
@@ -194,6 +240,29 @@ export declare const schemaReadResponseSchema: z.ZodDiscriminatedUnion<[z.ZodObj
|
|
|
194
240
|
}>>;
|
|
195
241
|
}, z.core.$strip>>;
|
|
196
242
|
hash: z.ZodString;
|
|
243
|
+
fields: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
244
|
+
type: z.ZodEnum<{
|
|
245
|
+
string: "string";
|
|
246
|
+
number: "number";
|
|
247
|
+
boolean: "boolean";
|
|
248
|
+
date: "date";
|
|
249
|
+
enum: "enum";
|
|
250
|
+
json: "json";
|
|
251
|
+
}>;
|
|
252
|
+
isOptional: z.ZodBoolean;
|
|
253
|
+
isIndexed: z.ZodBoolean;
|
|
254
|
+
column: z.ZodOptional<z.ZodString>;
|
|
255
|
+
enumValues: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
256
|
+
}, z.core.$strip>>>;
|
|
257
|
+
relations: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
258
|
+
type: z.ZodEnum<{
|
|
259
|
+
belongsTo: "belongsTo";
|
|
260
|
+
hasMany: "hasMany";
|
|
261
|
+
hasOne: "hasOne";
|
|
262
|
+
}>;
|
|
263
|
+
target: z.ZodString;
|
|
264
|
+
foreignKey: z.ZodString;
|
|
265
|
+
}, z.core.$strip>>>;
|
|
197
266
|
}, z.core.$strip>>>;
|
|
198
267
|
schemaId: z.ZodString;
|
|
199
268
|
version: z.ZodNumber;
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* schemas is what removes them from it.
|
|
14
14
|
*/
|
|
15
15
|
import { z } from 'zod';
|
|
16
|
+
// Composed, never restated: these are the artifact's own shapes, and a
|
|
17
|
+
// hand-written mirror here would be a second copy of the exact record this
|
|
18
|
+
// response exists to stop withholding.
|
|
19
|
+
import { fieldMetaSchema, relationMetaSchema } from './modelShape.js';
|
|
16
20
|
import { onStaleModeSchema } from '../coordination/schema.js';
|
|
17
21
|
/**
|
|
18
22
|
* One project — an app's own schema, data planes, and keys. `default` marks the
|
|
@@ -85,8 +89,39 @@ export const schemaModelResponseSchema = z.object({
|
|
|
85
89
|
typename: z.string(),
|
|
86
90
|
/** Declared disposition, or null for the engine default. */
|
|
87
91
|
conflict: conflictAxisWireSchema.nullable(),
|
|
88
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Per-model content hash — the unit of the client's drift check, and the
|
|
94
|
+
* revalidation key for everything below it.
|
|
95
|
+
*
|
|
96
|
+
* `fields` and `relations` change only when this does, so a client reads the
|
|
97
|
+
* shape once and thereafter compares hashes: a cheap poll that refetches on a
|
|
98
|
+
* push and never otherwise. Introspection that costs one request per schema
|
|
99
|
+
* version rather than one per question.
|
|
100
|
+
*/
|
|
89
101
|
hash: z.string(),
|
|
102
|
+
/**
|
|
103
|
+
* The model's fields, by name.
|
|
104
|
+
*
|
|
105
|
+
* The schema artifact has always carried these and this response dropped
|
|
106
|
+
* them, so a caller with no local schema declaration — which is every caller
|
|
107
|
+
* that is not TypeScript — had no way to learn that `task.title` is a
|
|
108
|
+
* required string. It found out from a 400 at the end of a round trip.
|
|
109
|
+
*
|
|
110
|
+
* For an agent that is not merely slow, it is a reasoning detour: a typo
|
|
111
|
+
* discovered through a rejected write costs a turn and an explanation, where
|
|
112
|
+
* the same typo checked locally costs a millisecond.
|
|
113
|
+
*
|
|
114
|
+
* Absent on an artifact written before this was reported, which is why it is
|
|
115
|
+
* optional rather than empty — "this server does not tell me" and "this model
|
|
116
|
+
* has no fields" are different facts.
|
|
117
|
+
*/
|
|
118
|
+
fields: z.record(z.string(), fieldMetaSchema).optional(),
|
|
119
|
+
/**
|
|
120
|
+
* The model's relations, by name — what a caller may expand, and into what.
|
|
121
|
+
*
|
|
122
|
+
* Optional for the same reason as {@link fields}.
|
|
123
|
+
*/
|
|
124
|
+
relations: z.record(z.string(), relationMetaSchema).optional(),
|
|
90
125
|
});
|
|
91
126
|
/**
|
|
92
127
|
* `GET /api/schema` — the schema active on the caller's plane.
|
|
@@ -44,6 +44,13 @@ export type EphemeralKeyRequest = z.infer<typeof ephemeralKeyRequestSchema>;
|
|
|
44
44
|
* `auth/capability.ts` — the grant is one structure, and its request form is a
|
|
45
45
|
* face of it rather than a separate shape. Re-exported here so the wire barrel
|
|
46
46
|
* stays the single import path for the boundary.
|
|
47
|
+
*
|
|
48
|
+
* This is the only import wire makes for something larger than a schema leaf,
|
|
49
|
+
* so it carries exactly the shape the barrel forwards and nothing else: the
|
|
50
|
+
* grant's own vocabulary — the verb enum, the `model.verb` spelling, the
|
|
51
|
+
* helpers that derive one from the other — is read from `auth/capability.ts`
|
|
52
|
+
* by the callers that build a grant, not through here.
|
|
47
53
|
*/
|
|
48
|
-
export { capabilityRequestSchema,
|
|
49
|
-
export type {
|
|
54
|
+
export { capabilityRequestSchema, capabilityMintResponseSchema, } from '../auth/capability.js';
|
|
55
|
+
export type { CapabilityMintResponse } from '../auth/capability.js';
|
|
56
|
+
export type { CapabilityRequest } from '../auth/capability.js';
|