@abloatai/transaction 0.48.0 → 0.50.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 +7 -7
- package/README.md +1 -1
- package/dist/ai-sdk/modelTools.d.ts +1 -1
- package/dist/ai-sdk/modelTools.js +1 -1
- package/dist/ai-sdk/updateTool.d.ts +2 -2
- package/dist/ai-sdk/updateTool.d.ts.map +1 -1
- package/dist/auth/bootstrapScope.d.ts +1 -1
- package/dist/auth/capability.d.ts +32 -10
- package/dist/auth/capability.d.ts.map +1 -1
- package/dist/auth/capability.js +9 -3
- package/dist/auth/capability.js.map +1 -1
- package/dist/auth/capabilityLifecycle.d.ts +2 -2
- package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
- package/dist/auth/capabilityLifecycle.js +3 -2
- package/dist/auth/capabilityLifecycle.js.map +1 -1
- package/dist/auth/identity.d.ts +3 -0
- package/dist/auth/identity.d.ts.map +1 -1
- package/dist/auth/identity.js +13 -1
- package/dist/auth/identity.js.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- 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/auth/schemas.d.ts +18 -1
- package/dist/auth/schemas.d.ts.map +1 -1
- package/dist/auth/schemas.js +3 -1
- package/dist/auth/schemas.js.map +1 -1
- package/dist/auth/sessionMint.d.ts.map +1 -1
- package/dist/auth/sessionMint.js +0 -1
- package/dist/auth/sessionMint.js.map +1 -1
- package/dist/commitRecordRuntime.d.ts +30 -0
- package/dist/commitRecordRuntime.d.ts.map +1 -0
- package/dist/commitRecordRuntime.js +188 -0
- package/dist/commitRecordRuntime.js.map +1 -0
- package/dist/coordination/index.d.ts +3 -3
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +2 -2
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/locator.d.ts +5 -0
- package/dist/coordination/locator.d.ts.map +1 -1
- package/dist/coordination/locator.js +12 -0
- package/dist/coordination/locator.js.map +1 -1
- package/dist/coordination/schema.d.ts +205 -15
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +88 -47
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/targetConflict.js +1 -1
- package/dist/durableWrites.d.ts +3 -3
- package/dist/durableWrites.d.ts.map +1 -1
- package/dist/durableWrites.js +1 -1
- package/dist/errorCodes.d.ts +20 -1
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +44 -10
- package/dist/errorCodes.js.map +1 -1
- package/dist/errorObservation.d.ts +82 -0
- package/dist/errorObservation.d.ts.map +1 -0
- package/dist/errorObservation.js +101 -0
- package/dist/errorObservation.js.map +1 -0
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/read-set.d.ts +8 -0
- package/dist/internal/read-set.d.ts.map +1 -0
- package/dist/internal/read-set.js +7 -0
- package/dist/internal/read-set.js.map +1 -0
- package/dist/logger.d.ts +1 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +1 -1
- package/dist/readSetContext.d.ts +65 -0
- package/dist/readSetContext.d.ts.map +1 -0
- package/dist/readSetContext.js +121 -0
- package/dist/readSetContext.js.map +1 -0
- package/dist/resources/functionalUpdate.d.ts +13 -3
- package/dist/resources/functionalUpdate.d.ts.map +1 -1
- package/dist/resources/functionalUpdate.js.map +1 -1
- package/dist/resources/httpResources.d.ts +23 -7
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/modelOperations.d.ts +2 -2
- package/dist/resources/modelOperations.js +1 -1
- package/dist/resources/mutationOptions.d.ts +1 -1
- package/dist/resources/mutationOptions.js +1 -1
- package/dist/resources/where.d.ts +1 -1
- package/dist/resources/where.js +1 -1
- package/dist/resources/writeOptionsSchema.d.ts +39 -4
- package/dist/resources/writeOptionsSchema.d.ts.map +1 -1
- package/dist/resources/writeOptionsSchema.js +12 -6
- package/dist/resources/writeOptionsSchema.js.map +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +41 -7
- package/dist/schema/openapi.js.map +1 -1
- package/dist/server/commit.d.ts +7 -1
- package/dist/server/commit.d.ts.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/source/adapters/kysely.d.ts.map +1 -1
- package/dist/source/adapters/kysely.js +11 -4
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.js +22 -4
- package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
- package/dist/source/contract.d.ts +2 -0
- package/dist/source/contract.d.ts.map +1 -1
- package/dist/source/types.d.ts +1 -1
- package/dist/testing/fixtures/httpResponses.d.ts +30 -1
- package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
- package/dist/testing/fixtures/httpResponses.js +48 -1
- package/dist/testing/fixtures/httpResponses.js.map +1 -1
- package/dist/transactionLayer.d.ts +1 -1
- package/dist/transactions/{settlement → confirmation}/commitEnvelope.d.ts +4 -2
- package/dist/transactions/confirmation/commitEnvelope.d.ts.map +1 -0
- package/dist/transactions/{settlement → confirmation}/commitEnvelope.js +8 -3
- package/dist/transactions/confirmation/commitEnvelope.js.map +1 -0
- package/dist/transactions/confirmation/httpCommitEnvelope.d.ts.map +1 -0
- package/dist/transactions/confirmation/httpCommitEnvelope.js.map +1 -0
- package/dist/transactions/confirmation/idempotencyKey.d.ts.map +1 -0
- package/dist/transactions/confirmation/idempotencyKey.js.map +1 -0
- package/dist/transactions/{settlement → confirmation}/pendingWrite.d.ts +3 -2
- package/dist/transactions/confirmation/pendingWrite.d.ts.map +1 -0
- package/dist/transactions/confirmation/pendingWrite.js.map +1 -0
- package/dist/transport/commitFrames.d.ts +1 -0
- package/dist/transport/commitFrames.d.ts.map +1 -1
- package/dist/transport/commitFrames.js +8 -7
- package/dist/transport/commitFrames.js.map +1 -1
- package/dist/transport/connectionManager.d.ts +1 -1
- package/dist/transport/connectionManager.js +1 -1
- package/dist/transport/httpClient.d.ts +23 -9
- package/dist/transport/httpClient.d.ts.map +1 -1
- package/dist/transport/httpClient.js +137 -15
- package/dist/transport/httpClient.js.map +1 -1
- package/dist/transport/httpOptions.d.ts +9 -1
- package/dist/transport/httpOptions.d.ts.map +1 -1
- package/dist/transport/httpTransport.d.ts +3 -0
- package/dist/transport/httpTransport.d.ts.map +1 -1
- package/dist/transport/httpTransport.js +109 -22
- package/dist/transport/httpTransport.js.map +1 -1
- package/dist/transport/wsFrameHandlers.d.ts +3 -1
- package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
- package/dist/transport/wsFrameHandlers.js +38 -17
- package/dist/transport/wsFrameHandlers.js.map +1 -1
- package/dist/transport/wsTransport.d.ts +3 -0
- package/dist/transport/wsTransport.d.ts.map +1 -1
- package/dist/transport/wsTransport.js +32 -1
- package/dist/transport/wsTransport.js.map +1 -1
- package/dist/utils/json.d.ts +1 -1
- package/dist/utils/json.js +1 -1
- package/dist/wire/accountResponses.d.ts +1 -1
- package/dist/wire/auth.d.ts +2 -2
- package/dist/wire/commit.d.ts +1652 -213
- package/dist/wire/commit.d.ts.map +1 -1
- package/dist/wire/commit.js +196 -132
- package/dist/wire/commit.js.map +1 -1
- package/dist/wire/delta.d.ts +1 -1
- package/dist/wire/errorEnvelope.d.ts +1 -0
- package/dist/wire/errorEnvelope.d.ts.map +1 -1
- package/dist/wire/errorEnvelope.js +1 -0
- package/dist/wire/errorEnvelope.js.map +1 -1
- package/dist/wire/feedCursor.d.ts +1 -1
- package/dist/wire/feedCursor.js +1 -1
- package/dist/wire/feedEvent.d.ts +3 -3
- package/dist/wire/frames.d.ts +8 -4
- package/dist/wire/frames.d.ts.map +1 -1
- package/dist/wire/frames.js +6 -3
- package/dist/wire/frames.js.map +1 -1
- package/dist/wire/inboundFrames.d.ts +44 -4
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/index.d.ts +6 -4
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +6 -5
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/modelMutations.d.ts +35 -0
- package/dist/wire/modelMutations.d.ts.map +1 -1
- package/dist/wire/modelMutations.js +9 -2
- package/dist/wire/modelMutations.js.map +1 -1
- package/dist/wire/modelResponses.d.ts +12 -1
- package/dist/wire/modelResponses.d.ts.map +1 -1
- package/dist/wire/modelResponses.js +12 -1
- package/dist/wire/modelResponses.js.map +1 -1
- package/package.json +12 -2
- package/src/ai-sdk/modelTools.ts +1 -1
- package/src/ai-sdk/updateTool.ts +2 -2
- package/src/auth/bootstrapScope.ts +1 -1
- package/src/auth/capability.ts +13 -4
- package/src/auth/capabilityLifecycle.ts +3 -2
- package/src/auth/identity.ts +16 -1
- package/src/auth/index.ts +2 -0
- package/src/auth/schemas.ts +3 -0
- package/src/auth/sessionMint.ts +0 -1
- package/src/commitRecordRuntime.ts +245 -0
- package/src/coordination/index.ts +24 -1
- package/src/coordination/locator.ts +17 -0
- package/src/coordination/schema.ts +114 -49
- package/src/coordination/targetConflict.ts +1 -1
- package/src/durableWrites.ts +3 -3
- package/src/errorCodes.ts +107 -11
- package/src/errorObservation.ts +121 -0
- package/src/errors.ts +5 -1
- package/src/index.ts +8 -1
- package/src/internal/read-set.ts +23 -0
- package/src/logger.ts +1 -1
- package/src/observability.ts +1 -1
- package/src/readSetContext.ts +195 -0
- package/src/resources/functionalUpdate.ts +16 -4
- package/src/resources/httpResources.ts +27 -8
- package/src/resources/modelOperations.ts +2 -2
- package/src/resources/mutationOptions.ts +1 -1
- package/src/resources/where.ts +1 -1
- package/src/resources/writeOptionsSchema.ts +30 -12
- package/src/schema/index.ts +1 -1
- package/src/schema/openapi.ts +48 -7
- package/src/server/commit.ts +7 -1
- package/src/server/index.ts +5 -1
- package/src/source/adapters/kysely.ts +12 -4
- package/src/source/adapters/kyselyMutationCore.ts +28 -4
- package/src/source/types.ts +1 -1
- package/src/testing/fixtures/httpResponses.ts +60 -1
- package/src/transactionLayer.ts +1 -1
- package/src/transactions/{settlement → confirmation}/commitEnvelope.ts +13 -3
- package/src/transport/commitFrames.ts +7 -5
- package/src/transport/connectionManager.ts +1 -1
- package/src/transport/httpClient.ts +260 -29
- package/src/transport/httpOptions.ts +9 -1
- package/src/transport/httpTransport.ts +139 -27
- package/src/transport/wsFrameHandlers.ts +46 -8
- package/src/transport/wsTransport.ts +42 -1
- package/src/utils/json.ts +1 -1
- package/src/wire/commit.ts +248 -147
- package/src/wire/errorEnvelope.ts +1 -0
- package/src/wire/feedCursor.ts +1 -1
- package/src/wire/frames.ts +9 -4
- package/src/wire/index.ts +38 -5
- package/src/wire/modelMutations.ts +16 -2
- package/src/wire/modelResponses.ts +14 -1
- package/dist/transactions/settlement/commitEnvelope.d.ts.map +0 -1
- package/dist/transactions/settlement/commitEnvelope.js.map +0 -1
- package/dist/transactions/settlement/httpCommitEnvelope.d.ts.map +0 -1
- package/dist/transactions/settlement/httpCommitEnvelope.js.map +0 -1
- package/dist/transactions/settlement/idempotencyKey.d.ts.map +0 -1
- package/dist/transactions/settlement/idempotencyKey.js.map +0 -1
- package/dist/transactions/settlement/pendingWrite.d.ts.map +0 -1
- package/dist/transactions/settlement/pendingWrite.js.map +0 -1
- /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.d.ts +0 -0
- /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.js +0 -0
- /package/dist/transactions/{settlement → confirmation}/idempotencyKey.d.ts +0 -0
- /package/dist/transactions/{settlement → confirmation}/idempotencyKey.js +0 -0
- /package/dist/transactions/{settlement → confirmation}/pendingWrite.js +0 -0
- /package/src/transactions/{settlement → confirmation}/httpCommitEnvelope.ts +0 -0
- /package/src/transactions/{settlement → confirmation}/idempotencyKey.ts +0 -0
- /package/src/transactions/{settlement → confirmation}/pendingWrite.ts +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
hasWireCode,
|
|
5
5
|
translateHttpError,
|
|
6
6
|
} from '../errors.js';
|
|
7
|
-
import {
|
|
7
|
+
import { effectiveAuthoritySchema } from './capability.js';
|
|
8
8
|
import type {
|
|
9
9
|
RotateSessionParams,
|
|
10
10
|
SessionRevocation,
|
|
@@ -27,7 +27,8 @@ export const capabilityRotationResponseSchema = z.object({
|
|
|
27
27
|
token: z.string().trim().min(1),
|
|
28
28
|
expiresAt: z.iso.datetime().nullable(),
|
|
29
29
|
organizationId: z.string().min(1),
|
|
30
|
-
|
|
30
|
+
branchRoot: z.boolean().default(false),
|
|
31
|
+
scope: effectiveAuthoritySchema,
|
|
31
32
|
rotatedFrom: z.object({
|
|
32
33
|
capabilityId: z.string().min(1),
|
|
33
34
|
expiresAt: z.iso.datetime(),
|
package/src/auth/identity.ts
CHANGED
|
@@ -34,6 +34,7 @@ import type { AuthCredentialSource } from '../auth/credentialSource.js';
|
|
|
34
34
|
import type { CredentialProvider } from './credentialResult.js';
|
|
35
35
|
import { resolveApiKeyValue, resolveBootstrapBaseUrl } from './apiKey.js';
|
|
36
36
|
import type { DeliveryPartitionRoute } from './deliveryPartition.js';
|
|
37
|
+
import type { EffectiveAuthority } from './capability.js';
|
|
37
38
|
|
|
38
39
|
export interface IdentityResolveInput {
|
|
39
40
|
readonly options: {
|
|
@@ -71,6 +72,8 @@ export interface ResolvedIdentity {
|
|
|
71
72
|
readonly syncGroups: readonly string[] | undefined;
|
|
72
73
|
readonly participantKind: ParticipantKind;
|
|
73
74
|
readonly deliveryPartition: DeliveryPartitionRoute | null;
|
|
75
|
+
/** Effective authority confirmed by the server (or explicit self-hosted configuration). */
|
|
76
|
+
readonly authority: EffectiveAuthority;
|
|
74
77
|
/** Set only on the hosted-cloud path; the caller keeps it to stop refreshes on shutdown. */
|
|
75
78
|
readonly refreshScheduler: RefreshScheduler | null;
|
|
76
79
|
}
|
|
@@ -195,6 +198,16 @@ export async function resolveParticipantIdentity(
|
|
|
195
198
|
syncGroups: options.syncGroups,
|
|
196
199
|
participantKind: kind,
|
|
197
200
|
deliveryPartition: null,
|
|
201
|
+
authority: {
|
|
202
|
+
organizationId: accountScope,
|
|
203
|
+
projectId: accountScope,
|
|
204
|
+
branchId: internalOptions.branchId ?? null,
|
|
205
|
+
participantKind: kind,
|
|
206
|
+
participantId: userId,
|
|
207
|
+
operations: [],
|
|
208
|
+
syncGroups: options.syncGroups ?? [],
|
|
209
|
+
deliveryPartition: null,
|
|
210
|
+
},
|
|
198
211
|
refreshScheduler: null,
|
|
199
212
|
};
|
|
200
213
|
}
|
|
@@ -246,6 +259,7 @@ async function resolveViaIdentity(
|
|
|
246
259
|
syncGroups: mergedSyncGroups,
|
|
247
260
|
participantKind: identity.participantKind,
|
|
248
261
|
deliveryPartition: identity.deliveryPartition,
|
|
262
|
+
authority: identity.authority,
|
|
249
263
|
refreshScheduler: null,
|
|
250
264
|
};
|
|
251
265
|
}
|
|
@@ -322,7 +336,7 @@ async function resolveHosted(input: HostedInput): Promise<ResolvedIdentity> {
|
|
|
322
336
|
accountScope: exchange.scope.organizationId,
|
|
323
337
|
projectId: exchange.scope.projectId ?? exchange.scope.organizationId,
|
|
324
338
|
branchId: exchange.scope.branchId,
|
|
325
|
-
branchRoot: exchange.
|
|
339
|
+
branchRoot: exchange.branchRoot,
|
|
326
340
|
// teamIds isn't needed because the server already encoded
|
|
327
341
|
// team-level access into scope.syncGroups.
|
|
328
342
|
teamIds: undefined,
|
|
@@ -330,6 +344,7 @@ async function resolveHosted(input: HostedInput): Promise<ResolvedIdentity> {
|
|
|
330
344
|
syncGroups: exchange.scope.syncGroups,
|
|
331
345
|
participantKind: input.kind,
|
|
332
346
|
deliveryPartition: exchange.scope.deliveryPartition,
|
|
347
|
+
authority: exchange.scope,
|
|
333
348
|
refreshScheduler,
|
|
334
349
|
};
|
|
335
350
|
}
|
package/src/auth/index.ts
CHANGED
|
@@ -74,6 +74,7 @@ export {
|
|
|
74
74
|
capabilityOperationSchema,
|
|
75
75
|
capabilityCanSchema,
|
|
76
76
|
capabilityCanSchemaFor,
|
|
77
|
+
effectiveAuthoritySchema,
|
|
77
78
|
capabilityScopeSchema,
|
|
78
79
|
capabilityRequestSchema,
|
|
79
80
|
capabilityMintResponseSchema,
|
|
@@ -88,6 +89,7 @@ export type {
|
|
|
88
89
|
CapabilityOperation,
|
|
89
90
|
CapabilityCan,
|
|
90
91
|
CapabilityGrant,
|
|
92
|
+
EffectiveAuthority,
|
|
91
93
|
CapabilityScope,
|
|
92
94
|
CapabilityMintResponse,
|
|
93
95
|
CapabilityRequest,
|
package/src/auth/schemas.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { z } from 'zod';
|
|
|
2
2
|
import { participantKindSchema } from '../coordination/schema.js';
|
|
3
3
|
import {
|
|
4
4
|
capabilityMintResponseSchema,
|
|
5
|
+
effectiveAuthoritySchema,
|
|
5
6
|
grantedOperationSchema,
|
|
6
7
|
type CapabilityMintResponse,
|
|
7
8
|
} from './capability.js';
|
|
@@ -26,6 +27,8 @@ export const IdentityResolveResponseSchema = z.object({
|
|
|
26
27
|
branchRoot: z.boolean().default(false),
|
|
27
28
|
syncGroups: z.array(z.string()),
|
|
28
29
|
deliveryPartition: deliveryPartitionRouteSchema.nullable().default(null),
|
|
30
|
+
/** Canonical server-confirmed authority of the authenticated credential. */
|
|
31
|
+
authority: effectiveAuthoritySchema,
|
|
29
32
|
userMeta: z.record(z.string(), z.unknown()),
|
|
30
33
|
});
|
|
31
34
|
|
package/src/auth/sessionMint.ts
CHANGED
|
@@ -93,7 +93,6 @@ export async function mintSession<S extends SchemaRecord>(
|
|
|
93
93
|
organizationId: res.organizationId,
|
|
94
94
|
projectId: res.projectId,
|
|
95
95
|
branchId: res.branchId,
|
|
96
|
-
branchRoot: res.branchRoot,
|
|
97
96
|
syncGroups: res.syncGroups,
|
|
98
97
|
operations: res.operations,
|
|
99
98
|
participantKind: 'user',
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_TRACK_ON_STALE,
|
|
3
|
+
type CommitReadSetEntry,
|
|
4
|
+
type ReadDependency,
|
|
5
|
+
type ReadSetEntry,
|
|
6
|
+
type TrackDependency,
|
|
7
|
+
} from './coordination/schema.js';
|
|
8
|
+
import {
|
|
9
|
+
commitRecordIdentity,
|
|
10
|
+
publishCommitRecord,
|
|
11
|
+
type ReadSetContext,
|
|
12
|
+
} from './readSetContext.js';
|
|
13
|
+
import {
|
|
14
|
+
commitRecordSchema,
|
|
15
|
+
type CommitOperationBody,
|
|
16
|
+
type CommitReceiptWire,
|
|
17
|
+
} from './wire/commit.js';
|
|
18
|
+
|
|
19
|
+
interface HttpCommitObservation {
|
|
20
|
+
readonly receipt: CommitReceiptWire;
|
|
21
|
+
readonly method: string;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly body: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WebSocketCommitObservation {
|
|
27
|
+
readonly receipt: CommitReceiptWire;
|
|
28
|
+
readonly operations: readonly {
|
|
29
|
+
readonly type: string;
|
|
30
|
+
readonly model: string;
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly input?: Record<string, unknown>;
|
|
33
|
+
readonly claimId?: string | null;
|
|
34
|
+
readonly readAt?: number | null;
|
|
35
|
+
readonly onStale?: CommitOperationBody['onStale'] | null;
|
|
36
|
+
readonly fenceToken?: number | null;
|
|
37
|
+
}[];
|
|
38
|
+
readonly reads?: readonly ReadDependency[] | null;
|
|
39
|
+
readonly track?: readonly TrackDependency[] | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function commitEntry(dependency: ReadDependency): CommitReadSetEntry {
|
|
43
|
+
return {
|
|
44
|
+
target: 'group' in dependency
|
|
45
|
+
? { scope: 'group', group: dependency.group }
|
|
46
|
+
: {
|
|
47
|
+
scope: 'row',
|
|
48
|
+
model: dependency.model,
|
|
49
|
+
id: dependency.id,
|
|
50
|
+
...(dependency.fields ? { fields: dependency.fields } : {}),
|
|
51
|
+
},
|
|
52
|
+
watermark: dependency.readAt,
|
|
53
|
+
lifetime: 'commit',
|
|
54
|
+
onStale: dependency.onStale ?? 'reject',
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function persistedEntry(
|
|
59
|
+
dependency: TrackDependency,
|
|
60
|
+
defaultWatermark: number,
|
|
61
|
+
): ReadSetEntry {
|
|
62
|
+
return {
|
|
63
|
+
target: 'group' in dependency
|
|
64
|
+
? { scope: 'group', group: dependency.group }
|
|
65
|
+
: { scope: 'row', model: dependency.model, id: dependency.id },
|
|
66
|
+
watermark: dependency.readAt ?? defaultWatermark,
|
|
67
|
+
lifetime: 'persisted',
|
|
68
|
+
onStale: dependency.onStale ?? DEFAULT_TRACK_ON_STALE,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function bodyRecord(value: unknown): Record<string, unknown> {
|
|
73
|
+
return typeof value === 'object' && value !== null
|
|
74
|
+
? value as Record<string, unknown>
|
|
75
|
+
: {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function retainedOperations(operations: readonly CommitOperationBody[]) {
|
|
79
|
+
return operations.map(({ data: _data, ...operation }) => ({
|
|
80
|
+
...operation,
|
|
81
|
+
data: { retention: 'redacted' as const },
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function commitEvidence(
|
|
86
|
+
receipt: CommitReceiptWire,
|
|
87
|
+
operations: readonly CommitOperationBody[],
|
|
88
|
+
transport: 'http' | 'websocket',
|
|
89
|
+
) {
|
|
90
|
+
return {
|
|
91
|
+
attempts: [{
|
|
92
|
+
id: receipt.clientTxId,
|
|
93
|
+
observedAt: receipt.createdAt,
|
|
94
|
+
transport,
|
|
95
|
+
kind: 'execution' as const,
|
|
96
|
+
}],
|
|
97
|
+
actor: {
|
|
98
|
+
kind: receipt.authority.participantKind,
|
|
99
|
+
id: receipt.authority.participantId,
|
|
100
|
+
},
|
|
101
|
+
// A receipt does not prove which live claim grant the server matched.
|
|
102
|
+
// Durable server records populate this from the claim gate instead.
|
|
103
|
+
claims: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function operationFromModelRequest(
|
|
108
|
+
observation: HttpCommitObservation,
|
|
109
|
+
body: Record<string, unknown>,
|
|
110
|
+
): CommitOperationBody[] {
|
|
111
|
+
const segments = observation.path.split('/').filter(Boolean);
|
|
112
|
+
const modelIndex = segments.indexOf('models');
|
|
113
|
+
if (modelIndex < 0) return [];
|
|
114
|
+
const model = decodeURIComponent(segments[modelIndex + 1] ?? '');
|
|
115
|
+
const pathId = segments[modelIndex + 2];
|
|
116
|
+
const action = observation.method === 'POST'
|
|
117
|
+
? 'create'
|
|
118
|
+
: observation.method === 'DELETE'
|
|
119
|
+
? 'delete'
|
|
120
|
+
: 'update';
|
|
121
|
+
return [{
|
|
122
|
+
action,
|
|
123
|
+
model,
|
|
124
|
+
...(pathId ? { id: decodeURIComponent(pathId) } : typeof body.id === 'string' ? { id: body.id } : {}),
|
|
125
|
+
...(body.data && typeof body.data === 'object'
|
|
126
|
+
? { data: body.data as Record<string, unknown> }
|
|
127
|
+
: {}),
|
|
128
|
+
...(typeof body.readAt === 'number' ? { readAt: body.readAt } : {}),
|
|
129
|
+
...(typeof body.onStale === 'string'
|
|
130
|
+
? { onStale: body.onStale as CommitOperationBody['onStale'] }
|
|
131
|
+
: {}),
|
|
132
|
+
...(typeof body.fenceToken === 'number' ? { fenceToken: body.fenceToken } : {}),
|
|
133
|
+
...(typeof body.claim === 'string' ? { claimId: body.claim } : {}),
|
|
134
|
+
}];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @internal Joins exact HTTP request and receipt projections. */
|
|
138
|
+
export function recordHttpCommitReceipt(
|
|
139
|
+
context: ReadSetContext | undefined,
|
|
140
|
+
observation: HttpCommitObservation,
|
|
141
|
+
): void {
|
|
142
|
+
if (!context?.getStore()) return;
|
|
143
|
+
const body = bodyRecord(observation.body);
|
|
144
|
+
const operations = Array.isArray(body.operations)
|
|
145
|
+
? body.operations as CommitOperationBody[]
|
|
146
|
+
: operationFromModelRequest(observation, body);
|
|
147
|
+
const reads = Array.isArray(body.reads) ? body.reads as ReadDependency[] : [];
|
|
148
|
+
const track = Array.isArray(body.track) ? body.track as TrackDependency[] : [];
|
|
149
|
+
const readSet: ReadSetEntry[] = reads.map(commitEntry);
|
|
150
|
+
for (const operation of operations) {
|
|
151
|
+
if (typeof operation.readAt !== 'number' || !operation.id) continue;
|
|
152
|
+
readSet.push(commitEntry({
|
|
153
|
+
model: operation.model,
|
|
154
|
+
id: operation.id,
|
|
155
|
+
readAt: operation.readAt,
|
|
156
|
+
onStale: operation.onStale ?? 'reject',
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
readSet.push(...track.map((entry) => persistedEntry(entry, observation.receipt.lastSyncId)));
|
|
160
|
+
|
|
161
|
+
const record = commitRecordSchema.parse({
|
|
162
|
+
id: commitRecordIdentity(context, observation.receipt.clientTxId).id,
|
|
163
|
+
createdAt: observation.receipt.createdAt,
|
|
164
|
+
status: observation.receipt.status,
|
|
165
|
+
statusAt: observation.receipt.statusAt,
|
|
166
|
+
lastSyncId: observation.receipt.lastSyncId,
|
|
167
|
+
...(observation.receipt.correlationId
|
|
168
|
+
? { correlationId: observation.receipt.correlationId }
|
|
169
|
+
: {}),
|
|
170
|
+
readSet,
|
|
171
|
+
operations: retainedOperations(operations),
|
|
172
|
+
...commitEvidence(observation.receipt, operations, 'http'),
|
|
173
|
+
authority: observation.receipt.authority,
|
|
174
|
+
receipt: {
|
|
175
|
+
clientTxId: observation.receipt.clientTxId,
|
|
176
|
+
serverTxId: observation.receipt.serverTxId,
|
|
177
|
+
ops: observation.receipt.ops,
|
|
178
|
+
...(observation.receipt.notifications
|
|
179
|
+
? { notifications: observation.receipt.notifications }
|
|
180
|
+
: {}),
|
|
181
|
+
...(observation.receipt.missingIds
|
|
182
|
+
? { missingIds: observation.receipt.missingIds }
|
|
183
|
+
: {}),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
publishCommitRecord(context, record);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @internal Joins an exact commit frame and its authoritative WS receipt. */
|
|
190
|
+
export function recordWebSocketCommitReceipt(
|
|
191
|
+
context: ReadSetContext | undefined,
|
|
192
|
+
observation: WebSocketCommitObservation,
|
|
193
|
+
): void {
|
|
194
|
+
if (!context?.getStore()) return;
|
|
195
|
+
const operations: CommitOperationBody[] = observation.operations.map((operation) => ({
|
|
196
|
+
action: operation.type.toLowerCase() as CommitOperationBody['action'],
|
|
197
|
+
model: operation.model,
|
|
198
|
+
id: operation.id,
|
|
199
|
+
...(operation.input ? { data: operation.input } : {}),
|
|
200
|
+
...(operation.claimId ? { claimId: operation.claimId } : {}),
|
|
201
|
+
...(typeof operation.readAt === 'number' ? { readAt: operation.readAt } : {}),
|
|
202
|
+
...(operation.onStale ? { onStale: operation.onStale } : {}),
|
|
203
|
+
...(typeof operation.fenceToken === 'number'
|
|
204
|
+
? { fenceToken: operation.fenceToken }
|
|
205
|
+
: {}),
|
|
206
|
+
}));
|
|
207
|
+
const readSet: ReadSetEntry[] = (observation.reads ?? []).map(commitEntry);
|
|
208
|
+
for (const operation of operations) {
|
|
209
|
+
if (typeof operation.readAt !== 'number' || !operation.id) continue;
|
|
210
|
+
readSet.push(commitEntry({
|
|
211
|
+
model: operation.model,
|
|
212
|
+
id: operation.id,
|
|
213
|
+
readAt: operation.readAt,
|
|
214
|
+
onStale: operation.onStale ?? 'reject',
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
readSet.push(...(observation.track ?? []).map((entry) =>
|
|
218
|
+
persistedEntry(entry, observation.receipt.lastSyncId)));
|
|
219
|
+
const record = commitRecordSchema.parse({
|
|
220
|
+
id: commitRecordIdentity(context, observation.receipt.clientTxId).id,
|
|
221
|
+
createdAt: observation.receipt.createdAt,
|
|
222
|
+
status: observation.receipt.status,
|
|
223
|
+
statusAt: observation.receipt.statusAt,
|
|
224
|
+
lastSyncId: observation.receipt.lastSyncId,
|
|
225
|
+
...(observation.receipt.correlationId
|
|
226
|
+
? { correlationId: observation.receipt.correlationId }
|
|
227
|
+
: {}),
|
|
228
|
+
readSet,
|
|
229
|
+
operations: retainedOperations(operations),
|
|
230
|
+
...commitEvidence(observation.receipt, operations, 'websocket'),
|
|
231
|
+
authority: observation.receipt.authority,
|
|
232
|
+
receipt: {
|
|
233
|
+
clientTxId: observation.receipt.clientTxId,
|
|
234
|
+
serverTxId: observation.receipt.serverTxId,
|
|
235
|
+
ops: observation.receipt.ops,
|
|
236
|
+
...(observation.receipt.notifications
|
|
237
|
+
? { notifications: observation.receipt.notifications }
|
|
238
|
+
: {}),
|
|
239
|
+
...(observation.receipt.missingIds
|
|
240
|
+
? { missingIds: observation.receipt.missingIds }
|
|
241
|
+
: {}),
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
publishCommitRecord(context, record);
|
|
245
|
+
}
|
|
@@ -22,6 +22,17 @@ export {
|
|
|
22
22
|
targetRefSchema,
|
|
23
23
|
// Layer 3 — optimistic stale-context
|
|
24
24
|
onStaleModeSchema,
|
|
25
|
+
MAX_READ_SET_ENTRIES,
|
|
26
|
+
readSetWatermarkSchema,
|
|
27
|
+
readSetRowTargetSchema,
|
|
28
|
+
readSetGroupTargetSchema,
|
|
29
|
+
commitReadSetTargetSchema,
|
|
30
|
+
persistedReadSetRowTargetSchema,
|
|
31
|
+
persistedReadSetTargetSchema,
|
|
32
|
+
commitReadSetEntrySchema,
|
|
33
|
+
persistedReadSetEntrySchema,
|
|
34
|
+
readSetEntrySchema,
|
|
35
|
+
readSetSchema,
|
|
25
36
|
writeGuardSchema,
|
|
26
37
|
staleNotificationSchema,
|
|
27
38
|
rowStaleNotificationSchema,
|
|
@@ -29,7 +40,10 @@ export {
|
|
|
29
40
|
stalePropagationSchema,
|
|
30
41
|
|
|
31
42
|
readDependencySchema,
|
|
43
|
+
readDependencyListSchema,
|
|
44
|
+
readSetProjectionEntryCount,
|
|
32
45
|
trackDependencySchema,
|
|
46
|
+
trackDependencyListSchema,
|
|
33
47
|
// Layer 2 — pessimistic claim / claim-lease
|
|
34
48
|
claimStatusSchema,
|
|
35
49
|
wireClaimStatusSchema,
|
|
@@ -103,6 +117,15 @@ export type {
|
|
|
103
117
|
TargetRef,
|
|
104
118
|
// Layer 3 — optimistic stale-context
|
|
105
119
|
OnStaleMode,
|
|
120
|
+
ReadSetWatermark,
|
|
121
|
+
ReadSetRowTarget,
|
|
122
|
+
ReadSetGroupTarget,
|
|
123
|
+
CommitReadSetTarget,
|
|
124
|
+
PersistedReadSetTarget,
|
|
125
|
+
CommitReadSetEntry,
|
|
126
|
+
PersistedReadSetEntry,
|
|
127
|
+
ReadSetEntry,
|
|
128
|
+
ReadSet,
|
|
106
129
|
WriteGuard,
|
|
107
130
|
StaleNotification,
|
|
108
131
|
RowStaleNotification,
|
|
@@ -179,6 +202,7 @@ export type {
|
|
|
179
202
|
} from './locator.js';
|
|
180
203
|
export {
|
|
181
204
|
batchFence,
|
|
205
|
+
claimIdFor,
|
|
182
206
|
fenceTokenFor,
|
|
183
207
|
isTargetTuple,
|
|
184
208
|
subTarget,
|
|
@@ -186,4 +210,3 @@ export {
|
|
|
186
210
|
modelTarget,
|
|
187
211
|
streamTarget,
|
|
188
212
|
} from './locator.js';
|
|
189
|
-
|
|
@@ -198,3 +198,20 @@ export function fenceTokenFor(
|
|
|
198
198
|
? fence.token
|
|
199
199
|
: null;
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The claim identity an operation carries when it writes the row named by a
|
|
204
|
+
* held claim. Like the fence token, a claim id cannot travel to another row.
|
|
205
|
+
*/
|
|
206
|
+
export function claimIdFor(
|
|
207
|
+
source: EntityLocator | null | undefined,
|
|
208
|
+
claimId: string | null | undefined,
|
|
209
|
+
model: string,
|
|
210
|
+
id: string | null,
|
|
211
|
+
): string | null {
|
|
212
|
+
if (source == null || claimId == null || id === null) return null;
|
|
213
|
+
const target = modelTarget(source);
|
|
214
|
+
return model.toLowerCase() === target.model.toLowerCase() && id === target.id
|
|
215
|
+
? claimId
|
|
216
|
+
: null;
|
|
217
|
+
}
|
|
@@ -267,6 +267,17 @@ const streamTargetSchema = targetRefSchema
|
|
|
267
267
|
export const onStaleModeSchema = z.enum(['reject', 'overwrite', 'notify']);
|
|
268
268
|
export type OnStaleMode = z.infer<typeof onStaleModeSchema>;
|
|
269
269
|
|
|
270
|
+
/** The exact authoritative position retained for one ReadSet entry. */
|
|
271
|
+
export const readSetWatermarkSchema = z.number().int().nonnegative();
|
|
272
|
+
export type ReadSetWatermark = z.infer<typeof readSetWatermarkSchema>;
|
|
273
|
+
|
|
274
|
+
/** Maximum decision-input entries one logical commit may ask the server to scan. */
|
|
275
|
+
export const MAX_READ_SET_ENTRIES = 500;
|
|
276
|
+
|
|
277
|
+
/** A persisted entry cannot overwrite: it observes future writes, not its own. */
|
|
278
|
+
export const trackOnStaleSchema = onStaleModeSchema.exclude(['overwrite']);
|
|
279
|
+
export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
|
|
280
|
+
|
|
270
281
|
/**
|
|
271
282
|
* The optimistic guard carried on a commit operation. `readAt` is the
|
|
272
283
|
* snapshot watermark from `context.capture` (null/absent ⇒ unguarded write).
|
|
@@ -274,7 +285,7 @@ export type OnStaleMode = z.infer<typeof onStaleModeSchema>;
|
|
|
274
285
|
* claim — see the claim layer below.
|
|
275
286
|
*/
|
|
276
287
|
export const writeGuardSchema = z.object({
|
|
277
|
-
readAt:
|
|
288
|
+
readAt: readSetWatermarkSchema.nullish(),
|
|
278
289
|
onStale: onStaleModeSchema.nullish(),
|
|
279
290
|
bypass: z.boolean().optional(),
|
|
280
291
|
});
|
|
@@ -445,42 +456,79 @@ export const staleNotificationSchema = z.discriminatedUnion('scope', [
|
|
|
445
456
|
]);
|
|
446
457
|
export type StaleNotification = z.infer<typeof staleNotificationSchema>;
|
|
447
458
|
|
|
448
|
-
/**
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
* write-target check, which only validates the rows being written. The server
|
|
452
|
-
* re-runs stale detection against each entry at its `readAt`; a moved premise
|
|
453
|
-
* fires the entry's `onStale` disposition (default `reject`) across the whole
|
|
454
|
-
* batch (`notify` holds every write and notifies, `reject` aborts, `overwrite`
|
|
455
|
-
* proceeds silently). An entry comes at one of two granularities:
|
|
456
|
-
*
|
|
457
|
-
* • Row — `{ model, id, readAt, fields? }`: did this specific row, or these
|
|
458
|
-
* specific fields, change?
|
|
459
|
-
* • Group — `{ group, readAt }`: did anything in this sync group change?
|
|
460
|
-
* `group` is a sync-group key such as `report:abc` or `section:s1`, the
|
|
461
|
-
* same unit a participant watches and claims.
|
|
462
|
-
*
|
|
463
|
-
* See `packages/ablo/docs/concurrency-convention.md` (§4) for the
|
|
464
|
-
* governing convention and the receive → reconcile loop.
|
|
465
|
-
*/
|
|
466
|
-
const readRowDependencySchema = z.object({
|
|
459
|
+
/** One row named by a commit-lifetime ReadSet entry. */
|
|
460
|
+
export const readSetRowTargetSchema = z.object({
|
|
461
|
+
scope: z.literal('row'),
|
|
467
462
|
model: z.string(),
|
|
468
463
|
id: z.string(),
|
|
469
|
-
readAt: z.number(),
|
|
470
464
|
fields: z.array(z.string()).readonly().optional(),
|
|
471
|
-
onStale: onStaleModeSchema.optional(),
|
|
472
465
|
});
|
|
466
|
+
export type ReadSetRowTarget = z.infer<typeof readSetRowTargetSchema>;
|
|
473
467
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
* type-checks inline, `'nonsense'` does not. The
|
|
478
|
-
* {@link groupStaleNotificationSchema} this premise fires uses the branded
|
|
479
|
-
* view, because there Ablo is the author.
|
|
480
|
-
*/
|
|
468
|
+
/** One sync group named by a ReadSet entry. */
|
|
469
|
+
export const readSetGroupTargetSchema = z.object({
|
|
470
|
+
scope: z.literal('group'),
|
|
481
471
|
group: syncGroupRefSchema,
|
|
482
|
-
|
|
483
|
-
|
|
472
|
+
});
|
|
473
|
+
export type ReadSetGroupTarget = z.infer<typeof readSetGroupTargetSchema>;
|
|
474
|
+
|
|
475
|
+
export const commitReadSetTargetSchema = z.discriminatedUnion('scope', [
|
|
476
|
+
readSetRowTargetSchema,
|
|
477
|
+
readSetGroupTargetSchema,
|
|
478
|
+
]);
|
|
479
|
+
export type CommitReadSetTarget = z.infer<typeof commitReadSetTargetSchema>;
|
|
480
|
+
|
|
481
|
+
/** Persisted row tracking is row-grained; it has no field storage axis. */
|
|
482
|
+
export const persistedReadSetRowTargetSchema = readSetRowTargetSchema.omit({ fields: true });
|
|
483
|
+
export const persistedReadSetTargetSchema = z.discriminatedUnion('scope', [
|
|
484
|
+
persistedReadSetRowTargetSchema,
|
|
485
|
+
readSetGroupTargetSchema,
|
|
486
|
+
]);
|
|
487
|
+
export type PersistedReadSetTarget = z.infer<typeof persistedReadSetTargetSchema>;
|
|
488
|
+
|
|
489
|
+
/** One exact input to the decision made by this commit. */
|
|
490
|
+
export const commitReadSetEntrySchema = z.object({
|
|
491
|
+
target: commitReadSetTargetSchema,
|
|
492
|
+
watermark: readSetWatermarkSchema,
|
|
493
|
+
lifetime: z.literal('commit'),
|
|
494
|
+
onStale: onStaleModeSchema,
|
|
495
|
+
});
|
|
496
|
+
export type CommitReadSetEntry = z.infer<typeof commitReadSetEntrySchema>;
|
|
497
|
+
|
|
498
|
+
/** One exact input retained after the declaring commit settles. */
|
|
499
|
+
export const persistedReadSetEntrySchema = z.object({
|
|
500
|
+
target: persistedReadSetTargetSchema,
|
|
501
|
+
watermark: readSetWatermarkSchema,
|
|
502
|
+
lifetime: z.literal('persisted'),
|
|
503
|
+
onStale: trackOnStaleSchema,
|
|
504
|
+
});
|
|
505
|
+
export type PersistedReadSetEntry = z.infer<typeof persistedReadSetEntrySchema>;
|
|
506
|
+
|
|
507
|
+
export const readSetEntrySchema = z.discriminatedUnion('lifetime', [
|
|
508
|
+
commitReadSetEntrySchema,
|
|
509
|
+
persistedReadSetEntrySchema,
|
|
510
|
+
]);
|
|
511
|
+
export type ReadSetEntry = z.infer<typeof readSetEntrySchema>;
|
|
512
|
+
|
|
513
|
+
export const readSetSchema = z
|
|
514
|
+
.array(readSetEntrySchema)
|
|
515
|
+
.max(MAX_READ_SET_ENTRIES)
|
|
516
|
+
.readonly();
|
|
517
|
+
export type ReadSet = z.infer<typeof readSetSchema>;
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Wire projection of a commit-lifetime ReadSet entry. The public field names
|
|
521
|
+
* stay compatible (`readAt`, flat row/group target); every member is selected
|
|
522
|
+
* from the canonical entry rather than restated beside it.
|
|
523
|
+
*/
|
|
524
|
+
const readRowDependencySchema = readSetRowTargetSchema.omit({ scope: true }).extend({
|
|
525
|
+
readAt: commitReadSetEntrySchema.shape.watermark,
|
|
526
|
+
onStale: commitReadSetEntrySchema.shape.onStale.optional(),
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const readGroupDependencySchema = readSetGroupTargetSchema.omit({ scope: true }).extend({
|
|
530
|
+
readAt: commitReadSetEntrySchema.shape.watermark,
|
|
531
|
+
onStale: commitReadSetEntrySchema.shape.onStale.optional(),
|
|
484
532
|
});
|
|
485
533
|
|
|
486
534
|
export const readDependencySchema = z.union([
|
|
@@ -490,14 +538,14 @@ export const readDependencySchema = z.union([
|
|
|
490
538
|
export type ReadDependency = z.infer<typeof readDependencySchema>;
|
|
491
539
|
|
|
492
540
|
/**
|
|
493
|
-
* A durable
|
|
541
|
+
* A durable ReadSet entry — what a participant is watching so that a later
|
|
494
542
|
* change to it opens a {@link StaleNotification}. Where a {@link ReadDependency}
|
|
495
543
|
* is checked once at commit and discarded, a `TrackDependency` is kept and
|
|
496
544
|
* re-checked against every future delta. The row form watches one object; the
|
|
497
545
|
* group form watches a whole sync group ("anything in `report:abc`"). See
|
|
498
546
|
* `packages/ablo/docs/groups.md` for how it drives change propagation.
|
|
499
547
|
*
|
|
500
|
-
* It is a
|
|
548
|
+
* It is a projection of the canonical persisted entry, not a second declaration of the
|
|
501
549
|
* same reference: a track names its target exactly as a read does, and the
|
|
502
550
|
* three ways it differs are stated here as omissions the compiler holds.
|
|
503
551
|
*
|
|
@@ -514,18 +562,10 @@ export type ReadDependency = z.infer<typeof readDependencySchema>;
|
|
|
514
562
|
* registered the track, so a caller with nothing to say about when it last
|
|
515
563
|
* looked gets "from here on".
|
|
516
564
|
*
|
|
517
|
-
* A field added to the
|
|
565
|
+
* A field added to the ReadSet target reaches both halves; a field the durable half
|
|
518
566
|
* genuinely cannot carry has to be omitted here on purpose, in one line, rather
|
|
519
567
|
* than by being quietly left out of a copy.
|
|
520
568
|
*/
|
|
521
|
-
/**
|
|
522
|
-
* A track's disposition — the premise enum with `overwrite` subtracted, derived
|
|
523
|
-
* from it rather than spelled again, so a mode added to the convention reaches
|
|
524
|
-
* a track unless it is deliberately excluded here.
|
|
525
|
-
*/
|
|
526
|
-
export const trackOnStaleSchema = onStaleModeSchema.exclude(['overwrite']);
|
|
527
|
-
export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
|
|
528
|
-
|
|
529
569
|
/**
|
|
530
570
|
* What a track does when the caller says nothing — reporting, the behavior every
|
|
531
571
|
* track had before the disposition existed, so an existing registration is
|
|
@@ -539,18 +579,32 @@ export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
|
|
|
539
579
|
export const DEFAULT_TRACK_ON_STALE: TrackOnStale = 'notify';
|
|
540
580
|
|
|
541
581
|
const trackBaseSchema = {
|
|
542
|
-
readAt:
|
|
543
|
-
onStale:
|
|
582
|
+
readAt: persistedReadSetEntrySchema.shape.watermark.optional(),
|
|
583
|
+
onStale: persistedReadSetEntrySchema.shape.onStale.optional(),
|
|
544
584
|
} as const;
|
|
545
585
|
|
|
546
586
|
export const trackDependencySchema = z.union([
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
.extend(trackBaseSchema),
|
|
550
|
-
readGroupDependencySchema.omit({ onStale: true, readAt: true }).extend(trackBaseSchema),
|
|
587
|
+
persistedReadSetRowTargetSchema.omit({ scope: true }).extend(trackBaseSchema),
|
|
588
|
+
readSetGroupTargetSchema.omit({ scope: true }).extend(trackBaseSchema),
|
|
551
589
|
]);
|
|
552
590
|
export type TrackDependency = z.infer<typeof trackDependencySchema>;
|
|
553
591
|
|
|
592
|
+
/** Bounded wire projections of the two ReadSet lifetimes. */
|
|
593
|
+
export const readDependencyListSchema = z
|
|
594
|
+
.array(readDependencySchema)
|
|
595
|
+
.max(MAX_READ_SET_ENTRIES);
|
|
596
|
+
export const trackDependencyListSchema = z
|
|
597
|
+
.array(trackDependencySchema)
|
|
598
|
+
.max(MAX_READ_SET_ENTRIES);
|
|
599
|
+
|
|
600
|
+
/** Shared combined-cardinality rule for envelopes carrying both projections. */
|
|
601
|
+
export function readSetProjectionEntryCount(value: {
|
|
602
|
+
readonly reads?: readonly unknown[] | null;
|
|
603
|
+
readonly track?: readonly unknown[] | null;
|
|
604
|
+
}): number {
|
|
605
|
+
return (value.reads?.length ?? 0) + (value.track?.length ?? 0);
|
|
606
|
+
}
|
|
607
|
+
|
|
554
608
|
// ─────────────────────────────────────────────────────────────────────────
|
|
555
609
|
// Layer 2 — pessimistic claims and leases
|
|
556
610
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1328,7 +1382,12 @@ export type UpdateSubscriptionPayload = z.infer<
|
|
|
1328
1382
|
export const subscriptionAckPayloadSchema = z.object({
|
|
1329
1383
|
success: z.boolean(),
|
|
1330
1384
|
syncGroups: z.array(z.string()),
|
|
1331
|
-
error: z.object({
|
|
1385
|
+
error: z.object({
|
|
1386
|
+
code: z.string(),
|
|
1387
|
+
message: z.string(),
|
|
1388
|
+
request_id: z.string().optional(),
|
|
1389
|
+
event_id: z.string().optional(),
|
|
1390
|
+
}).optional(),
|
|
1332
1391
|
});
|
|
1333
1392
|
export type SubscriptionAckPayload = z.infer<
|
|
1334
1393
|
typeof subscriptionAckPayloadSchema
|
|
@@ -1360,6 +1419,12 @@ export const commitOperationSchema = writeGuardSchema.extend({
|
|
|
1360
1419
|
input: z.record(z.string(), z.unknown()).nullish(),
|
|
1361
1420
|
/** Per-op client tx id, echoed on the broadcast delta. */
|
|
1362
1421
|
transactionId: z.string().nullish(),
|
|
1422
|
+
/**
|
|
1423
|
+
* The server-issued claim identity this operation was performed under.
|
|
1424
|
+
* Distinct from `transactionId`: a claim attributes and fences coordinated
|
|
1425
|
+
* ownership, while a transaction id correlates the resulting source echo.
|
|
1426
|
+
*/
|
|
1427
|
+
claimId: z.string().min(1).nullish(),
|
|
1363
1428
|
/**
|
|
1364
1429
|
* The fencing token from the held claim this write belongs to (Option B).
|
|
1365
1430
|
* Present only on a write issued under a claim that was granted one; the
|