@abloatai/transaction 0.55.0 → 0.57.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/CONVENTIONS.md +34 -0
- package/dist/auth/hostedEndpoints.d.ts +21 -5
- package/dist/auth/hostedEndpoints.d.ts.map +1 -1
- package/dist/auth/hostedEndpoints.js +21 -5
- package/dist/auth/hostedEndpoints.js.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/index.js.map +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +1 -1
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +0 -3
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +4 -5
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +1 -0
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +1 -0
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -1
- package/dist/errors.js.map +1 -1
- package/dist/log/syncDeltaRow.d.ts +3 -3
- package/dist/readSetContext.d.ts.map +1 -1
- package/dist/readSetContext.js +8 -3
- package/dist/readSetContext.js.map +1 -1
- package/dist/resources/httpResources.d.ts +43 -22
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/httpResources.js +81 -4
- package/dist/resources/httpResources.js.map +1 -1
- package/dist/resources/modelCreate.d.ts +21 -0
- package/dist/resources/modelCreate.d.ts.map +1 -0
- package/dist/resources/modelCreate.js +49 -0
- package/dist/resources/modelCreate.js.map +1 -0
- package/dist/resources/modelOperations.d.ts +24 -2
- package/dist/resources/modelOperations.d.ts.map +1 -1
- package/dist/resources/modelOperations.js.map +1 -1
- package/dist/resources/writeOptionsSchema.d.ts +14 -0
- package/dist/resources/writeOptionsSchema.d.ts.map +1 -1
- package/dist/resources/writeOptionsSchema.js +23 -2
- package/dist/resources/writeOptionsSchema.js.map +1 -1
- package/dist/schema/audit.d.ts +15 -0
- package/dist/schema/audit.d.ts.map +1 -0
- package/dist/schema/audit.js +90 -0
- package/dist/schema/audit.js.map +1 -0
- package/dist/schema/ddl.d.ts.map +1 -1
- package/dist/schema/ddl.js +68 -1
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/index.d.ts +3 -1
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +3 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/model.d.ts +14 -0
- package/dist/schema/model.d.ts.map +1 -1
- package/dist/schema/model.js +2 -0
- package/dist/schema/model.js.map +1 -1
- package/dist/schema/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +104 -2
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/roles.d.ts +27 -0
- package/dist/schema/roles.d.ts.map +1 -1
- package/dist/schema/roles.js +40 -0
- package/dist/schema/roles.js.map +1 -1
- package/dist/schema/schema.d.ts +23 -4
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +30 -1
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/serialize.d.ts +4 -1
- package/dist/schema/serialize.d.ts.map +1 -1
- package/dist/schema/serialize.js +5 -1
- package/dist/schema/serialize.js.map +1 -1
- package/dist/schema/subject.d.ts +18 -0
- package/dist/schema/subject.d.ts.map +1 -0
- package/dist/schema/subject.js +27 -0
- package/dist/schema/subject.js.map +1 -0
- package/dist/server/adapter.d.ts +2 -0
- package/dist/server/adapter.d.ts.map +1 -1
- package/dist/server/readConfig.d.ts +3 -0
- package/dist/server/readConfig.d.ts.map +1 -1
- package/dist/server/readConfig.js +0 -21
- package/dist/server/readConfig.js.map +1 -1
- package/dist/source/adapters/drizzle.d.ts.map +1 -1
- package/dist/source/adapters/drizzle.js +37 -6
- package/dist/source/adapters/drizzle.js.map +1 -1
- package/dist/source/adapters/kysely.d.ts +2 -0
- package/dist/source/adapters/kysely.d.ts.map +1 -1
- package/dist/source/adapters/kysely.js +33 -5
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts +4 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.js +17 -8
- package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
- package/dist/source/adapters/memory.d.ts.map +1 -1
- package/dist/source/adapters/memory.js +1 -0
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.d.ts.map +1 -1
- package/dist/source/adapters/prisma.js +59 -7
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/contract.d.ts +14 -0
- package/dist/source/contract.d.ts.map +1 -1
- package/dist/source/contract.js +10 -0
- package/dist/source/contract.js.map +1 -1
- package/dist/source/factory.d.ts +7 -1
- package/dist/source/factory.d.ts.map +1 -1
- package/dist/source/factory.js +116 -5
- package/dist/source/factory.js.map +1 -1
- package/dist/source/index.d.ts +2 -1
- package/dist/source/index.d.ts.map +1 -1
- package/dist/source/index.js +1 -0
- package/dist/source/index.js.map +1 -1
- package/dist/source/migrations.d.ts.map +1 -1
- package/dist/source/migrations.js +21 -0
- package/dist/source/migrations.js.map +1 -1
- package/dist/source/subjectAuthorization.d.ts +16 -0
- package/dist/source/subjectAuthorization.d.ts.map +1 -0
- package/dist/source/subjectAuthorization.js +126 -0
- package/dist/source/subjectAuthorization.js.map +1 -0
- package/dist/source/types.d.ts +52 -2
- package/dist/source/types.d.ts.map +1 -1
- package/dist/source/types.js +1 -0
- package/dist/source/types.js.map +1 -1
- package/dist/syncLog/contract.d.ts +45 -3
- package/dist/syncLog/contract.d.ts.map +1 -1
- package/dist/syncLog/contract.js +46 -4
- package/dist/syncLog/contract.js.map +1 -1
- package/dist/testing/fixtures/httpResponses.d.ts +6 -0
- package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
- package/dist/testing/fixtures/httpResponses.js +1 -0
- package/dist/testing/fixtures/httpResponses.js.map +1 -1
- package/dist/transport/httpClient.d.ts +8 -1
- package/dist/transport/httpClient.d.ts.map +1 -1
- package/dist/transport/httpClient.js +51 -20
- package/dist/transport/httpClient.js.map +1 -1
- package/dist/transport/httpCommitRequest.d.ts +45 -0
- package/dist/transport/httpCommitRequest.d.ts.map +1 -0
- package/dist/transport/httpCommitRequest.js +67 -0
- package/dist/transport/httpCommitRequest.js.map +1 -0
- package/dist/transport/httpTransport.d.ts.map +1 -1
- package/dist/transport/httpTransport.js +69 -106
- package/dist/transport/httpTransport.js.map +1 -1
- package/dist/transport/httpTransportHelpers.d.ts +10 -0
- package/dist/transport/httpTransportHelpers.d.ts.map +1 -0
- package/dist/transport/httpTransportHelpers.js +46 -0
- package/dist/transport/httpTransportHelpers.js.map +1 -0
- package/dist/wire/apiLifecycle.d.ts +63 -0
- package/dist/wire/apiLifecycle.d.ts.map +1 -0
- package/dist/wire/apiLifecycle.js +89 -0
- package/dist/wire/apiLifecycle.js.map +1 -0
- package/dist/wire/auth.d.ts.map +1 -1
- package/dist/wire/auth.js +6 -3
- package/dist/wire/auth.js.map +1 -1
- package/dist/wire/commit.d.ts +74 -9
- package/dist/wire/commit.d.ts.map +1 -1
- package/dist/wire/commit.js +18 -5
- package/dist/wire/commit.js.map +1 -1
- package/dist/wire/index.d.ts +5 -2
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +12 -1
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/modelMutations.js +3 -2
- package/dist/wire/modelMutations.js.map +1 -1
- package/dist/wire/rateLimit.d.ts +82 -0
- package/dist/wire/rateLimit.d.ts.map +1 -0
- package/dist/wire/rateLimit.js +142 -0
- package/dist/wire/rateLimit.js.map +1 -0
- package/package.json +1 -1
- package/src/auth/hostedEndpoints.ts +23 -5
- package/src/auth/index.ts +2 -0
- package/src/coordination/index.ts +0 -2
- package/src/coordination/schema.ts +4 -7
- package/src/errorCodes.ts +6 -0
- package/src/errors.ts +4 -1
- package/src/readSetContext.ts +8 -2
- package/src/resources/httpResources.ts +116 -25
- package/src/resources/modelCreate.ts +73 -0
- package/src/resources/modelOperations.ts +29 -2
- package/src/resources/writeOptionsSchema.ts +30 -2
- package/src/schema/audit.ts +121 -0
- package/src/schema/ddl.ts +71 -1
- package/src/schema/index.ts +16 -0
- package/src/schema/model.ts +17 -0
- package/src/schema/openapi.ts +123 -2
- package/src/schema/roles.ts +53 -0
- package/src/schema/schema.ts +68 -3
- package/src/schema/serialize.ts +8 -1
- package/src/schema/subject.ts +43 -0
- package/src/server/adapter.ts +2 -0
- package/src/server/readConfig.ts +4 -0
- package/src/source/adapters/drizzle.ts +50 -6
- package/src/source/adapters/kysely.ts +47 -4
- package/src/source/adapters/kyselyMutationCore.ts +21 -9
- package/src/source/adapters/memory.ts +1 -0
- package/src/source/adapters/prisma.ts +70 -7
- package/src/source/contract.ts +11 -0
- package/src/source/factory.ts +141 -5
- package/src/source/index.ts +6 -0
- package/src/source/migrations.ts +21 -0
- package/src/source/subjectAuthorization.ts +182 -0
- package/src/source/types.ts +56 -2
- package/src/syncLog/contract.ts +47 -5
- package/src/testing/fixtures/httpResponses.ts +7 -0
- package/src/transport/httpClient.ts +85 -23
- package/src/transport/httpCommitRequest.ts +104 -0
- package/src/transport/httpTransport.ts +92 -142
- package/src/transport/httpTransportHelpers.ts +63 -0
- package/src/wire/apiLifecycle.ts +94 -0
- package/src/wire/auth.ts +6 -3
- package/src/wire/commit.ts +20 -5
- package/src/wire/index.ts +31 -0
- package/src/wire/modelMutations.ts +2 -2
- package/src/wire/rateLimit.ts +155 -0
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
claimedError,
|
|
18
18
|
translateHttpError,
|
|
19
19
|
} from '../errors.js';
|
|
20
|
-
import { v5 as uuidv5 } from 'uuid';
|
|
21
20
|
import { z } from 'zod';
|
|
22
21
|
import {
|
|
23
22
|
reconcileFunctionalUpdate,
|
|
@@ -37,7 +36,6 @@ import {
|
|
|
37
36
|
} from '../auth/apiKey.js';
|
|
38
37
|
import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
|
|
39
38
|
import {
|
|
40
|
-
commitReceiptSchema,
|
|
41
39
|
commitRecordSchema,
|
|
42
40
|
commitRecordListSchema,
|
|
43
41
|
commitRecordListOptionsSchema,
|
|
@@ -99,25 +97,11 @@ import {
|
|
|
99
97
|
import { parseIdentityResolveResponse } from '../auth/schemas.js';
|
|
100
98
|
import type { EffectiveAuthority } from '../auth/capability.js';
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* signal that stops the auto-heartbeat loop.
|
|
108
|
-
*/
|
|
109
|
-
function heldHeartbeatReply(reply: ClaimHeartbeatReply, label: string): ClaimHeartbeat {
|
|
110
|
-
if (reply.status === 'held' && typeof reply.expiresAt === 'number') {
|
|
111
|
-
return {
|
|
112
|
-
expiresAt: reply.expiresAt,
|
|
113
|
-
...(reply.queueDepth !== undefined ? { queueDepth: reply.queueDepth } : {}),
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
throw new AbloClaimedError(
|
|
117
|
-
`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.`,
|
|
118
|
-
{ code: 'claim_lost' }
|
|
119
|
-
);
|
|
120
|
-
}
|
|
100
|
+
import {
|
|
101
|
+
claimFromModelClaim,
|
|
102
|
+
heldHeartbeatReply,
|
|
103
|
+
parseSuccessfulCommitResponse,
|
|
104
|
+
} from './httpTransportHelpers.js';
|
|
121
105
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
122
106
|
import type {
|
|
123
107
|
ClaimLookupParams,
|
|
@@ -127,6 +111,7 @@ import type {
|
|
|
127
111
|
ClaimReorderParams,
|
|
128
112
|
ModelTrackParams,
|
|
129
113
|
ModelTrackResult,
|
|
114
|
+
ModelCreateManyParams,
|
|
130
115
|
ServerReadOptions,
|
|
131
116
|
ResolvedClaimContentionOptions,
|
|
132
117
|
ClaimQueueView,
|
|
@@ -151,7 +136,15 @@ import {
|
|
|
151
136
|
import { declaredMeta, wireMeta } from '../coordination/claimMeta.js';
|
|
152
137
|
import type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim } from '../types/streams.js';
|
|
153
138
|
import type { CoordinationObservability } from '../observability.js';
|
|
154
|
-
import {
|
|
139
|
+
import {
|
|
140
|
+
assertWriteOptions,
|
|
141
|
+
assertWriteTarget,
|
|
142
|
+
} from '../resources/writeOptionsSchema.js';
|
|
143
|
+
import {
|
|
144
|
+
createModelId,
|
|
145
|
+
resolveCreatedRows,
|
|
146
|
+
resolveCreateId,
|
|
147
|
+
} from '../resources/modelCreate.js';
|
|
155
148
|
import { normalizeWhere } from '../resources/where.js';
|
|
156
149
|
import {
|
|
157
150
|
createDurableHttpCommitEnvelope,
|
|
@@ -163,6 +156,12 @@ import {
|
|
|
163
156
|
type DurableHttpCommitMethod,
|
|
164
157
|
} from '../transactions/confirmation/httpCommitEnvelope.js';
|
|
165
158
|
import type { CommitOutboxScope } from '../transactions/confirmation/commitEnvelope.js';
|
|
159
|
+
import {
|
|
160
|
+
createClientTxId,
|
|
161
|
+
normalizeCommitOperations,
|
|
162
|
+
replicationLagTimeout,
|
|
163
|
+
type ExactHttpCommitRequest,
|
|
164
|
+
} from './httpCommitRequest.js';
|
|
166
165
|
import { resolveDurableWrites } from '../durableWrites.js';
|
|
167
166
|
|
|
168
167
|
/** @internal Private options for the schema-agnostic HTTP protocol transport. */
|
|
@@ -239,46 +238,6 @@ export interface HttpTransport {
|
|
|
239
238
|
|
|
240
239
|
type CommitResponse = CommitReceiptWire;
|
|
241
240
|
|
|
242
|
-
function parseSuccessfulCommitResponse(value: unknown, idempotencyKey: string): CommitResponse {
|
|
243
|
-
const parsed = commitReceiptSchema.safeParse(value);
|
|
244
|
-
if (!parsed.success || parsed.data.clientTxId !== idempotencyKey) {
|
|
245
|
-
throw new AbloConnectionError(
|
|
246
|
-
'The commit endpoint returned an invalid success receipt; its outcome remains pending and is safe to retry.',
|
|
247
|
-
{
|
|
248
|
-
code: 'commit_no_result',
|
|
249
|
-
cause: parsed.success
|
|
250
|
-
? new Error('Commit receipt clientTxId did not match its idempotency key')
|
|
251
|
-
: parsed.error,
|
|
252
|
-
}
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
return parsed.data;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/** Decode the HTTP claim DTO into the one public Claim shape. */
|
|
259
|
-
function claimFromModelClaim(claim: ModelClaim): Claim {
|
|
260
|
-
// The handle a caller reads back is a public claim, so its `meta` is the
|
|
261
|
-
// declared shape; the rest of the sub-entity locator crosses whole rather
|
|
262
|
-
// than member by member, which is how `fields` used to die on this hop.
|
|
263
|
-
const { meta, ...details } = subTarget(claim.target);
|
|
264
|
-
return {
|
|
265
|
-
object: 'claim',
|
|
266
|
-
id: claim.id,
|
|
267
|
-
...(claim.status ? { status: claim.status } : {}),
|
|
268
|
-
// The server always stamps a description; default only for total safety.
|
|
269
|
-
description: claim.description ?? 'editing',
|
|
270
|
-
heldBy: claim.actor,
|
|
271
|
-
participantKind: claim.participantKind,
|
|
272
|
-
expiresAt: claim.expiresAt,
|
|
273
|
-
...(claim.position !== undefined ? { position: claim.position } : {}),
|
|
274
|
-
target: {
|
|
275
|
-
...streamTarget(claim.target),
|
|
276
|
-
...details,
|
|
277
|
-
...(meta !== undefined ? { meta: declaredMeta(meta) } : {}),
|
|
278
|
-
},
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
241
|
/** @internal Constructed only by the typed HTTP facade. */
|
|
283
242
|
export function createHttpTransport(options: HttpTransportOptions): HttpTransport {
|
|
284
243
|
const env = readProcessEnv();
|
|
@@ -703,14 +662,6 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
703
662
|
readonly response: CommitResponse;
|
|
704
663
|
}
|
|
705
664
|
|
|
706
|
-
interface ExactHttpCommitRequest {
|
|
707
|
-
readonly idempotencyKey: string;
|
|
708
|
-
readonly method: DurableHttpCommitMethod;
|
|
709
|
-
readonly path: string;
|
|
710
|
-
readonly body: string;
|
|
711
|
-
readonly sealedProtocolVersion?: number;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
665
|
function observeCommitReceipt(
|
|
715
666
|
request: ExactHttpCommitRequest,
|
|
716
667
|
receipt: CommitResponse,
|
|
@@ -738,25 +689,6 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
738
689
|
}
|
|
739
690
|
}
|
|
740
691
|
|
|
741
|
-
function replicationLagTimeout(
|
|
742
|
-
request: ExactHttpCommitRequest,
|
|
743
|
-
response: CommitResponse
|
|
744
|
-
): AbloConnectionError {
|
|
745
|
-
return new AbloConnectionError(
|
|
746
|
-
`The source accepted commit ${request.idempotencyKey}, but its replication echo did not arrive within ${requestTimeoutMs}ms.`,
|
|
747
|
-
{
|
|
748
|
-
code: 'replication_lag_timeout',
|
|
749
|
-
httpStatus: 504,
|
|
750
|
-
details: {
|
|
751
|
-
clientTxId: request.idempotencyKey,
|
|
752
|
-
...(response.correlationId ? { correlationId: response.correlationId } : {}),
|
|
753
|
-
timeoutMs: requestTimeoutMs,
|
|
754
|
-
accepted: true,
|
|
755
|
-
},
|
|
756
|
-
}
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
692
|
/**
|
|
761
693
|
* Replays one byte-identical, idempotent HTTP commit until mutation-log
|
|
762
694
|
* replay reports the source echo as confirmed. `queued` is acceptance only:
|
|
@@ -779,7 +711,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
779
711
|
while (current.status === 'queued') {
|
|
780
712
|
const remaining = deadlineAt === null ? null : deadlineAt - Date.now();
|
|
781
713
|
if (remaining !== null && remaining <= 0) {
|
|
782
|
-
throw replicationLagTimeout(request, current);
|
|
714
|
+
throw replicationLagTimeout(request, current, requestTimeoutMs);
|
|
783
715
|
}
|
|
784
716
|
|
|
785
717
|
const confirmationController = new AbortController();
|
|
@@ -817,7 +749,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
817
749
|
confirmationController.signal.aborted ||
|
|
818
750
|
(deadlineAt !== null && Date.now() >= deadlineAt)
|
|
819
751
|
) {
|
|
820
|
-
throw replicationLagTimeout(request, current);
|
|
752
|
+
throw replicationLagTimeout(request, current, requestTimeoutMs);
|
|
821
753
|
}
|
|
822
754
|
throw error;
|
|
823
755
|
} finally {
|
|
@@ -1128,57 +1060,6 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1128
1060
|
});
|
|
1129
1061
|
}
|
|
1130
1062
|
|
|
1131
|
-
function createClientTxId(idempotencyKey?: string | null): string {
|
|
1132
|
-
if (idempotencyKey && idempotencyKey.length > 0) return idempotencyKey;
|
|
1133
|
-
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
1134
|
-
? crypto.randomUUID()
|
|
1135
|
-
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
function createModelId(modelName: string, idempotencyKey?: string | null): string {
|
|
1139
|
-
if (idempotencyKey) {
|
|
1140
|
-
return uuidv5(`${modelName}:${idempotencyKey}`, 'aa4ba6d4-bf0b-5b38-9c45-116f79a6e548');
|
|
1141
|
-
}
|
|
1142
|
-
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
1143
|
-
? crypto.randomUUID()
|
|
1144
|
-
: `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
function normalizeCommitOperation(
|
|
1148
|
-
op: CommitOperationInput,
|
|
1149
|
-
defaults: Pick<CommitCreateOptions, 'readAt' | 'onStale'>,
|
|
1150
|
-
fence: BatchFence | null,
|
|
1151
|
-
claim: Claim | null,
|
|
1152
|
-
): CommitOperationInput {
|
|
1153
|
-
return {
|
|
1154
|
-
action: op.action,
|
|
1155
|
-
model: op.model,
|
|
1156
|
-
id: op.id ?? null,
|
|
1157
|
-
data: op.data ?? null,
|
|
1158
|
-
where: op.where ?? null,
|
|
1159
|
-
transactionId: op.transactionId ?? null,
|
|
1160
|
-
claimId:
|
|
1161
|
-
op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null),
|
|
1162
|
-
readAt: op.readAt ?? defaults.readAt ?? null,
|
|
1163
|
-
onStale: op.onStale ?? defaults.onStale ?? null,
|
|
1164
|
-
fenceToken: op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null),
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
function normalizeCommitOperations(
|
|
1169
|
-
commitOptions: CommitCreateOptions,
|
|
1170
|
-
fence: BatchFence | null
|
|
1171
|
-
): readonly CommitOperationInput[] {
|
|
1172
|
-
if (commitOptions.operations.length === 0) {
|
|
1173
|
-
throw new AbloValidationError('Commit requires a non-empty `operations` array.', {
|
|
1174
|
-
code: 'commit_operation_required',
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
return commitOptions.operations.map((op) =>
|
|
1178
|
-
normalizeCommitOperation(op, commitOptions, fence, commitOptions.claim ?? null)
|
|
1179
|
-
);
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
1063
|
async function listClaimState(
|
|
1183
1064
|
target?: Partial<ModelTarget>
|
|
1184
1065
|
): Promise<{ active: readonly ModelClaim[]; queue: readonly ModelClaim[] }> {
|
|
@@ -1607,6 +1488,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1607
1488
|
},
|
|
1608
1489
|
`${modelName} ${action}`
|
|
1609
1490
|
);
|
|
1491
|
+
if (action !== 'create') assertWriteTarget(action, modelName, id);
|
|
1610
1492
|
const clientTxId = createClientTxId(options?.idempotencyKey);
|
|
1611
1493
|
const encModel = encodeURIComponent(modelName);
|
|
1612
1494
|
const path =
|
|
@@ -1945,12 +1827,26 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1945
1827
|
if (!claimInput) return run(input);
|
|
1946
1828
|
|
|
1947
1829
|
if (isClaimHandle(claimInput)) {
|
|
1830
|
+
// The lease's watermark travels with it. Unwrapping the handle to a
|
|
1831
|
+
// `claimRef` is what put this here: `mutateModel` reads `readAt` off a
|
|
1832
|
+
// handle on `claim`, and this sets `claim: undefined` before it looks,
|
|
1833
|
+
// so the stale guard was silently dropped on every write that named a
|
|
1834
|
+
// handle. The result was mutual exclusion without lost-update
|
|
1835
|
+
// detection, on the transport agents run, under code that reads as
|
|
1836
|
+
// though it had both. The reactive client binds the same three values
|
|
1837
|
+
// from the claim it holds; a caller's own `readAt`/`onStale` win.
|
|
1948
1838
|
return run({
|
|
1949
1839
|
...input,
|
|
1950
1840
|
claimRef: { id: claimInput.id },
|
|
1951
1841
|
...(claimInput.fenceToken !== undefined
|
|
1952
1842
|
? { fenceToken: claimInput.fenceToken }
|
|
1953
1843
|
: {}),
|
|
1844
|
+
...(input?.readAt === undefined && claimInput.readAt !== undefined
|
|
1845
|
+
? { readAt: claimInput.readAt }
|
|
1846
|
+
: {}),
|
|
1847
|
+
...(input?.onStale === undefined && claimInput.readAt !== undefined
|
|
1848
|
+
? { onStale: 'reject' as const }
|
|
1849
|
+
: {}),
|
|
1954
1850
|
claim: undefined,
|
|
1955
1851
|
});
|
|
1956
1852
|
}
|
|
@@ -2079,6 +1975,60 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
2079
1975
|
return created;
|
|
2080
1976
|
});
|
|
2081
1977
|
},
|
|
1978
|
+
/**
|
|
1979
|
+
* Creates many rows as ONE commit, and returns them.
|
|
1980
|
+
*
|
|
1981
|
+
* Atomic on purpose: a batch that half-lands leaves the caller to work
|
|
1982
|
+
* out which half, and every other write door here declines whole. One
|
|
1983
|
+
* rejected row declines the batch.
|
|
1984
|
+
*
|
|
1985
|
+
* A fresh receipt returns the authoritative rows directly from the
|
|
1986
|
+
* writing transaction. A durable idempotency replay deliberately redacts
|
|
1987
|
+
* those rows, so that path verifies every deterministic id with a point
|
|
1988
|
+
* read and fails loudly if policy prevents a complete answer. Neither
|
|
1989
|
+
* path can report a partial collection as success.
|
|
1990
|
+
*
|
|
1991
|
+
* Ids are resolved here rather than left to the server, so the results
|
|
1992
|
+
* can be returned in the caller's own order rather than the order the
|
|
1993
|
+
* transaction happened to settle them.
|
|
1994
|
+
*/
|
|
1995
|
+
async createMany(
|
|
1996
|
+
params: ModelCreateManyParams<Record<string, unknown>>
|
|
1997
|
+
): Promise<T[]> {
|
|
1998
|
+
if (params.data.length === 0) return [];
|
|
1999
|
+
|
|
2000
|
+
// A claim/stale guard addresses one existing row, so the canonical
|
|
2001
|
+
// batch-create options do not admit those fields at all.
|
|
2002
|
+
const { data, ...options } = params;
|
|
2003
|
+
const ids = data.map(
|
|
2004
|
+
(row, index) =>
|
|
2005
|
+
resolveCreateId(undefined, row) ??
|
|
2006
|
+
createModelId(
|
|
2007
|
+
name,
|
|
2008
|
+
params.idempotencyKey ? `${params.idempotencyKey}:${index}` : null
|
|
2009
|
+
)
|
|
2010
|
+
);
|
|
2011
|
+
const receipt = await commits.create({
|
|
2012
|
+
...options,
|
|
2013
|
+
operations: data.map((row, index) => ({
|
|
2014
|
+
action: 'create' as const,
|
|
2015
|
+
model: name,
|
|
2016
|
+
id: ids[index] ?? null,
|
|
2017
|
+
data: row,
|
|
2018
|
+
})),
|
|
2019
|
+
wait: 'confirmed',
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
return resolveCreatedRows<T>({
|
|
2023
|
+
modelName: name,
|
|
2024
|
+
ids,
|
|
2025
|
+
operationResults: receipt.operationResults,
|
|
2026
|
+
readRow: async (id) => {
|
|
2027
|
+
const read = await retrieveModel<T>(name, { id });
|
|
2028
|
+
return read.data;
|
|
2029
|
+
},
|
|
2030
|
+
});
|
|
2031
|
+
},
|
|
2082
2032
|
update: updateModel,
|
|
2083
2033
|
async delete(params: ModelMutationOptions & { readonly id: string }): Promise<CommitReceipt> {
|
|
2084
2034
|
return withMutationClaim(params.id, params, async (options) => {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { AbloClaimedError, AbloConnectionError } from '../errors.js';
|
|
2
|
+
import { commitReceiptSchema, type CommitReceiptWire } from '../wire/commit.js';
|
|
3
|
+
import type { ClaimHeartbeatReply } from '../wire/claims.js';
|
|
4
|
+
import type { ModelClaim } from '../resources/httpResources.js';
|
|
5
|
+
import { subTarget, streamTarget } from '../coordination/locator.js';
|
|
6
|
+
import { declaredMeta } from '../coordination/claimMeta.js';
|
|
7
|
+
import type { Claim, ClaimHeartbeat } from '../types/streams.js';
|
|
8
|
+
|
|
9
|
+
/** Interpret a heartbeat reply for a lease this handle currently holds. */
|
|
10
|
+
export function heldHeartbeatReply(
|
|
11
|
+
reply: ClaimHeartbeatReply,
|
|
12
|
+
label: string,
|
|
13
|
+
): ClaimHeartbeat {
|
|
14
|
+
if (reply.status === 'held' && typeof reply.expiresAt === 'number') {
|
|
15
|
+
return {
|
|
16
|
+
expiresAt: reply.expiresAt,
|
|
17
|
+
...(reply.queueDepth !== undefined ? { queueDepth: reply.queueDepth } : {}),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
throw new AbloClaimedError(
|
|
21
|
+
`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.`,
|
|
22
|
+
{ code: 'claim_lost' },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseSuccessfulCommitResponse(
|
|
27
|
+
value: unknown,
|
|
28
|
+
idempotencyKey: string,
|
|
29
|
+
): CommitReceiptWire {
|
|
30
|
+
const parsed = commitReceiptSchema.safeParse(value);
|
|
31
|
+
if (!parsed.success || parsed.data.clientTxId !== idempotencyKey) {
|
|
32
|
+
throw new AbloConnectionError(
|
|
33
|
+
'The commit endpoint returned an invalid success receipt; its outcome remains pending and is safe to retry.',
|
|
34
|
+
{
|
|
35
|
+
code: 'commit_no_result',
|
|
36
|
+
cause: parsed.success
|
|
37
|
+
? new Error('Commit receipt clientTxId did not match its idempotency key')
|
|
38
|
+
: parsed.error,
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return parsed.data;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Decode the HTTP claim DTO into the public Claim shape. */
|
|
46
|
+
export function claimFromModelClaim(claim: ModelClaim): Claim {
|
|
47
|
+
const { meta, ...details } = subTarget(claim.target);
|
|
48
|
+
return {
|
|
49
|
+
object: 'claim',
|
|
50
|
+
id: claim.id,
|
|
51
|
+
...(claim.status ? { status: claim.status } : {}),
|
|
52
|
+
description: claim.description ?? 'editing',
|
|
53
|
+
heldBy: claim.actor,
|
|
54
|
+
participantKind: claim.participantKind,
|
|
55
|
+
expiresAt: claim.expiresAt,
|
|
56
|
+
...(claim.position !== undefined ? { position: claim.position } : {}),
|
|
57
|
+
target: {
|
|
58
|
+
...streamTarget(claim.target),
|
|
59
|
+
...details,
|
|
60
|
+
...(meta !== undefined ? { meta: declaredMeta(meta) } : {}),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a caller can rely on about the HTTP surface not moving under it: where
|
|
3
|
+
* the version is written, what a removal looks like before it happens, and how
|
|
4
|
+
* much warning it comes with.
|
|
5
|
+
*
|
|
6
|
+
* An agent decides whether to integrate against an API partly on whether the
|
|
7
|
+
* API can change without telling it. That decision needs facts, not a promise,
|
|
8
|
+
* so each of these is a value some other surface derives from — the path
|
|
9
|
+
* segment every route is mounted under, the header every response carries, and
|
|
10
|
+
* the two standard fields a withdrawal is announced on. {@link API_LIFECYCLE}
|
|
11
|
+
* is the same set written as prose, and it is what the published OpenAPI
|
|
12
|
+
* document's description carries: the policy and the routes it governs are then
|
|
13
|
+
* one artifact, and a reader who has the spec has the policy.
|
|
14
|
+
*
|
|
15
|
+
* Two axes, deliberately separate:
|
|
16
|
+
*
|
|
17
|
+
* - The PATH version ({@link API_PATH_VERSION}) is the breaking axis. It
|
|
18
|
+
* changes only when a shape a caller sends or reads changes incompatibly,
|
|
19
|
+
* and the old segment keeps serving through the notice window.
|
|
20
|
+
* - The CONTRACT date on {@link API_VERSION_HEADER} is the additive axis. It
|
|
21
|
+
* moves when something observable is added — a new error code, a new field
|
|
22
|
+
* — and a caller pinned to an older reading keeps working. Its value is
|
|
23
|
+
* `ERROR_CONTRACT_VERSION`, stamped by the server on every response.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The path segment every route is mounted under: `/api/v1/...`. Callers address
|
|
28
|
+
* this literally, and the OpenAPI document's paths are written from it, so the
|
|
29
|
+
* spec cannot advertise a version the server does not serve.
|
|
30
|
+
*/
|
|
31
|
+
export const API_PATH_VERSION = 'v1';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Carries the date-stamped contract version on every response. A client that
|
|
35
|
+
* records the value it was built against can tell that the server has moved
|
|
36
|
+
* without waiting for a field it does not recognize to break something.
|
|
37
|
+
*/
|
|
38
|
+
export const API_VERSION_HEADER = 'Ablo-Version';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Announces that a route will be withdrawn, as an sf-Date of the moment the
|
|
42
|
+
* deprecation took effect (RFC 9745): `Deprecation: @1774483200`. It appears
|
|
43
|
+
* while the route still works — a deprecated route answers normally.
|
|
44
|
+
*/
|
|
45
|
+
export const API_DEPRECATION_HEADER = 'Deprecation';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The moment the route stops answering, as an HTTP-date (RFC 8594):
|
|
49
|
+
* `Sunset: Tue, 08 Sep 2026 00:00:00 GMT`. Paired with
|
|
50
|
+
* {@link API_DEPRECATION_HEADER}, the two bound the window a caller has.
|
|
51
|
+
*/
|
|
52
|
+
export const API_SUNSET_HEADER = 'Sunset';
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The minimum gap between the two headers above. A number rather than a
|
|
56
|
+
* sentence because it is the part a caller plans against, and because the
|
|
57
|
+
* published policy renders it rather than restating it.
|
|
58
|
+
*/
|
|
59
|
+
export const API_DEPRECATION_NOTICE_DAYS = 180;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The policy, in the words it is published in.
|
|
63
|
+
*
|
|
64
|
+
* Written here rather than in the document that shows it because two documents
|
|
65
|
+
* show it — the OpenAPI description and the site's API page — and a policy
|
|
66
|
+
* stated twice is a policy that will eventually say two things.
|
|
67
|
+
*/
|
|
68
|
+
export const API_LIFECYCLE = `## Versioning
|
|
69
|
+
|
|
70
|
+
Every route is mounted under \`/${API_PATH_VERSION}\`, and that segment is part
|
|
71
|
+
of the address you call. A change that would break a caller — a field removed, a
|
|
72
|
+
type narrowed, a status changed — arrives as a new segment beside this one, never
|
|
73
|
+
as a change to this one. Additive changes do land here: a new field on a
|
|
74
|
+
response, a new optional parameter, a new error code. Ignore what you do not
|
|
75
|
+
recognize and you will not be broken by them.
|
|
76
|
+
|
|
77
|
+
Every response carries \`${API_VERSION_HEADER}\`, a date stamp for the contract
|
|
78
|
+
the server is serving. Record the value your integration was built against and
|
|
79
|
+
compare it if behavior surprises you.
|
|
80
|
+
|
|
81
|
+
## Deprecation
|
|
82
|
+
|
|
83
|
+
A route being withdrawn says so on itself, for at least
|
|
84
|
+
${API_DEPRECATION_NOTICE_DAYS} days before it stops answering:
|
|
85
|
+
|
|
86
|
+
- \`${API_DEPRECATION_HEADER}\` — an sf-Date of when the deprecation took effect
|
|
87
|
+
(RFC 9745). The route still answers normally while this is present.
|
|
88
|
+
- \`${API_SUNSET_HEADER}\` — an HTTP-date of when it stops answering (RFC 8594).
|
|
89
|
+
- \`Link\` with \`rel="deprecation"\` pointing at what to read, and
|
|
90
|
+
\`rel="successor-version"\` at what to call instead when there is one.
|
|
91
|
+
|
|
92
|
+
The same operations are marked \`deprecated: true\` in this document. Treating
|
|
93
|
+
either signal as a build failure is the intended use; nothing is removed without
|
|
94
|
+
both.`;
|
package/src/wire/auth.ts
CHANGED
|
@@ -34,9 +34,12 @@ export const ephemeralKeyRequestSchema = z.object({
|
|
|
34
34
|
/** The participant this session acts as. */
|
|
35
35
|
user: ephemeralKeyUserSchema,
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* `organization:act-as` scope
|
|
39
|
-
*
|
|
37
|
+
* Open the session in a DIFFERENT organization, one the caller does not
|
|
38
|
+
* belong to. Requires the `organization:act-as` scope. This is the identity
|
|
39
|
+
* provider case, where each organization is an account in its own right.
|
|
40
|
+
*
|
|
41
|
+
* A platform serving its own customers does not use this: its customers are
|
|
42
|
+
* rows in its own schema, and a session reaches them through `syncGroups`.
|
|
40
43
|
*/
|
|
41
44
|
organizationId: z.string().min(1).optional(),
|
|
42
45
|
/**
|
package/src/wire/commit.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { z } from 'zod';
|
|
21
|
+
import { logPositionSchema } from '../syncLog/contract.js';
|
|
21
22
|
import {
|
|
22
23
|
onStaleModeSchema,
|
|
23
24
|
MAX_READ_SET_ENTRIES,
|
|
@@ -25,7 +26,6 @@ import {
|
|
|
25
26
|
readSetRowTargetSchema,
|
|
26
27
|
readSetProjectionEntryCount,
|
|
27
28
|
readSetSchema,
|
|
28
|
-
readSetWatermarkSchema,
|
|
29
29
|
participantKindSchema,
|
|
30
30
|
staleNotificationSchema,
|
|
31
31
|
trackDependencyListSchema,
|
|
@@ -61,7 +61,7 @@ export const confirmedCommitStatusSchema = z
|
|
|
61
61
|
.strictObject({
|
|
62
62
|
status: confirmedStatusSchema,
|
|
63
63
|
statusAt: commitTimestampSchema,
|
|
64
|
-
lastSyncId:
|
|
64
|
+
lastSyncId: logPositionSchema,
|
|
65
65
|
correlationId: correlationIdSchema.optional(),
|
|
66
66
|
})
|
|
67
67
|
.refine(({ correlationId, lastSyncId }) => correlationId === undefined || lastSyncId > 0, {
|
|
@@ -375,7 +375,7 @@ export type MutationCommitResult = z.infer<typeof mutationCommitResultSchema>;
|
|
|
375
375
|
export const clientCommitReceiptSchema = z.strictObject({
|
|
376
376
|
id: z.string().min(1),
|
|
377
377
|
status: z.union([queuedStatusSchema, confirmedStatusSchema]),
|
|
378
|
-
lastSyncId:
|
|
378
|
+
lastSyncId: logPositionSchema.optional(),
|
|
379
379
|
notifications: notificationsSchema.optional(),
|
|
380
380
|
missingIds: missingIdsSchema.optional(),
|
|
381
381
|
operationResults: operationResultsSchema.optional(),
|
|
@@ -400,15 +400,30 @@ export const commitOperationControlShape = {
|
|
|
400
400
|
id: z.string().nullish(),
|
|
401
401
|
transactionId: z.string().nullish(),
|
|
402
402
|
claimId: z.string().min(1).nullish(),
|
|
403
|
-
readAt:
|
|
403
|
+
readAt: logPositionSchema.nullish(),
|
|
404
404
|
onStale: onStaleModeSchema.nullish(),
|
|
405
405
|
fenceToken: z.number().nullish(),
|
|
406
406
|
};
|
|
407
407
|
|
|
408
|
+
/** Canonical public spelling of a model operation. */
|
|
409
|
+
export const modelOperationActionSchema = z.enum([
|
|
410
|
+
'create',
|
|
411
|
+
'update',
|
|
412
|
+
'delete',
|
|
413
|
+
'archive',
|
|
414
|
+
'unarchive',
|
|
415
|
+
]);
|
|
416
|
+
export type ModelOperationAction = z.infer<typeof modelOperationActionSchema>;
|
|
417
|
+
|
|
418
|
+
/** Convert a storage operation verb into the canonical lowercase commit action. */
|
|
419
|
+
export function normalizeStorageOperationAction(value: string): ModelOperationAction {
|
|
420
|
+
return modelOperationActionSchema.parse(value.toLowerCase());
|
|
421
|
+
}
|
|
422
|
+
|
|
408
423
|
/** One write inside a commit, in the canonical spelling. */
|
|
409
424
|
export const commitOperationBodySchema = z.object({
|
|
410
425
|
...commitOperationControlShape,
|
|
411
|
-
action:
|
|
426
|
+
action: modelOperationActionSchema,
|
|
412
427
|
model: z.string(),
|
|
413
428
|
data: z.record(z.string(), z.unknown()).nullish(),
|
|
414
429
|
where: z.record(z.string().min(1), z.unknown()).nullish(),
|
package/src/wire/index.ts
CHANGED
|
@@ -120,6 +120,8 @@ export {
|
|
|
120
120
|
// The request side of the same boundary — one definition, which the published
|
|
121
121
|
// OpenAPI reference derives from rather than describing separately.
|
|
122
122
|
commitOperationControlShape,
|
|
123
|
+
modelOperationActionSchema,
|
|
124
|
+
normalizeStorageOperationAction,
|
|
123
125
|
commitOperationBodySchema,
|
|
124
126
|
commitActorSchema,
|
|
125
127
|
commitAttemptSchema,
|
|
@@ -133,6 +135,7 @@ export {
|
|
|
133
135
|
commitRecordListSchema,
|
|
134
136
|
} from './commit.js';
|
|
135
137
|
export type {
|
|
138
|
+
ModelOperationAction,
|
|
136
139
|
CommitOperationBody,
|
|
137
140
|
CommitOperationResult,
|
|
138
141
|
CommitActor,
|
|
@@ -408,3 +411,31 @@ export type {
|
|
|
408
411
|
CapabilityRequest,
|
|
409
412
|
CapabilityMintResponse,
|
|
410
413
|
} from './auth.js';
|
|
414
|
+
|
|
415
|
+
// How a rate limit is stated on a response, so a caller can pace itself rather
|
|
416
|
+
// than discover the ceiling by hitting it. The field names and their Structured
|
|
417
|
+
// Fields spellings live in one module because every producer — the engine's
|
|
418
|
+
// per-key limiter and the public docs surfaces both — has to spell them the
|
|
419
|
+
// same way for a client to read either.
|
|
420
|
+
export {
|
|
421
|
+
RATE_LIMIT_HEADER,
|
|
422
|
+
RATE_LIMIT_POLICY_HEADER,
|
|
423
|
+
RETRY_AFTER_HEADER,
|
|
424
|
+
rateLimitField,
|
|
425
|
+
rateLimitPolicyField,
|
|
426
|
+
rateLimitHeaders,
|
|
427
|
+
} from './rateLimit.js';
|
|
428
|
+
export type { QuotaPolicy, ServiceLimit, RateLimitSignal } from './rateLimit.js';
|
|
429
|
+
|
|
430
|
+
// What a caller can rely on about this surface not moving under it: the path
|
|
431
|
+
// segment the routes live under, the header carrying the contract date, and the
|
|
432
|
+
// two standard fields a withdrawal is announced on. The published OpenAPI
|
|
433
|
+
// description renders `API_LIFECYCLE` rather than restating it.
|
|
434
|
+
export {
|
|
435
|
+
API_PATH_VERSION,
|
|
436
|
+
API_VERSION_HEADER,
|
|
437
|
+
API_DEPRECATION_HEADER,
|
|
438
|
+
API_SUNSET_HEADER,
|
|
439
|
+
API_DEPRECATION_NOTICE_DAYS,
|
|
440
|
+
API_LIFECYCLE,
|
|
441
|
+
} from './apiLifecycle.js';
|
|
@@ -16,12 +16,12 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { z } from 'zod';
|
|
19
|
+
import { logPositionSchema } from '../syncLog/contract.js';
|
|
19
20
|
import {
|
|
20
21
|
onStaleModeSchema,
|
|
21
22
|
MAX_READ_SET_ENTRIES,
|
|
22
23
|
readDependencyListSchema,
|
|
23
24
|
readSetProjectionEntryCount,
|
|
24
|
-
readSetWatermarkSchema,
|
|
25
25
|
trackDependencyListSchema,
|
|
26
26
|
} from '../coordination/schema.js';
|
|
27
27
|
|
|
@@ -50,7 +50,7 @@ export const modelMutationRequestSchema = z.object({
|
|
|
50
50
|
* the read. Ablo rejects the write if the row moved in between, which is
|
|
51
51
|
* what makes read → decide → write safe without a lock across the deciding.
|
|
52
52
|
*/
|
|
53
|
-
readAt:
|
|
53
|
+
readAt: logPositionSchema.nullish(),
|
|
54
54
|
/** Commit-lifetime dependencies checked with the single model operation. */
|
|
55
55
|
reads: readDependencyListSchema.nullish(),
|
|
56
56
|
/** Durable dependencies registered with the single model operation. */
|