@agentplat/mesh 0.3.0-alpha.4 → 0.3.0-beta.1
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/README.md +10 -0
- package/dist/coordination-discovery-contracts.d.ts +3 -1
- package/dist/coordination-discovery-contracts.d.ts.map +1 -1
- package/dist/coordination-discovery-state.d.ts +3 -0
- package/dist/coordination-discovery-state.d.ts.map +1 -1
- package/dist/coordination-discovery-state.js +93 -8
- package/dist/coordination-discovery-state.js.map +1 -1
- package/dist/coordination-inbound.d.ts.map +1 -1
- package/dist/coordination-inbound.js +54 -3
- package/dist/coordination-inbound.js.map +1 -1
- package/dist/coordination-wire-version-contracts.d.ts +67 -0
- package/dist/coordination-wire-version-contracts.d.ts.map +1 -0
- package/dist/coordination-wire-version-contracts.js +2 -0
- package/dist/coordination-wire-version-contracts.js.map +1 -0
- package/dist/coordination-wire-version.d.ts +8 -0
- package/dist/coordination-wire-version.d.ts.map +1 -0
- package/dist/coordination-wire-version.js +285 -0
- package/dist/coordination-wire-version.js.map +1 -0
- package/dist/coordination.d.ts +3 -1
- package/dist/coordination.d.ts.map +1 -1
- package/dist/coordination.js +3 -1
- package/dist/coordination.js.map +1 -1
- package/dist/durability.d.ts +283 -0
- package/dist/durability.d.ts.map +1 -0
- package/dist/durability.js +749 -0
- package/dist/durability.js.map +1 -0
- package/dist/loopback.d.ts +3 -1
- package/dist/loopback.d.ts.map +1 -1
- package/dist/loopback.js +9 -7
- package/dist/loopback.js.map +1 -1
- package/fixtures/beta1/README.md +11 -0
- package/fixtures/beta1/durability/wrapper-v1.json +19 -0
- package/fixtures/beta1/durability/wrapper-v2.json +51 -0
- package/fixtures/beta1/manifest.json +147 -0
- package/fixtures/beta1/postgres/alpha5-dump-manifest.json +29 -0
- package/fixtures/beta1/postgres/migrations.json +25 -0
- package/fixtures/beta1/simulation/replay-trace-v1.json +60 -0
- package/fixtures/beta1/simulation/snapshot-v2.json +71 -0
- package/fixtures/beta1/states/allocation-v1.json +24 -0
- package/fixtures/beta1/states/allocation-v2.json +28 -0
- package/fixtures/beta1/states/allocation-v3.json +38 -0
- package/fixtures/beta1/states/allocation-v4.json +43 -0
- package/fixtures/beta1/states/allocation-v5.json +46 -0
- package/fixtures/beta1/states/allocation-v6.json +56 -0
- package/fixtures/beta1/states/coordination-v1.json +20 -0
- package/fixtures/beta1/states/core-peer.json +26 -0
- package/fixtures/beta1/states/discovery-v1.json +31 -0
- package/fixtures/beta1/states/discovery-v2.json +32 -0
- package/fixtures/beta1/states/inbound-v1.json +19 -0
- package/fixtures/beta1/states/objective-work-v1.json +28 -0
- package/fixtures/beta1/states/objective-work-v2.json +28 -0
- package/package.json +9 -4
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
import { canonicalizeMeshJsonBytes, validateSignedMeshEnvelope, } from "@agentplat/mesh-protocol";
|
|
2
|
+
export const MESH_DURABILITY_SCHEMA_VERSION = 2;
|
|
3
|
+
export const MESH_PREVIOUS_DURABILITY_SCHEMA_VERSION = 1;
|
|
4
|
+
export const MESH_DURABLE_ENVELOPE_FORMAT = "application/vnd.agentplat.mesh-envelope+json";
|
|
5
|
+
export const MESH_DURABLE_OPAQUE_SNAPSHOT_FORMAT = "application/json";
|
|
6
|
+
export const MESH_DURABLE_LEGACY_OPAQUE_SNAPSHOT_FORMAT = "application/json; profile=legacy-opaque";
|
|
7
|
+
export const MESH_DURABLE_JOURNAL_VERSION = 1;
|
|
8
|
+
export const MESH_DURABLE_GENESIS_DIGEST = "sha256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
|
9
|
+
/** Creates an inert worker. Work starts only through an explicit run method. */
|
|
10
|
+
export function createMeshDurableWorker(options) {
|
|
11
|
+
if (!options ||
|
|
12
|
+
!options.repository ||
|
|
13
|
+
typeof options.processInbox !== "function" ||
|
|
14
|
+
typeof options.deliverOutbox !== "function") {
|
|
15
|
+
throw new TypeError("Mesh durable worker dependencies are required");
|
|
16
|
+
}
|
|
17
|
+
const scope = normalizeMeshDurableScope(options.scope);
|
|
18
|
+
const workerId = boundedIdentifier(options.workerId, "workerId");
|
|
19
|
+
const inboxBatchSize = boundedInteger(options.inboxBatchSize ?? 16, "inboxBatchSize", 256);
|
|
20
|
+
const outboxBatchSize = boundedInteger(options.outboxBatchSize ?? 16, "outboxBatchSize", 256);
|
|
21
|
+
const leaseDurationMs = boundedInteger(options.leaseDurationMs ?? 30_000, "leaseDurationMs", 3_600_000);
|
|
22
|
+
const failureRetryAfterMs = boundedInteger(options.failureRetryAfterMs ?? 1_000, "failureRetryAfterMs", 3_600_000);
|
|
23
|
+
const runInboxBatch = async (signal) => {
|
|
24
|
+
if (signal?.aborted)
|
|
25
|
+
return emptyBatch();
|
|
26
|
+
const records = await options.repository.claimInbox({
|
|
27
|
+
scope,
|
|
28
|
+
workerId,
|
|
29
|
+
limit: inboxBatchSize,
|
|
30
|
+
leaseDurationMs,
|
|
31
|
+
});
|
|
32
|
+
let completed = 0;
|
|
33
|
+
let conflicted = 0;
|
|
34
|
+
let failed = 0;
|
|
35
|
+
for (const inbox of records) {
|
|
36
|
+
if (signal?.aborted)
|
|
37
|
+
break;
|
|
38
|
+
try {
|
|
39
|
+
assertInboxClaim(inbox, scope, workerId);
|
|
40
|
+
const snapshot = await options.repository.loadSnapshot(scope);
|
|
41
|
+
const processed = await options.processInbox({
|
|
42
|
+
scope,
|
|
43
|
+
inbox,
|
|
44
|
+
snapshot,
|
|
45
|
+
signal,
|
|
46
|
+
});
|
|
47
|
+
const normalized = normalizeProcessorResult(processed, scope, inbox);
|
|
48
|
+
const commit = await options.repository.commitInboxTransition({
|
|
49
|
+
inbox,
|
|
50
|
+
expectedSnapshotRevision: snapshot?.revision ?? 0,
|
|
51
|
+
transitionId: normalized.transitionId ?? `inbox:${inbox.messageId}`,
|
|
52
|
+
outcome: normalized.outcome,
|
|
53
|
+
...(normalized.outcome === "applied"
|
|
54
|
+
? { nextState: normalized.nextState }
|
|
55
|
+
: { reasonCode: normalized.reasonCode }),
|
|
56
|
+
journal: normalized.journal ?? [],
|
|
57
|
+
outbox: normalized.outcome === "applied" ? (normalized.outbox ?? []) : [],
|
|
58
|
+
});
|
|
59
|
+
if (commit.committed)
|
|
60
|
+
completed += 1;
|
|
61
|
+
else {
|
|
62
|
+
conflicted += 1;
|
|
63
|
+
workerDiagnostic(options, {
|
|
64
|
+
kind: "inbox.commit_conflict",
|
|
65
|
+
recordId: inbox.messageId,
|
|
66
|
+
code: commit.code,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
failed += 1;
|
|
72
|
+
workerDiagnostic(options, {
|
|
73
|
+
kind: "inbox.processor_failure",
|
|
74
|
+
recordId: inbox.messageId,
|
|
75
|
+
});
|
|
76
|
+
const abandoned = await options.repository
|
|
77
|
+
.abandonInbox({
|
|
78
|
+
inbox,
|
|
79
|
+
retryAfterMs: failureRetryAfterMs,
|
|
80
|
+
reasonCode: "processor_failure",
|
|
81
|
+
})
|
|
82
|
+
.catch(() => false);
|
|
83
|
+
if (abandoned) {
|
|
84
|
+
workerDiagnostic(options, {
|
|
85
|
+
kind: "inbox.abandoned",
|
|
86
|
+
recordId: inbox.messageId,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
claimed: records.length,
|
|
93
|
+
completed,
|
|
94
|
+
conflicted,
|
|
95
|
+
failed,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const runOutboxBatch = async (signal) => {
|
|
99
|
+
if (signal?.aborted)
|
|
100
|
+
return emptyBatch();
|
|
101
|
+
const records = await options.repository.claimOutbox({
|
|
102
|
+
scope,
|
|
103
|
+
workerId,
|
|
104
|
+
limit: outboxBatchSize,
|
|
105
|
+
leaseDurationMs,
|
|
106
|
+
});
|
|
107
|
+
let completed = 0;
|
|
108
|
+
let conflicted = 0;
|
|
109
|
+
let failed = 0;
|
|
110
|
+
for (const outbox of records) {
|
|
111
|
+
if (signal?.aborted)
|
|
112
|
+
break;
|
|
113
|
+
let settlement;
|
|
114
|
+
try {
|
|
115
|
+
assertOutboxClaim(outbox, scope, workerId);
|
|
116
|
+
settlement = normalizeSettlement(await options.deliverOutbox(outbox, signal));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
failed += 1;
|
|
120
|
+
workerDiagnostic(options, {
|
|
121
|
+
kind: "outbox.delivery_failure",
|
|
122
|
+
recordId: outbox.effectId,
|
|
123
|
+
});
|
|
124
|
+
settlement = {
|
|
125
|
+
disposition: "retryable",
|
|
126
|
+
retryAfterMs: failureRetryAfterMs,
|
|
127
|
+
reasonCode: "delivery_failure",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const settled = await options.repository
|
|
131
|
+
.settleOutbox({ outbox, settlement })
|
|
132
|
+
.catch(() => false);
|
|
133
|
+
if (settled)
|
|
134
|
+
completed += 1;
|
|
135
|
+
else {
|
|
136
|
+
conflicted += 1;
|
|
137
|
+
workerDiagnostic(options, {
|
|
138
|
+
kind: "outbox.settle_conflict",
|
|
139
|
+
recordId: outbox.effectId,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
claimed: records.length,
|
|
145
|
+
completed,
|
|
146
|
+
conflicted,
|
|
147
|
+
failed,
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
const runOnce = async (signal) => {
|
|
151
|
+
const inbox = await runInboxBatch(signal);
|
|
152
|
+
const outbox = signal?.aborted
|
|
153
|
+
? emptyBatch()
|
|
154
|
+
: await runOutboxBatch(signal);
|
|
155
|
+
return Object.freeze({ inbox, outbox });
|
|
156
|
+
};
|
|
157
|
+
return Object.freeze({
|
|
158
|
+
runInboxBatch,
|
|
159
|
+
runOutboxBatch,
|
|
160
|
+
runOnce,
|
|
161
|
+
async start({ signal, idleDelayMs = 100, }) {
|
|
162
|
+
const idle = boundedInteger(idleDelayMs, "idleDelayMs", 60_000);
|
|
163
|
+
while (!signal.aborted) {
|
|
164
|
+
const result = await runOnce(signal);
|
|
165
|
+
if (!signal.aborted &&
|
|
166
|
+
result.inbox.claimed === 0 &&
|
|
167
|
+
result.outbox.claimed === 0) {
|
|
168
|
+
await abortableDelay(idle, signal);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export function normalizeMeshDurableScope(scope) {
|
|
175
|
+
if (!scope || typeof scope !== "object") {
|
|
176
|
+
throw new TypeError("Mesh durable scope is required");
|
|
177
|
+
}
|
|
178
|
+
const keys = Object.keys(scope).sort();
|
|
179
|
+
if (keys.length !== 4 ||
|
|
180
|
+
keys[0] !== "instanceId" ||
|
|
181
|
+
keys[1] !== "meshId" ||
|
|
182
|
+
keys[2] !== "peerId" ||
|
|
183
|
+
keys[3] !== "tenantId") {
|
|
184
|
+
throw new TypeError("Mesh durable scope must have an exact shape");
|
|
185
|
+
}
|
|
186
|
+
return Object.freeze({
|
|
187
|
+
tenantId: boundedIdentifier(scope.tenantId, "tenantId"),
|
|
188
|
+
meshId: boundedIdentifier(scope.meshId, "meshId"),
|
|
189
|
+
peerId: boundedIdentifier(scope.peerId, "peerId"),
|
|
190
|
+
instanceId: boundedIdentifier(scope.instanceId, "instanceId"),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Creates an immutable, exact-format snapshot codec registry. Codec selection
|
|
195
|
+
* never guesses from state shape and every decode verifies the stored digest.
|
|
196
|
+
*/
|
|
197
|
+
export function createMeshDurableSnapshotCodecRegistry(codecs, options = {}) {
|
|
198
|
+
if (!Array.isArray(codecs) || codecs.length < 1 || codecs.length > 64) {
|
|
199
|
+
throw new RangeError("Mesh snapshot codec count is outside its range");
|
|
200
|
+
}
|
|
201
|
+
const registered = new Map();
|
|
202
|
+
for (const codec of codecs) {
|
|
203
|
+
if (!codec ||
|
|
204
|
+
typeof codec !== "object" ||
|
|
205
|
+
typeof codec.encode !== "function" ||
|
|
206
|
+
typeof codec.decode !== "function") {
|
|
207
|
+
throw new TypeError("Mesh snapshot codec is invalid");
|
|
208
|
+
}
|
|
209
|
+
const descriptor = normalizeSnapshotDescriptor(codec.descriptor);
|
|
210
|
+
if (registered.has(descriptor.format)) {
|
|
211
|
+
throw new TypeError("Mesh snapshot codec format is duplicated");
|
|
212
|
+
}
|
|
213
|
+
const versions = codec.readableSchemaVersions ?? [descriptor.schemaVersion];
|
|
214
|
+
if (!Array.isArray(versions) ||
|
|
215
|
+
versions.length < 1 ||
|
|
216
|
+
versions.length > 64) {
|
|
217
|
+
throw new RangeError("Mesh snapshot readable schema count is outside its range");
|
|
218
|
+
}
|
|
219
|
+
const readable = new Set();
|
|
220
|
+
for (const version of versions) {
|
|
221
|
+
const normalized = snapshotSchemaVersion(version);
|
|
222
|
+
if (readable.has(normalized)) {
|
|
223
|
+
throw new TypeError("Mesh snapshot readable schema is duplicated");
|
|
224
|
+
}
|
|
225
|
+
readable.add(normalized);
|
|
226
|
+
}
|
|
227
|
+
if (!readable.has(descriptor.schemaVersion)) {
|
|
228
|
+
throw new TypeError("Mesh snapshot codec must read its current schema");
|
|
229
|
+
}
|
|
230
|
+
const encodeSnapshot = codec.encode;
|
|
231
|
+
const decodeSnapshot = codec.decode;
|
|
232
|
+
const migrateSnapshot = codec.migrate;
|
|
233
|
+
registered.set(descriptor.format, Object.freeze({
|
|
234
|
+
descriptor,
|
|
235
|
+
readable,
|
|
236
|
+
encode: (value) => encodeSnapshot(value),
|
|
237
|
+
decode: (state, schemaVersion) => decodeSnapshot(state, schemaVersion),
|
|
238
|
+
...(migrateSnapshot === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: {
|
|
241
|
+
migrate: (state, fromSchemaVersion) => migrateSnapshot(state, fromSchemaVersion),
|
|
242
|
+
}),
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
const encode = async (format, value) => {
|
|
246
|
+
const registration = requireSnapshotCodec(registered, format);
|
|
247
|
+
return snapshotEncoding(registration.descriptor, registration.encode(value), options);
|
|
248
|
+
};
|
|
249
|
+
const decode = async (snapshot) => {
|
|
250
|
+
const metadata = snapshotMetadata(snapshot);
|
|
251
|
+
const registration = requireSnapshotCodec(registered, metadata.format);
|
|
252
|
+
if (!registration.readable.has(metadata.schemaVersion)) {
|
|
253
|
+
throw new TypeError("Mesh snapshot schema version is unsupported");
|
|
254
|
+
}
|
|
255
|
+
const actualDigest = await computeMeshDurableValueDigest(snapshot.state, options);
|
|
256
|
+
if (actualDigest !== snapshot.stateDigest) {
|
|
257
|
+
throw new TypeError("Mesh snapshot state digest does not match");
|
|
258
|
+
}
|
|
259
|
+
return deepFreezeDurableValue(registration.decode(cloneSnapshotState(snapshot.state, options.protocolOptions), metadata.schemaVersion));
|
|
260
|
+
};
|
|
261
|
+
const migrate = async (snapshot) => {
|
|
262
|
+
const metadata = snapshotMetadata(snapshot);
|
|
263
|
+
const registration = requireSnapshotCodec(registered, metadata.format);
|
|
264
|
+
await decode(snapshot);
|
|
265
|
+
if (metadata.schemaVersion === registration.descriptor.schemaVersion) {
|
|
266
|
+
return snapshotEncoding(registration.descriptor, snapshot.state, options);
|
|
267
|
+
}
|
|
268
|
+
if (registration.migrate === undefined) {
|
|
269
|
+
throw new TypeError("Mesh snapshot migration is unavailable");
|
|
270
|
+
}
|
|
271
|
+
const first = registration.migrate(cloneSnapshotState(snapshot.state, options.protocolOptions), metadata.schemaVersion);
|
|
272
|
+
const second = registration.migrate(cloneSnapshotState(snapshot.state, options.protocolOptions), metadata.schemaVersion);
|
|
273
|
+
const firstCanonical = canonicalSnapshotBytes(first, options.protocolOptions);
|
|
274
|
+
const secondCanonical = canonicalSnapshotBytes(second, options.protocolOptions);
|
|
275
|
+
if (!bytesEqual(firstCanonical, secondCanonical)) {
|
|
276
|
+
throw new TypeError("Mesh snapshot migration is nondeterministic");
|
|
277
|
+
}
|
|
278
|
+
const decoded = registration.decode(cloneSnapshotState(first, options.protocolOptions), registration.descriptor.schemaVersion);
|
|
279
|
+
const normalized = registration.encode(deepFreezeDurableValue(decoded));
|
|
280
|
+
if (!bytesEqual(firstCanonical, canonicalSnapshotBytes(normalized, options.protocolOptions))) {
|
|
281
|
+
throw new TypeError("Mesh snapshot migration is not canonical");
|
|
282
|
+
}
|
|
283
|
+
return snapshotEncoding(registration.descriptor, normalized, options);
|
|
284
|
+
};
|
|
285
|
+
return Object.freeze({
|
|
286
|
+
formats: Object.freeze([...registered.keys()].sort()),
|
|
287
|
+
encode,
|
|
288
|
+
decode,
|
|
289
|
+
migrate,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
export async function computeMeshDurableValueDigest(value, options = {}) {
|
|
293
|
+
const canonical = canonicalizeMeshJsonBytes(value, options.protocolOptions);
|
|
294
|
+
if (!canonical.ok) {
|
|
295
|
+
throw new TypeError("Mesh durable value must be bounded strict JSON");
|
|
296
|
+
}
|
|
297
|
+
const buffer = new ArrayBuffer(canonical.value.byteLength);
|
|
298
|
+
new Uint8Array(buffer).set(canonical.value);
|
|
299
|
+
const digest = await (options.crypto ?? globalThis.crypto).subtle.digest("SHA-256", buffer);
|
|
300
|
+
return `sha256:${base64Url(new Uint8Array(digest))}`;
|
|
301
|
+
}
|
|
302
|
+
export async function createMeshDurableJournalEntry(input) {
|
|
303
|
+
const scope = normalizeMeshDurableScope(input.scope);
|
|
304
|
+
const sequence = boundedInteger(input.sequence, "journal sequence", Number.MAX_SAFE_INTEGER);
|
|
305
|
+
const snapshotRevision = nonNegativeInteger(input.snapshotRevision, "snapshot revision");
|
|
306
|
+
const draft = normalizeJournalDraft(input.draft);
|
|
307
|
+
assertDigest(input.previousDigest, "previous digest");
|
|
308
|
+
assertDigest(input.snapshotDigest, "snapshot digest");
|
|
309
|
+
boundedIdentifier(input.transitionId, "transitionId");
|
|
310
|
+
if (input.inboxMessageId !== undefined) {
|
|
311
|
+
assertMessageId(input.inboxMessageId);
|
|
312
|
+
}
|
|
313
|
+
assertTimestamp(input.occurredAt, "occurredAt");
|
|
314
|
+
const schemaVersion = input.schemaVersion ?? MESH_DURABILITY_SCHEMA_VERSION;
|
|
315
|
+
if (schemaVersion !== MESH_PREVIOUS_DURABILITY_SCHEMA_VERSION &&
|
|
316
|
+
schemaVersion !== MESH_DURABILITY_SCHEMA_VERSION) {
|
|
317
|
+
throw new TypeError("Mesh durability schema version is unsupported");
|
|
318
|
+
}
|
|
319
|
+
const document = {
|
|
320
|
+
schemaVersion,
|
|
321
|
+
...(schemaVersion === MESH_DURABILITY_SCHEMA_VERSION
|
|
322
|
+
? { journalVersion: MESH_DURABLE_JOURNAL_VERSION }
|
|
323
|
+
: {}),
|
|
324
|
+
scope: {
|
|
325
|
+
tenantId: scope.tenantId,
|
|
326
|
+
meshId: scope.meshId,
|
|
327
|
+
peerId: scope.peerId,
|
|
328
|
+
instanceId: scope.instanceId,
|
|
329
|
+
},
|
|
330
|
+
sequence,
|
|
331
|
+
previousDigest: input.previousDigest,
|
|
332
|
+
transitionId: input.transitionId,
|
|
333
|
+
...(input.inboxMessageId === undefined
|
|
334
|
+
? {}
|
|
335
|
+
: { inboxMessageId: input.inboxMessageId }),
|
|
336
|
+
snapshotRevision,
|
|
337
|
+
snapshotDigest: input.snapshotDigest,
|
|
338
|
+
entryId: draft.entryId,
|
|
339
|
+
kind: draft.kind,
|
|
340
|
+
...(draft.reasonCode === undefined ? {} : { reasonCode: draft.reasonCode }),
|
|
341
|
+
occurredAt: input.occurredAt,
|
|
342
|
+
};
|
|
343
|
+
const digest = await computeMeshDurableValueDigest(document, {
|
|
344
|
+
crypto: input.crypto,
|
|
345
|
+
});
|
|
346
|
+
return Object.freeze({
|
|
347
|
+
schemaVersion,
|
|
348
|
+
...(schemaVersion === MESH_DURABILITY_SCHEMA_VERSION
|
|
349
|
+
? { journalVersion: MESH_DURABLE_JOURNAL_VERSION }
|
|
350
|
+
: {}),
|
|
351
|
+
scope,
|
|
352
|
+
sequence,
|
|
353
|
+
previousDigest: input.previousDigest,
|
|
354
|
+
digest,
|
|
355
|
+
transitionId: input.transitionId,
|
|
356
|
+
...(input.inboxMessageId === undefined
|
|
357
|
+
? {}
|
|
358
|
+
: { inboxMessageId: input.inboxMessageId }),
|
|
359
|
+
snapshotRevision,
|
|
360
|
+
snapshotDigest: input.snapshotDigest,
|
|
361
|
+
entryId: draft.entryId,
|
|
362
|
+
kind: draft.kind,
|
|
363
|
+
...(draft.reasonCode === undefined ? {} : { reasonCode: draft.reasonCode }),
|
|
364
|
+
occurredAt: input.occurredAt,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
export async function verifyMeshDurableJournal(input) {
|
|
368
|
+
if ((input.anchorDigest === undefined) !==
|
|
369
|
+
(input.anchorSequence === undefined) ||
|
|
370
|
+
(input.expectedHeadDigest === undefined) !==
|
|
371
|
+
(input.expectedHeadSequence === undefined)) {
|
|
372
|
+
throw new TypeError("Mesh durable journal anchor is incomplete");
|
|
373
|
+
}
|
|
374
|
+
let previousDigest = input.anchorDigest ?? MESH_DURABLE_GENESIS_DIGEST;
|
|
375
|
+
let sequence = input.anchorSequence ?? 0;
|
|
376
|
+
assertDigest(previousDigest, "anchor digest");
|
|
377
|
+
nonNegativeInteger(sequence, "anchor sequence");
|
|
378
|
+
if (input.expectedHeadDigest !== undefined) {
|
|
379
|
+
assertDigest(input.expectedHeadDigest, "expected head digest");
|
|
380
|
+
nonNegativeInteger(input.expectedHeadSequence, "expected head sequence");
|
|
381
|
+
}
|
|
382
|
+
for (const entry of input.entries) {
|
|
383
|
+
if (entry.sequence !== sequence + 1 ||
|
|
384
|
+
entry.previousDigest !== previousDigest ||
|
|
385
|
+
(entry.schemaVersion === MESH_DURABILITY_SCHEMA_VERSION
|
|
386
|
+
? entry.journalVersion !== MESH_DURABLE_JOURNAL_VERSION
|
|
387
|
+
: entry.journalVersion !== undefined)) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const rebuilt = await createMeshDurableJournalEntry({
|
|
391
|
+
scope: entry.scope,
|
|
392
|
+
sequence: entry.sequence,
|
|
393
|
+
previousDigest: entry.previousDigest,
|
|
394
|
+
transitionId: entry.transitionId,
|
|
395
|
+
inboxMessageId: entry.inboxMessageId,
|
|
396
|
+
snapshotRevision: entry.snapshotRevision,
|
|
397
|
+
snapshotDigest: entry.snapshotDigest,
|
|
398
|
+
draft: entry,
|
|
399
|
+
occurredAt: entry.occurredAt,
|
|
400
|
+
crypto: input.crypto,
|
|
401
|
+
schemaVersion: entry.schemaVersion,
|
|
402
|
+
});
|
|
403
|
+
if (rebuilt.digest !== entry.digest)
|
|
404
|
+
return false;
|
|
405
|
+
previousDigest = entry.digest;
|
|
406
|
+
sequence = entry.sequence;
|
|
407
|
+
}
|
|
408
|
+
return (input.expectedHeadDigest === undefined ||
|
|
409
|
+
(previousDigest === input.expectedHeadDigest &&
|
|
410
|
+
sequence === input.expectedHeadSequence));
|
|
411
|
+
}
|
|
412
|
+
function normalizeProcessorResult(result, scope, inbox) {
|
|
413
|
+
if (!result || typeof result !== "object") {
|
|
414
|
+
throw new TypeError("Mesh durable processor result is required");
|
|
415
|
+
}
|
|
416
|
+
if (result.transitionId !== undefined) {
|
|
417
|
+
boundedIdentifier(result.transitionId, "transitionId");
|
|
418
|
+
}
|
|
419
|
+
const journal = Object.freeze((result.journal ?? []).map(normalizeJournalDraft));
|
|
420
|
+
if (journal.length > 64) {
|
|
421
|
+
throw new RangeError("Mesh durable transition journal is too large");
|
|
422
|
+
}
|
|
423
|
+
if (result.outcome === "rejected") {
|
|
424
|
+
boundedReason(result.reasonCode);
|
|
425
|
+
if ("outbox" in result || "nextState" in result) {
|
|
426
|
+
throw new TypeError("Rejected durable transition cannot change state");
|
|
427
|
+
}
|
|
428
|
+
return Object.freeze({
|
|
429
|
+
outcome: "rejected",
|
|
430
|
+
...(result.transitionId ? { transitionId: result.transitionId } : {}),
|
|
431
|
+
reasonCode: result.reasonCode,
|
|
432
|
+
journal,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
if (result.outcome !== "applied" || result.nextState === undefined) {
|
|
436
|
+
throw new TypeError("Applied durable transition requires nextState");
|
|
437
|
+
}
|
|
438
|
+
const canonical = canonicalizeMeshJsonBytes(result.nextState);
|
|
439
|
+
if (!canonical.ok) {
|
|
440
|
+
throw new TypeError("Mesh durable nextState must be bounded strict JSON");
|
|
441
|
+
}
|
|
442
|
+
const outbox = Object.freeze((result.outbox ?? []).map((draft) => normalizeOutboundDraft(draft, scope, inbox)));
|
|
443
|
+
if (outbox.length > 256) {
|
|
444
|
+
throw new RangeError("Mesh durable transition outbox is too large");
|
|
445
|
+
}
|
|
446
|
+
return Object.freeze({
|
|
447
|
+
outcome: "applied",
|
|
448
|
+
...(result.transitionId ? { transitionId: result.transitionId } : {}),
|
|
449
|
+
nextState: result.nextState,
|
|
450
|
+
journal,
|
|
451
|
+
outbox,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function normalizeOutboundDraft(draft, scope, inbox) {
|
|
455
|
+
if (!draft || typeof draft !== "object") {
|
|
456
|
+
throw new TypeError("Mesh durable outbound draft is invalid");
|
|
457
|
+
}
|
|
458
|
+
boundedIdentifier(draft.effectId, "effectId");
|
|
459
|
+
const validated = validateSignedMeshEnvelope(draft.envelope);
|
|
460
|
+
if (!validated.ok ||
|
|
461
|
+
validated.value.tenantId !== scope.tenantId ||
|
|
462
|
+
validated.value.meshId !== scope.meshId ||
|
|
463
|
+
validated.value.sender.peerId !== scope.peerId ||
|
|
464
|
+
validated.value.sender.instanceId !== scope.instanceId) {
|
|
465
|
+
throw new TypeError("Mesh durable outbound envelope scope is invalid");
|
|
466
|
+
}
|
|
467
|
+
if (draft.targetPeerId !== undefined) {
|
|
468
|
+
boundedIdentifier(draft.targetPeerId, "targetPeerId");
|
|
469
|
+
if (validated.value.audience.kind === "peer" &&
|
|
470
|
+
validated.value.audience.peerId !== draft.targetPeerId) {
|
|
471
|
+
throw new TypeError("Mesh durable outbound target is invalid");
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (validated.value.audience.kind === "mesh" &&
|
|
475
|
+
draft.targetPeerId === undefined) {
|
|
476
|
+
throw new TypeError("Mesh durable topic outbox requires targetPeerId");
|
|
477
|
+
}
|
|
478
|
+
if (validated.value.messageId === inbox.messageId) {
|
|
479
|
+
throw new TypeError("Mesh durable outbound messageId must be distinct");
|
|
480
|
+
}
|
|
481
|
+
return Object.freeze({
|
|
482
|
+
effectId: draft.effectId,
|
|
483
|
+
envelope: validated.value,
|
|
484
|
+
...(draft.targetPeerId === undefined
|
|
485
|
+
? {}
|
|
486
|
+
: { targetPeerId: draft.targetPeerId }),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function normalizeJournalDraft(draft) {
|
|
490
|
+
if (!draft || typeof draft !== "object") {
|
|
491
|
+
throw new TypeError("Mesh durable journal draft is invalid");
|
|
492
|
+
}
|
|
493
|
+
boundedIdentifier(draft.entryId, "journal entryId");
|
|
494
|
+
boundedReason(draft.kind, "journal kind");
|
|
495
|
+
if (draft.reasonCode !== undefined)
|
|
496
|
+
boundedReason(draft.reasonCode);
|
|
497
|
+
return Object.freeze({
|
|
498
|
+
entryId: draft.entryId,
|
|
499
|
+
kind: draft.kind,
|
|
500
|
+
...(draft.reasonCode === undefined ? {} : { reasonCode: draft.reasonCode }),
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function normalizeSettlement(settlement) {
|
|
504
|
+
if (!settlement || typeof settlement !== "object") {
|
|
505
|
+
throw new TypeError("Mesh durable outbox settlement is invalid");
|
|
506
|
+
}
|
|
507
|
+
if (settlement.reasonCode !== undefined)
|
|
508
|
+
boundedReason(settlement.reasonCode);
|
|
509
|
+
if (settlement.disposition === "retryable") {
|
|
510
|
+
return Object.freeze({
|
|
511
|
+
disposition: "retryable",
|
|
512
|
+
retryAfterMs: boundedInteger(settlement.retryAfterMs, "retryAfterMs", 3_600_000),
|
|
513
|
+
...(settlement.reasonCode === undefined
|
|
514
|
+
? {}
|
|
515
|
+
: { reasonCode: settlement.reasonCode }),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
if (settlement.disposition !== "delivered" &&
|
|
519
|
+
settlement.disposition !== "permanent_rejection") {
|
|
520
|
+
throw new TypeError("Mesh durable outbox settlement is invalid");
|
|
521
|
+
}
|
|
522
|
+
return Object.freeze({
|
|
523
|
+
disposition: settlement.disposition,
|
|
524
|
+
...(settlement.reasonCode === undefined
|
|
525
|
+
? {}
|
|
526
|
+
: { reasonCode: settlement.reasonCode }),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function assertInboxClaim(inbox, scope, workerId) {
|
|
530
|
+
if (inbox.status !== "processing" ||
|
|
531
|
+
!inbox.claim ||
|
|
532
|
+
inbox.claim.workerId !== workerId ||
|
|
533
|
+
!scopeEquals(inbox.scope, scope)) {
|
|
534
|
+
throw new TypeError("Mesh durable inbox claim is invalid");
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
function assertOutboxClaim(outbox, scope, workerId) {
|
|
538
|
+
if (outbox.status !== "delivering" ||
|
|
539
|
+
!outbox.claim ||
|
|
540
|
+
outbox.claim.workerId !== workerId ||
|
|
541
|
+
!scopeEquals(outbox.scope, scope)) {
|
|
542
|
+
throw new TypeError("Mesh durable outbox claim is invalid");
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
function scopeEquals(left, right) {
|
|
546
|
+
return (left.tenantId === right.tenantId &&
|
|
547
|
+
left.meshId === right.meshId &&
|
|
548
|
+
left.peerId === right.peerId &&
|
|
549
|
+
left.instanceId === right.instanceId);
|
|
550
|
+
}
|
|
551
|
+
function workerDiagnostic(options, diagnostic) {
|
|
552
|
+
try {
|
|
553
|
+
options.onDiagnostic?.(Object.freeze(diagnostic));
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
// Diagnostics never change durable state.
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function emptyBatch() {
|
|
560
|
+
return Object.freeze({ claimed: 0, completed: 0, conflicted: 0, failed: 0 });
|
|
561
|
+
}
|
|
562
|
+
function abortableDelay(milliseconds, signal) {
|
|
563
|
+
if (signal.aborted)
|
|
564
|
+
return Promise.resolve();
|
|
565
|
+
return new Promise((resolve) => {
|
|
566
|
+
const timer = setTimeout(done, milliseconds);
|
|
567
|
+
signal.addEventListener("abort", done, { once: true });
|
|
568
|
+
function done() {
|
|
569
|
+
clearTimeout(timer);
|
|
570
|
+
signal.removeEventListener("abort", done);
|
|
571
|
+
resolve();
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
function boundedIdentifier(value, label) {
|
|
576
|
+
if (typeof value !== "string" ||
|
|
577
|
+
value.length < 1 ||
|
|
578
|
+
new TextEncoder().encode(value).byteLength > 256 ||
|
|
579
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:@-]*$/u.test(value)) {
|
|
580
|
+
throw new TypeError(`Mesh durable ${label} is invalid`);
|
|
581
|
+
}
|
|
582
|
+
return value;
|
|
583
|
+
}
|
|
584
|
+
function boundedReason(value, label = "reasonCode") {
|
|
585
|
+
if (typeof value !== "string" ||
|
|
586
|
+
value.length < 1 ||
|
|
587
|
+
value.length > 128 ||
|
|
588
|
+
!/^[a-z0-9][a-z0-9._:-]*$/u.test(value)) {
|
|
589
|
+
throw new TypeError(`Mesh durable ${label} is invalid`);
|
|
590
|
+
}
|
|
591
|
+
return value;
|
|
592
|
+
}
|
|
593
|
+
function assertMessageId(value) {
|
|
594
|
+
// Protocol message IDs are canonical base64url encodings of 16 bytes. The
|
|
595
|
+
// final character carries two significant bits and four zero padding bits.
|
|
596
|
+
if (typeof value !== "string" || !/^[A-Za-z0-9_-]{21}[AQgw]$/u.test(value)) {
|
|
597
|
+
throw new TypeError("Mesh durable inboxMessageId is invalid");
|
|
598
|
+
}
|
|
599
|
+
return value;
|
|
600
|
+
}
|
|
601
|
+
function boundedInteger(value, label, maximum) {
|
|
602
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
|
|
603
|
+
throw new RangeError(`Mesh durable ${label} is outside its range`);
|
|
604
|
+
}
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
function nonNegativeInteger(value, label) {
|
|
608
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
609
|
+
throw new RangeError(`Mesh durable ${label} is outside its range`);
|
|
610
|
+
}
|
|
611
|
+
return value;
|
|
612
|
+
}
|
|
613
|
+
function normalizeSnapshotDescriptor(descriptor) {
|
|
614
|
+
if (!descriptor || typeof descriptor !== "object") {
|
|
615
|
+
throw new TypeError("Mesh snapshot descriptor is required");
|
|
616
|
+
}
|
|
617
|
+
const keys = Object.keys(descriptor).sort();
|
|
618
|
+
if (keys.length !== 2 ||
|
|
619
|
+
keys[0] !== "format" ||
|
|
620
|
+
keys[1] !== "schemaVersion") {
|
|
621
|
+
throw new TypeError("Mesh snapshot descriptor must have an exact shape");
|
|
622
|
+
}
|
|
623
|
+
return Object.freeze({
|
|
624
|
+
format: snapshotFormat(descriptor.format),
|
|
625
|
+
schemaVersion: snapshotSchemaVersion(descriptor.schemaVersion),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
function snapshotMetadata(snapshot) {
|
|
629
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
630
|
+
throw new TypeError("Mesh snapshot is required");
|
|
631
|
+
}
|
|
632
|
+
const keys = Object.keys(snapshot).sort();
|
|
633
|
+
const expectedKeys = [
|
|
634
|
+
"committedAt",
|
|
635
|
+
"revision",
|
|
636
|
+
"schemaVersion",
|
|
637
|
+
"scope",
|
|
638
|
+
"snapshotFormat",
|
|
639
|
+
"snapshotSchemaVersion",
|
|
640
|
+
"state",
|
|
641
|
+
"stateDigest",
|
|
642
|
+
];
|
|
643
|
+
if (keys.length !== expectedKeys.length ||
|
|
644
|
+
keys.some((key, index) => key !== expectedKeys[index])) {
|
|
645
|
+
throw new TypeError("Mesh snapshot must have an exact shape");
|
|
646
|
+
}
|
|
647
|
+
if (snapshot.snapshotFormat === undefined ||
|
|
648
|
+
snapshot.snapshotSchemaVersion === undefined) {
|
|
649
|
+
throw new TypeError("Legacy Mesh snapshot requires an explicit backfill");
|
|
650
|
+
}
|
|
651
|
+
if (snapshot.schemaVersion !== MESH_PREVIOUS_DURABILITY_SCHEMA_VERSION &&
|
|
652
|
+
snapshot.schemaVersion !== MESH_DURABILITY_SCHEMA_VERSION) {
|
|
653
|
+
throw new TypeError("Mesh snapshot wrapper schema is unsupported");
|
|
654
|
+
}
|
|
655
|
+
normalizeMeshDurableScope(snapshot.scope);
|
|
656
|
+
boundedInteger(snapshot.revision, "snapshot revision", Number.MAX_SAFE_INTEGER);
|
|
657
|
+
assertTimestamp(snapshot.committedAt, "snapshot committedAt");
|
|
658
|
+
assertDigest(snapshot.stateDigest, "snapshot state digest");
|
|
659
|
+
const metadata = {
|
|
660
|
+
format: snapshotFormat(snapshot.snapshotFormat),
|
|
661
|
+
schemaVersion: snapshotSchemaVersion(snapshot.snapshotSchemaVersion),
|
|
662
|
+
};
|
|
663
|
+
if ((snapshot.schemaVersion === MESH_PREVIOUS_DURABILITY_SCHEMA_VERSION) !==
|
|
664
|
+
(metadata.format === MESH_DURABLE_LEGACY_OPAQUE_SNAPSHOT_FORMAT)) {
|
|
665
|
+
throw new TypeError("Mesh snapshot format does not match its wrapper");
|
|
666
|
+
}
|
|
667
|
+
return metadata;
|
|
668
|
+
}
|
|
669
|
+
function snapshotFormat(value) {
|
|
670
|
+
if (typeof value !== "string" ||
|
|
671
|
+
value.length < 3 ||
|
|
672
|
+
new TextEncoder().encode(value).byteLength > 256 ||
|
|
673
|
+
/[\u0000-\u001f\u007f]/u.test(value)) {
|
|
674
|
+
throw new TypeError("Mesh snapshot format is invalid");
|
|
675
|
+
}
|
|
676
|
+
return value;
|
|
677
|
+
}
|
|
678
|
+
function snapshotSchemaVersion(value) {
|
|
679
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > 65_535) {
|
|
680
|
+
throw new RangeError("Mesh snapshot schema version is outside its range");
|
|
681
|
+
}
|
|
682
|
+
return value;
|
|
683
|
+
}
|
|
684
|
+
function requireSnapshotCodec(registered, format) {
|
|
685
|
+
const normalized = snapshotFormat(format);
|
|
686
|
+
const registration = registered.get(normalized);
|
|
687
|
+
if (!registration) {
|
|
688
|
+
throw new TypeError("Mesh snapshot format is unsupported");
|
|
689
|
+
}
|
|
690
|
+
return registration;
|
|
691
|
+
}
|
|
692
|
+
async function snapshotEncoding(descriptor, state, options) {
|
|
693
|
+
const snapshot = cloneSnapshotState(state, options.protocolOptions);
|
|
694
|
+
return Object.freeze({
|
|
695
|
+
descriptor,
|
|
696
|
+
state: snapshot,
|
|
697
|
+
stateDigest: await computeMeshDurableValueDigest(snapshot, options),
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
function cloneSnapshotState(state, protocolOptions) {
|
|
701
|
+
const bytes = canonicalSnapshotBytes(state, protocolOptions);
|
|
702
|
+
return deepFreezeDurableValue(JSON.parse(new TextDecoder().decode(bytes)));
|
|
703
|
+
}
|
|
704
|
+
function canonicalSnapshotBytes(state, protocolOptions) {
|
|
705
|
+
const canonical = canonicalizeMeshJsonBytes(state, protocolOptions);
|
|
706
|
+
if (!canonical.ok) {
|
|
707
|
+
throw new TypeError("Mesh snapshot state must be bounded strict JSON");
|
|
708
|
+
}
|
|
709
|
+
return canonical.value;
|
|
710
|
+
}
|
|
711
|
+
function bytesEqual(left, right) {
|
|
712
|
+
if (left.byteLength !== right.byteLength)
|
|
713
|
+
return false;
|
|
714
|
+
for (let index = 0; index < left.byteLength; index += 1) {
|
|
715
|
+
if (left[index] !== right[index])
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
function deepFreezeDurableValue(value) {
|
|
721
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
722
|
+
Object.freeze(value);
|
|
723
|
+
for (const nested of Object.values(value))
|
|
724
|
+
deepFreezeDurableValue(nested);
|
|
725
|
+
}
|
|
726
|
+
return value;
|
|
727
|
+
}
|
|
728
|
+
function assertDigest(value, label) {
|
|
729
|
+
if (typeof value !== "string" || !/^sha256:[A-Za-z0-9_-]{43}$/u.test(value)) {
|
|
730
|
+
throw new TypeError(`Mesh durable ${label} is invalid`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function assertTimestamp(value, label) {
|
|
734
|
+
if (typeof value !== "string" ||
|
|
735
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u.test(value) ||
|
|
736
|
+
Number.isNaN(Date.parse(value))) {
|
|
737
|
+
throw new TypeError(`Mesh durable ${label} is invalid`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
function base64Url(bytes) {
|
|
741
|
+
let binary = "";
|
|
742
|
+
for (const byte of bytes)
|
|
743
|
+
binary += String.fromCharCode(byte);
|
|
744
|
+
return btoa(binary)
|
|
745
|
+
.replaceAll("+", "-")
|
|
746
|
+
.replaceAll("/", "_")
|
|
747
|
+
.replace(/=+$/u, "");
|
|
748
|
+
}
|
|
749
|
+
//# sourceMappingURL=durability.js.map
|