@abloatai/ablo 0.29.2 → 0.30.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 +27 -0
- package/README.md +3 -2
- package/dist/BaseSyncedStore.js +3 -1
- package/dist/Database.js +29 -2
- package/dist/SyncClient.d.ts +5 -3
- package/dist/SyncClient.js +8 -4
- package/dist/ai-sdk/coordinatedTool.d.ts +0 -1
- package/dist/ai-sdk/coordinatedTool.js +0 -1
- package/dist/ai-sdk/coordinationContext.js +1 -5
- package/dist/cli.cjs +561 -90
- package/dist/client/Ablo.d.ts +1 -1
- package/dist/client/Ablo.js +24 -36
- package/dist/client/auth.d.ts +8 -18
- package/dist/client/auth.js +16 -96
- package/dist/client/createModelProxy.d.ts +16 -11
- package/dist/client/createModelProxy.js +162 -20
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/httpTransport.js +263 -89
- package/dist/client/options.d.ts +0 -15
- package/dist/client/resourceTypes.d.ts +13 -26
- package/dist/client/schemaConfig.js +5 -0
- package/dist/client/writeOptionsSchema.d.ts +1 -0
- package/dist/client/writeOptionsSchema.js +11 -2
- package/dist/client/wsMutationExecutor.d.ts +3 -5
- package/dist/coordination/schema.d.ts +17 -13
- package/dist/coordination/schema.js +36 -10
- package/dist/errorCodes.d.ts +9 -4
- package/dist/errorCodes.js +10 -5
- package/dist/errors.js +4 -9
- package/dist/index.d.ts +4 -0
- package/dist/interfaces/index.d.ts +31 -19
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/schema.d.ts +13 -0
- package/dist/schema/select.js +7 -0
- package/dist/schema/syncDeltaRow.d.ts +31 -13
- package/dist/schema/syncDeltaRow.js +33 -15
- package/dist/schema/tenancy.d.ts +52 -16
- package/dist/schema/tenancy.js +86 -25
- package/dist/server/commit.d.ts +8 -24
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/readConfig.d.ts +10 -0
- package/dist/source/adapter.d.ts +26 -15
- package/dist/source/adapter.js +5 -3
- package/dist/source/adapters/drizzle.js +35 -9
- package/dist/source/adapters/kysely.d.ts +33 -61
- package/dist/source/adapters/kysely.js +170 -177
- package/dist/source/adapters/kyselyMutationCore.d.ts +76 -0
- package/dist/source/adapters/kyselyMutationCore.js +125 -0
- package/dist/source/adapters/memory.d.ts +1 -1
- package/dist/source/adapters/memory.js +24 -10
- package/dist/source/adapters/prisma.js +35 -8
- package/dist/source/conformance.d.ts +26 -28
- package/dist/source/conformance.js +142 -69
- package/dist/source/contract.d.ts +46 -4
- package/dist/source/contract.js +66 -5
- package/dist/source/factory.d.ts +3 -3
- package/dist/source/factory.js +24 -3
- package/dist/source/idempotency.d.ts +46 -0
- package/dist/source/idempotency.js +129 -0
- package/dist/source/index.d.ts +6 -4
- package/dist/source/index.js +5 -3
- package/dist/source/migrations.d.ts +9 -5
- package/dist/source/migrations.js +39 -6
- package/dist/source/types.d.ts +88 -14
- package/dist/source/types.js +16 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +0 -1
- package/dist/sync/BootstrapFetcher.js +7 -0
- package/dist/sync/SyncWebSocket.d.ts +5 -27
- package/dist/sync/awaitClaimGrant.d.ts +6 -0
- package/dist/sync/awaitClaimGrant.js +12 -2
- package/dist/sync/commitFrames.d.ts +2 -4
- package/dist/sync/commitFrames.js +1 -0
- package/dist/sync/createClaimStream.d.ts +1 -1
- package/dist/sync/createClaimStream.js +13 -15
- package/dist/sync/deltaPipeline.d.ts +1 -1
- package/dist/sync/deltaPipeline.js +1 -1
- package/dist/sync/participants.d.ts +2 -3
- package/dist/sync/participants.js +2 -2
- package/dist/sync/wsFrameHandlers.js +35 -15
- package/dist/testing/mocks/MockMutationExecutor.d.ts +21 -0
- package/dist/testing/mocks/MockMutationExecutor.js +53 -1
- package/dist/transactions/TransactionQueue.d.ts +61 -9
- package/dist/transactions/TransactionQueue.js +502 -114
- package/dist/transactions/commitEnvelope.d.ts +4 -0
- package/dist/transactions/commitEnvelope.js +25 -0
- package/dist/transactions/commitPayload.d.ts +23 -5
- package/dist/transactions/commitPayload.js +3 -0
- package/dist/transactions/deltaConfirmation.d.ts +2 -1
- package/dist/transactions/deltaConfirmation.js +22 -4
- package/dist/transactions/durableWriteStore.d.ts +8 -1
- package/dist/transactions/httpCommitEnvelope.d.ts +3 -1
- package/dist/transactions/httpCommitEnvelope.js +30 -0
- package/dist/transactions/replayValidation.d.ts +4 -0
- package/dist/transactions/replayValidation.js +6 -0
- package/dist/types/streams.d.ts +29 -17
- package/dist/wire/commit.d.ts +671 -0
- package/dist/wire/commit.js +300 -0
- package/dist/wire/delta.d.ts +4 -0
- package/dist/wire/delta.js +9 -0
- package/dist/wire/frames.d.ts +17 -45
- package/dist/wire/index.d.ts +2 -0
- package/dist/wire/index.js +4 -0
- package/docs/agent-messaging.md +4 -5
- package/docs/agents.md +1 -2
- package/docs/api-keys.md +1 -2
- package/docs/client-behavior.md +4 -6
- package/docs/coordination.md +64 -9
- package/docs/data-sources.md +4 -7
- package/docs/integration-guide.md +6 -8
- package/docs/migration.md +0 -3
- package/docs/quickstart.md +7 -8
- package/docs/schema-contract.md +7 -8
- package/package.json +4 -4
- package/dist/client/registerDataSource.d.ts +0 -19
- package/dist/client/registerDataSource.js +0 -66
|
@@ -33,6 +33,7 @@ export declare const durableCommitOperationSchema: z.ZodObject<{
|
|
|
33
33
|
overwrite: "overwrite";
|
|
34
34
|
notify: "notify";
|
|
35
35
|
}>>>;
|
|
36
|
+
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
36
37
|
model: z.ZodString;
|
|
37
38
|
id: z.ZodString;
|
|
38
39
|
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -78,6 +79,7 @@ export declare const durableCommitEnvelopeSchema: z.ZodObject<{
|
|
|
78
79
|
overwrite: "overwrite";
|
|
79
80
|
notify: "notify";
|
|
80
81
|
}>>>;
|
|
82
|
+
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
81
83
|
model: z.ZodString;
|
|
82
84
|
id: z.ZodString;
|
|
83
85
|
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -117,6 +119,8 @@ export declare const durableCommitEnvelopeSchema: z.ZodObject<{
|
|
|
117
119
|
}, z.core.$strict>>;
|
|
118
120
|
createdAt: z.ZodNumber;
|
|
119
121
|
sealedAt: z.ZodNumber;
|
|
122
|
+
acceptedAt: z.ZodOptional<z.ZodNumber>;
|
|
123
|
+
correlationId: z.ZodOptional<z.ZodString>;
|
|
120
124
|
sequence: z.ZodOptional<z.ZodNumber>;
|
|
121
125
|
timestamp: z.ZodNumber;
|
|
122
126
|
}, z.core.$strict>;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { readDependencySchema } from '../coordination/schema.js';
|
|
11
11
|
import { commitOperationSchema } from '../wire/frames.js';
|
|
12
|
+
import { correlationIdSchema } from '../wire/commit.js';
|
|
12
13
|
import { idempotencyKeySchema } from './idempotencyKey.js';
|
|
13
14
|
export const COMMIT_ENVELOPE_VERSION = 1;
|
|
14
15
|
export const COMMIT_ENVELOPE_RECORD_PREFIX = 'commit-envelope:';
|
|
@@ -32,6 +33,7 @@ export const durableCommitOperationSchema = commitOperationSchema
|
|
|
32
33
|
transactionId: true,
|
|
33
34
|
readAt: true,
|
|
34
35
|
onStale: true,
|
|
36
|
+
fenceToken: true,
|
|
35
37
|
})
|
|
36
38
|
.extend({
|
|
37
39
|
model: z.string().min(1),
|
|
@@ -74,6 +76,14 @@ export const durableCommitEnvelopeSchema = z
|
|
|
74
76
|
scope: commitOutboxScopeSchema.optional(),
|
|
75
77
|
createdAt: z.number().int().nonnegative(),
|
|
76
78
|
sealedAt: z.number().int().nonnegative(),
|
|
79
|
+
/**
|
|
80
|
+
* Monotonic evidence that a connected source accepted this exact request.
|
|
81
|
+
* Its server and customer idempotency keys are permanent, so an accepted
|
|
82
|
+
* envelope remains replayable until the WAL echo confirms it.
|
|
83
|
+
*/
|
|
84
|
+
acceptedAt: z.number().int().nonnegative().optional(),
|
|
85
|
+
/** Opaque server-derived source-batch identity paired with `acceptedAt`. */
|
|
86
|
+
correlationId: correlationIdSchema.optional(),
|
|
77
87
|
/** Monotonic within one client; disambiguates writes sealed in the same ms. */
|
|
78
88
|
sequence: z.number().int().nonnegative().optional(),
|
|
79
89
|
timestamp: z.number().int().nonnegative(),
|
|
@@ -93,6 +103,21 @@ export const durableCommitEnvelopeSchema = z
|
|
|
93
103
|
message: 'Source mutation ids must be unique',
|
|
94
104
|
});
|
|
95
105
|
}
|
|
106
|
+
if ((envelope.acceptedAt === undefined) !== (envelope.correlationId === undefined)) {
|
|
107
|
+
context.addIssue({
|
|
108
|
+
code: 'custom',
|
|
109
|
+
path: ['acceptedAt'],
|
|
110
|
+
message: 'Accepted commit envelopes require both acceptedAt and correlationId',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (envelope.acceptedAt !== undefined &&
|
|
114
|
+
envelope.acceptedAt < envelope.sealedAt) {
|
|
115
|
+
context.addIssue({
|
|
116
|
+
code: 'custom',
|
|
117
|
+
path: ['acceptedAt'],
|
|
118
|
+
message: 'Commit envelope cannot be accepted before it is sealed',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
96
121
|
if (envelope.origin === 'model_batch') {
|
|
97
122
|
const transactionIds = envelope.operations.map((operation) => operation.transactionId);
|
|
98
123
|
if (transactionIds.some((id) => typeof id !== 'string' || id.length === 0)) {
|
|
@@ -72,16 +72,33 @@ export interface Transaction {
|
|
|
72
72
|
localOnly?: boolean;
|
|
73
73
|
/** Sync-id threshold: the transaction confirms once a delta with an id at least this value arrives. */
|
|
74
74
|
syncIdNeededForCompletion?: number;
|
|
75
|
+
/**
|
|
76
|
+
* Opaque customer-side batch identity returned with a queued source receipt.
|
|
77
|
+
* It is retained durably and is the only identity a live WAL delta may use
|
|
78
|
+
* to settle this pending commit. Server-side replay additionally proves the
|
|
79
|
+
* complete stored operation-id set before returning `confirmed`.
|
|
80
|
+
*/
|
|
81
|
+
correlationId?: string;
|
|
82
|
+
/**
|
|
83
|
+
* A queued forward has no authoritative sync-id watermark yet. It may only
|
|
84
|
+
* confirm when a WAL delta carries its opaque server-derived correlation;
|
|
85
|
+
* unrelated deltas must never satisfy it by advancing the global watermark.
|
|
86
|
+
*/
|
|
87
|
+
requiresCorrelatedDelta?: boolean;
|
|
75
88
|
/**
|
|
76
89
|
* Resolves once the server has confirmed this transaction, whether by a delta
|
|
77
|
-
* or an HTTP acknowledgement
|
|
78
|
-
* transaction is permanently rolled back
|
|
79
|
-
*
|
|
90
|
+
* or an HTTP acknowledgement. It rejects with the originating error if the
|
|
91
|
+
* transaction is permanently rolled back; for a source-forwarded write it
|
|
92
|
+
* also rejects with `replication_lag_timeout` when the accepted write's WAL
|
|
93
|
+
* echo misses the confirmation deadline. That timeout does not roll back or
|
|
94
|
+
* fail the transaction: it remains pending and may confirm when replication
|
|
95
|
+
* recovers. This gives a caller one place to await the answer to "did my write
|
|
96
|
+
* land?", matching the
|
|
80
97
|
* `commits.create({ wait: 'confirmed' })` and
|
|
81
98
|
* {@link TransactionQueue.waitForConfirmation} vocabulary, so a failure
|
|
82
99
|
* surfaces at the call site instead of only as a silent local rollback. The
|
|
83
|
-
* rejection value is the same {@link AbloError} carried on the
|
|
84
|
-
* `transaction:failed` event.
|
|
100
|
+
* permanent-rejection value is the same {@link AbloError} carried on the
|
|
101
|
+
* queue's `transaction:failed` event.
|
|
85
102
|
*/
|
|
86
103
|
confirmation?: Promise<void>;
|
|
87
104
|
}
|
|
@@ -108,6 +125,7 @@ export declare function hasCommitCoalescingBarrier(options?: WriteOptions): bool
|
|
|
108
125
|
export interface WriteOperationFields {
|
|
109
126
|
readAt?: number | null;
|
|
110
127
|
onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
128
|
+
fenceToken?: number | null;
|
|
111
129
|
options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
|
|
112
130
|
}
|
|
113
131
|
/**
|
|
@@ -131,6 +131,9 @@ export function applyWriteOptions(op, transaction) {
|
|
|
131
131
|
if (writeOptions.onStale !== undefined) {
|
|
132
132
|
operation.onStale = writeOptions.onStale;
|
|
133
133
|
}
|
|
134
|
+
if (writeOptions.fenceToken !== undefined) {
|
|
135
|
+
operation.fenceToken = writeOptions.fenceToken;
|
|
136
|
+
}
|
|
134
137
|
if (writeOptions.idempotencyKey != null || writeOptions.label !== undefined) {
|
|
135
138
|
operation.options = {
|
|
136
139
|
...(writeOptions.idempotencyKey != null
|
|
@@ -40,13 +40,14 @@ export declare class DeltaConfirmationTracker {
|
|
|
40
40
|
constructor(ctx: DeltaConfirmationContext);
|
|
41
41
|
/** Applied-cursor alias, kept so the read sites below stay legible. */
|
|
42
42
|
private get lastSeenSyncId();
|
|
43
|
+
private matchesSourceEcho;
|
|
43
44
|
noteAck(lastSyncId: number | undefined): void;
|
|
44
45
|
/**
|
|
45
46
|
* Confirms every awaiting transaction whose sync-id threshold this delta
|
|
46
47
|
* meets or exceeds.
|
|
47
48
|
* @param syncId - The sync id of the received delta.
|
|
48
49
|
*/
|
|
49
|
-
onDeltaReceived(syncId: number): void;
|
|
50
|
+
onDeltaReceived(syncId: number, correlationId?: string): void;
|
|
50
51
|
scheduleDeltaConfirmationTimeout(tx: Transaction, timeoutMs: number): void;
|
|
51
52
|
private cancelDeltaConfirmationTimeout;
|
|
52
53
|
/**
|
|
@@ -27,6 +27,11 @@ export class DeltaConfirmationTracker {
|
|
|
27
27
|
get lastSeenSyncId() {
|
|
28
28
|
return this.ctx.position.applied;
|
|
29
29
|
}
|
|
30
|
+
matchesSourceEcho(tx, correlationId) {
|
|
31
|
+
return (correlationId !== undefined &&
|
|
32
|
+
tx.correlationId !== undefined &&
|
|
33
|
+
correlationId === tx.correlationId);
|
|
34
|
+
}
|
|
30
35
|
noteAck(lastSyncId) {
|
|
31
36
|
this.ctx.position.noteAck(lastSyncId);
|
|
32
37
|
}
|
|
@@ -35,7 +40,7 @@ export class DeltaConfirmationTracker {
|
|
|
35
40
|
* meets or exceeds.
|
|
36
41
|
* @param syncId - The sync id of the received delta.
|
|
37
42
|
*/
|
|
38
|
-
onDeltaReceived(syncId) {
|
|
43
|
+
onDeltaReceived(syncId, correlationId) {
|
|
39
44
|
// The cursor advances where the delta is applied (the store calls
|
|
40
45
|
// position.advanceApplied / advancePersisted); this hook only resolves
|
|
41
46
|
// confirmation thresholds against the incoming id.
|
|
@@ -52,7 +57,11 @@ export class DeltaConfirmationTracker {
|
|
|
52
57
|
txId: tx.id.slice(0, 8),
|
|
53
58
|
model: tx.modelName,
|
|
54
59
|
needed: tx.syncIdNeededForCompletion,
|
|
55
|
-
|
|
60
|
+
requiresCorrelatedDelta: tx.requiresCorrelatedDelta === true,
|
|
61
|
+
willConfirm: tx.requiresCorrelatedDelta === true
|
|
62
|
+
? this.matchesSourceEcho(tx, correlationId)
|
|
63
|
+
: tx.syncIdNeededForCompletion !== undefined &&
|
|
64
|
+
syncId >= tx.syncIdNeededForCompletion,
|
|
56
65
|
})),
|
|
57
66
|
});
|
|
58
67
|
}
|
|
@@ -61,8 +70,16 @@ export class DeltaConfirmationTracker {
|
|
|
61
70
|
return;
|
|
62
71
|
let confirmedCount = 0;
|
|
63
72
|
for (const tx of awaitingTxs) {
|
|
64
|
-
//
|
|
65
|
-
|
|
73
|
+
// Queued forward receipts deliberately have no watermark. They confirm
|
|
74
|
+
// only when the authoritative source echoes the receipt's opaque,
|
|
75
|
+
// authenticated-scope batch identity; the legacy anomaly path continues
|
|
76
|
+
// to use its sync-id threshold.
|
|
77
|
+
const confirmedByCorrelation = tx.requiresCorrelatedDelta === true &&
|
|
78
|
+
this.matchesSourceEcho(tx, correlationId);
|
|
79
|
+
const confirmedByThreshold = tx.requiresCorrelatedDelta !== true &&
|
|
80
|
+
tx.syncIdNeededForCompletion !== undefined &&
|
|
81
|
+
syncId >= tx.syncIdNeededForCompletion;
|
|
82
|
+
if (confirmedByCorrelation || confirmedByThreshold) {
|
|
66
83
|
this.cancelDeltaConfirmationTimeout(tx.id);
|
|
67
84
|
this.ctx.store.updateStatus(tx.id, 'completed');
|
|
68
85
|
this.ctx.emit('transaction:completed', tx);
|
|
@@ -74,6 +91,7 @@ export class DeltaConfirmationTracker {
|
|
|
74
91
|
model: tx.modelName,
|
|
75
92
|
neededSyncId: tx.syncIdNeededForCompletion,
|
|
76
93
|
receivedSyncId: syncId,
|
|
94
|
+
confirmation: confirmedByCorrelation ? 'source_correlation' : 'sync_id',
|
|
77
95
|
});
|
|
78
96
|
}
|
|
79
97
|
}
|
|
@@ -30,6 +30,7 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
30
30
|
overwrite: "overwrite";
|
|
31
31
|
notify: "notify";
|
|
32
32
|
}>>>;
|
|
33
|
+
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
33
34
|
model: z.ZodString;
|
|
34
35
|
id: z.ZodString;
|
|
35
36
|
input: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -69,6 +70,8 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
69
70
|
}, z.core.$strict>>;
|
|
70
71
|
createdAt: z.ZodNumber;
|
|
71
72
|
sealedAt: z.ZodNumber;
|
|
73
|
+
acceptedAt: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
correlationId: z.ZodOptional<z.ZodString>;
|
|
72
75
|
sequence: z.ZodOptional<z.ZodNumber>;
|
|
73
76
|
timestamp: z.ZodNumber;
|
|
74
77
|
}, z.core.$strict>, z.ZodObject<{
|
|
@@ -89,6 +92,8 @@ export declare const pendingWriteSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
89
92
|
scopeNamespace: z.ZodString;
|
|
90
93
|
createdAt: z.ZodNumber;
|
|
91
94
|
sealedAt: z.ZodNumber;
|
|
95
|
+
acceptedAt: z.ZodOptional<z.ZodNumber>;
|
|
96
|
+
correlationId: z.ZodOptional<z.ZodString>;
|
|
92
97
|
sequence: z.ZodOptional<z.ZodNumber>;
|
|
93
98
|
timestamp: z.ZodNumber;
|
|
94
99
|
}, z.core.$strict>]>;
|
|
@@ -105,7 +110,9 @@ export interface DurableWriteStore {
|
|
|
105
110
|
/**
|
|
106
111
|
* Atomically reserve a pending write and consume the staged records it owns.
|
|
107
112
|
* The same id + same request is idempotent; the same id + a different request
|
|
108
|
-
* must be rejected.
|
|
113
|
+
* must be rejected. For a source-accepted envelope, a re-seal may add the
|
|
114
|
+
* monotonic `acceptedAt`/`correlationId` evidence and the store must preserve
|
|
115
|
+
* that upgrade atomically rather than ignoring it.
|
|
109
116
|
*/
|
|
110
117
|
seal(write: PendingWrite, consumedRecordIds: readonly string[]): Promise<void>;
|
|
111
118
|
/** Load all unacknowledged writes. Stored data is treated as untrusted. */
|
|
@@ -24,6 +24,8 @@ export declare const durableHttpCommitEnvelopeSchema: z.ZodObject<{
|
|
|
24
24
|
scopeNamespace: z.ZodString;
|
|
25
25
|
createdAt: z.ZodNumber;
|
|
26
26
|
sealedAt: z.ZodNumber;
|
|
27
|
+
acceptedAt: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
correlationId: z.ZodOptional<z.ZodString>;
|
|
27
29
|
sequence: z.ZodOptional<z.ZodNumber>;
|
|
28
30
|
timestamp: z.ZodNumber;
|
|
29
31
|
}, z.core.$strict>;
|
|
@@ -41,4 +43,4 @@ export declare function createDurableHttpCommitEnvelope(input: {
|
|
|
41
43
|
sealedAt?: number;
|
|
42
44
|
sequence?: number;
|
|
43
45
|
}): DurableHttpCommitEnvelope;
|
|
44
|
-
export declare function isHttpCommitReplayExpired(envelope: Pick<DurableHttpCommitEnvelope, 'sealedAt'>, now?: number): boolean;
|
|
46
|
+
export declare function isHttpCommitReplayExpired(envelope: Pick<DurableHttpCommitEnvelope, 'sealedAt' | 'acceptedAt'>, now?: number): boolean;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { v5 as uuidv5 } from 'uuid';
|
|
4
4
|
import { stableStringify } from '../utils/json.js';
|
|
5
|
+
import { correlationIdSchema } from '../wire/commit.js';
|
|
5
6
|
import { PROTOCOL_VERSION } from '../wire/protocolVersion.js';
|
|
6
7
|
import { idempotencyKeySchema } from './idempotencyKey.js';
|
|
7
8
|
export const HTTP_COMMIT_ENVELOPE_VERSION = 1;
|
|
@@ -67,6 +68,15 @@ export const durableHttpCommitEnvelopeSchema = z
|
|
|
67
68
|
scopeNamespace: z.string().min(1),
|
|
68
69
|
createdAt: z.number().int().nonnegative(),
|
|
69
70
|
sealedAt: z.number().int().nonnegative(),
|
|
71
|
+
/**
|
|
72
|
+
* Monotonic evidence that the server accepted this exact request for a
|
|
73
|
+
* connected source. Such server keys are permanent, so this envelope must
|
|
74
|
+
* remain replayable past the ordinary hosted 24-hour window until its WAL
|
|
75
|
+
* echo confirms.
|
|
76
|
+
*/
|
|
77
|
+
acceptedAt: z.number().int().nonnegative().optional(),
|
|
78
|
+
/** Opaque server-derived source-batch identity paired with `acceptedAt`. */
|
|
79
|
+
correlationId: correlationIdSchema.optional(),
|
|
70
80
|
/** Monotonic within one client; disambiguates writes sealed in the same ms. */
|
|
71
81
|
sequence: z.number().int().nonnegative().optional(),
|
|
72
82
|
timestamp: z.number().int().nonnegative(),
|
|
@@ -88,6 +98,21 @@ export const durableHttpCommitEnvelopeSchema = z
|
|
|
88
98
|
message: 'HTTP envelope cannot be sealed before it is created',
|
|
89
99
|
});
|
|
90
100
|
}
|
|
101
|
+
if ((envelope.acceptedAt === undefined) !== (envelope.correlationId === undefined)) {
|
|
102
|
+
context.addIssue({
|
|
103
|
+
code: 'custom',
|
|
104
|
+
path: ['acceptedAt'],
|
|
105
|
+
message: 'Accepted HTTP envelopes require both acceptedAt and correlationId',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (envelope.acceptedAt !== undefined &&
|
|
109
|
+
envelope.acceptedAt < envelope.sealedAt) {
|
|
110
|
+
context.addIssue({
|
|
111
|
+
code: 'custom',
|
|
112
|
+
path: ['acceptedAt'],
|
|
113
|
+
message: 'HTTP envelope cannot be accepted before it is sealed',
|
|
114
|
+
});
|
|
115
|
+
}
|
|
91
116
|
const { method, path } = envelope.request;
|
|
92
117
|
const allowedPath = method === 'POST'
|
|
93
118
|
? commitPathSchema.safeParse(path).success ||
|
|
@@ -177,5 +202,10 @@ export function createDurableHttpCommitEnvelope(input) {
|
|
|
177
202
|
});
|
|
178
203
|
}
|
|
179
204
|
export function isHttpCommitReplayExpired(envelope, now = Date.now()) {
|
|
205
|
+
// Connected-source mutation-log and customer idempotency keys are
|
|
206
|
+
// permanent. Once acceptance is durable, age can no longer make replay
|
|
207
|
+
// ambiguous; only the matching WAL echo may settle the envelope.
|
|
208
|
+
if (envelope.acceptedAt !== undefined)
|
|
209
|
+
return false;
|
|
180
210
|
return now - envelope.sealedAt >= HTTP_COMMIT_REPLAY_WINDOW_MS;
|
|
181
211
|
}
|
|
@@ -62,6 +62,7 @@ export declare const persistedTransactionSchema: z.ZodObject<{
|
|
|
62
62
|
}>>>;
|
|
63
63
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
64
64
|
label: z.ZodOptional<z.ZodString>;
|
|
65
|
+
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
65
66
|
}, z.core.$loose>>;
|
|
66
67
|
localOnly: z.ZodOptional<z.ZodBoolean>;
|
|
67
68
|
}, z.core.$loose>;
|
|
@@ -104,6 +105,7 @@ export declare const persistedMutationSchema: z.ZodObject<{
|
|
|
104
105
|
}>>>;
|
|
105
106
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
106
107
|
label: z.ZodOptional<z.ZodString>;
|
|
108
|
+
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
107
109
|
}, z.core.$loose>>;
|
|
108
110
|
}, z.core.$loose>;
|
|
109
111
|
export type PersistedQueuedMutation = z.infer<typeof persistedMutationSchema>;
|
|
@@ -139,6 +141,7 @@ export declare const legacyPendingMutationRecordSchema: z.ZodObject<{
|
|
|
139
141
|
}>>>;
|
|
140
142
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
141
143
|
label: z.ZodOptional<z.ZodString>;
|
|
144
|
+
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
142
145
|
}, z.core.$loose>>;
|
|
143
146
|
mutationId: z.ZodString;
|
|
144
147
|
}, z.core.$loose>;
|
|
@@ -173,6 +176,7 @@ export declare const pendingMutationRecordSchema: z.ZodObject<{
|
|
|
173
176
|
}>>>;
|
|
174
177
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
175
178
|
label: z.ZodOptional<z.ZodString>;
|
|
179
|
+
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
176
180
|
}, z.core.$loose>>;
|
|
177
181
|
mutationId: z.ZodString;
|
|
178
182
|
}, z.core.$loose>;
|
|
@@ -24,6 +24,12 @@ const persistedWriteOptionsSchema = z
|
|
|
24
24
|
onStale: z.enum(['reject', 'overwrite', 'notify']).nullable().optional(),
|
|
25
25
|
idempotencyKey: z.string().optional(),
|
|
26
26
|
label: z.string().optional(),
|
|
27
|
+
// Aligned with the `WriteOptions` type: a claimed write persisted offline
|
|
28
|
+
// must replay carrying its fencing token (Option B), or a queued write that
|
|
29
|
+
// survives a reload would lose its fence and could land as a stale blind
|
|
30
|
+
// write. Declared (not just loose-passthrough) so it is validated as a
|
|
31
|
+
// number on rehydration.
|
|
32
|
+
fenceToken: z.number().nullable().optional(),
|
|
27
33
|
})
|
|
28
34
|
.loose();
|
|
29
35
|
/**
|
package/dist/types/streams.d.ts
CHANGED
|
@@ -289,16 +289,11 @@ export interface ClaimLeaseOptions {
|
|
|
289
289
|
export type Duration = import('../utils/duration.js').Duration;
|
|
290
290
|
export interface ClaimOptions extends ClaimLeaseOptions {
|
|
291
291
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
296
|
-
|
|
297
|
-
readonly reason?: string;
|
|
298
|
-
/**
|
|
299
|
-
* Peer-visible explanation of the exact work being performed. This is more
|
|
300
|
-
* specific than `reason`: `reason` is the phase (`'renaming'`), while
|
|
301
|
-
* `description` is the instruction other agents should see.
|
|
292
|
+
* Peer-visible description of the work you're doing — the sentence another
|
|
293
|
+
* participant reads to decide whether to wait, work elsewhere, or move on
|
|
294
|
+
* (`'rewriting the risk section to match Q3'`). Surfaces in conflict
|
|
295
|
+
* messages and the activity overlay, and rides back in the rejection a
|
|
296
|
+
* blocked writer receives. Defaults to `'editing'`.
|
|
302
297
|
*/
|
|
303
298
|
readonly description?: string;
|
|
304
299
|
/**
|
|
@@ -318,7 +313,7 @@ export interface ClaimStream {
|
|
|
318
313
|
readonly others: readonly Claim[];
|
|
319
314
|
/**
|
|
320
315
|
* A reactive view of the wait queue on one target — the ordered line of
|
|
321
|
-
* queued claims behind the current holder, each carrying its `
|
|
316
|
+
* queued claims behind the current holder, each carrying its `description`,
|
|
322
317
|
* `heldBy`, and `position`. Empty when no one is waiting. Pair it with
|
|
323
318
|
* `onChange(...)` for change notifications.
|
|
324
319
|
*/
|
|
@@ -410,13 +405,11 @@ export interface Claim<T = Record<string, unknown>> {
|
|
|
410
405
|
/** What is being coordinated. */
|
|
411
406
|
readonly target: ClaimTarget;
|
|
412
407
|
/**
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* `'editing'
|
|
408
|
+
* Peer-visible description of the work being done — the same field on every
|
|
409
|
+
* claim surface. Always present: the SDK resolves it as it decodes the frame,
|
|
410
|
+
* defaulting to `'editing'` when a claim carries no description.
|
|
416
411
|
*/
|
|
417
|
-
readonly
|
|
418
|
-
/** Peer-visible explanation of the work being performed. */
|
|
419
|
-
readonly description?: string;
|
|
412
|
+
readonly description: string;
|
|
420
413
|
/** Participant holding it. Absent on a handle you hold for yourself. */
|
|
421
414
|
readonly heldBy?: string;
|
|
422
415
|
/**
|
|
@@ -451,6 +444,15 @@ export interface Claim<T = Record<string, unknown>> {
|
|
|
451
444
|
* uses it as the `readAt` staleness guard. Present on a claim you hold.
|
|
452
445
|
*/
|
|
453
446
|
readonly readAt?: number;
|
|
447
|
+
/**
|
|
448
|
+
* The monotonic fencing token the server minted for this grant (Option B). A
|
|
449
|
+
* write made under this claim carries it, and the server rejects the write if
|
|
450
|
+
* a later holder already advanced this entity past the token — closing the
|
|
451
|
+
* "my claim lapsed and its successor came and went" lost-update window that
|
|
452
|
+
* `readAt` alone cannot (a blind write with no read basis has nothing to
|
|
453
|
+
* compare). Present on a claim you hold that was granted a token.
|
|
454
|
+
*/
|
|
455
|
+
readonly fenceToken?: number;
|
|
454
456
|
/** Row snapshot under the lease — present on a claim you hold. */
|
|
455
457
|
readonly data?: T;
|
|
456
458
|
/**
|
|
@@ -508,3 +510,13 @@ export interface ClaimHeartbeat {
|
|
|
508
510
|
* `AsyncDisposable`.
|
|
509
511
|
*/
|
|
510
512
|
export type HeldClaim<T = Record<string, unknown>> = Claim<T> & Required<Pick<Claim<T>, 'data' | 'release' | 'revoke' | 'heartbeat' | typeof Symbol.asyncDispose>>;
|
|
513
|
+
/**
|
|
514
|
+
* A held advisory lease you took on a key whose row Ablo does not hold — the
|
|
515
|
+
* resolved value of the row-free `ablo.<model>.claim(id)` overload. It is a
|
|
516
|
+
* {@link HeldClaim} minus `.data`: the same lease controls (`release`, `revoke`,
|
|
517
|
+
* `heartbeat`, and the `await using` disposer are all required and behave
|
|
518
|
+
* identically), only without a snapshot, because the row lives solely in the
|
|
519
|
+
* customer's own database and was never replicated into the local pool. Use it
|
|
520
|
+
* to serialize work against a key you know by id alone.
|
|
521
|
+
*/
|
|
522
|
+
export type HeldLease = Omit<HeldClaim, 'data'>;
|