@agentplat/mesh-postgres 0.3.0-alpha.5

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.
@@ -0,0 +1,769 @@
1
+ import { MESH_DURABILITY_SCHEMA_VERSION, MESH_DURABLE_GENESIS_DIGEST, computeMeshDurableValueDigest, createMeshDurableJournalEntry, normalizeMeshDurableScope, } from "@agentplat/mesh/durability";
2
+ import { canonicalizeMeshJsonBytes, parseSignedMeshEnvelope, validateSignedMeshEnvelope, } from "@agentplat/mesh-protocol";
3
+ import { defaultPostgresSchema, normalizePostgresIdentifier, quotePostgresIdentifier, } from "@agentplat/postgres";
4
+ function scopedDatabase(database, schema) {
5
+ const prefix = `${quotePostgresIdentifier(schema)}.`;
6
+ return {
7
+ query(text, values) {
8
+ return database.query(text.replaceAll("public.", prefix), values);
9
+ },
10
+ };
11
+ }
12
+ /**
13
+ * PostgreSQL implementation of the Mesh durable repository.
14
+ *
15
+ * The pool is caller-owned and is never closed by this adapter.
16
+ */
17
+ export class PostgresMeshDurableRepository {
18
+ pool;
19
+ #schema;
20
+ #maximumPendingInboxRowsPerScope;
21
+ constructor(pool, options = {}) {
22
+ this.pool = pool;
23
+ if (!pool || typeof pool.connect !== "function") {
24
+ throw new TypeError("A PostgreSQL pool is required");
25
+ }
26
+ this.#schema = normalizePostgresIdentifier(options.schema ?? defaultPostgresSchema, "schema");
27
+ this.#maximumPendingInboxRowsPerScope = positiveInteger(options.maximumPendingInboxRowsPerScope ?? 100_000, "maximumPendingInboxRowsPerScope", 1_000_000);
28
+ }
29
+ async receive(input) {
30
+ const scope = normalizeMeshDurableScope(input.scope);
31
+ const envelope = validateInboundEnvelope(input.envelope, scope);
32
+ const envelopeDigest = await digestEnvelope(envelope);
33
+ return this.#transaction(async (database) => {
34
+ await database.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [scopeKey(scope)]);
35
+ const existing = await database.query(`SELECT envelope_digest, received_at
36
+ FROM public.mesh_inbox
37
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
38
+ AND instance_id = $4 AND message_id = $5`, [...scopeValues(scope), envelope.messageId]);
39
+ if (existing.rowCount === 1) {
40
+ if (String(existing.rows[0].envelope_digest) !== envelopeDigest) {
41
+ return Object.freeze({
42
+ accepted: false,
43
+ code: "message_conflict",
44
+ });
45
+ }
46
+ return Object.freeze({
47
+ accepted: true,
48
+ duplicate: true,
49
+ receivedAt: iso(existing.rows[0].received_at),
50
+ envelopeDigest,
51
+ });
52
+ }
53
+ const capacity = await database.query(`SELECT count(*)::bigint AS count
54
+ FROM public.mesh_inbox
55
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
56
+ AND instance_id = $4 AND status IN ('pending', 'processing')`, scopeValues(scope));
57
+ if (Number(capacity.rows[0].count) >= this.#maximumPendingInboxRowsPerScope) {
58
+ return Object.freeze({
59
+ accepted: false,
60
+ code: "capacity_exceeded",
61
+ });
62
+ }
63
+ const inserted = await database.query(`INSERT INTO public.mesh_inbox
64
+ (tenant_id, mesh_id, peer_id, instance_id, message_id,
65
+ envelope, envelope_digest)
66
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7)
67
+ RETURNING received_at`, [
68
+ ...scopeValues(scope),
69
+ envelope.messageId,
70
+ JSON.stringify(envelope),
71
+ envelopeDigest,
72
+ ]);
73
+ return Object.freeze({
74
+ accepted: true,
75
+ duplicate: false,
76
+ receivedAt: iso(inserted.rows[0].received_at),
77
+ envelopeDigest,
78
+ });
79
+ });
80
+ }
81
+ async loadSnapshot(inputScope) {
82
+ const scope = normalizeMeshDurableScope(inputScope);
83
+ const result = await scopedDatabase(this.pool, this.#schema).query(`SELECT * FROM public.mesh_peer_snapshots
84
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
85
+ AND instance_id = $4`, scopeValues(scope));
86
+ return result.rowCount === 0
87
+ ? undefined
88
+ : await mapSnapshot(result.rows[0], scope);
89
+ }
90
+ async claimInbox(input) {
91
+ const options = normalizeClaimOptions(input);
92
+ return this.#transaction(async (database) => {
93
+ const selected = await database.query(`SELECT message_id
94
+ FROM public.mesh_inbox
95
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
96
+ AND instance_id = $4
97
+ AND (
98
+ (status = 'pending' AND available_at <= transaction_timestamp())
99
+ OR
100
+ (status = 'processing' AND claim_expires_at <= transaction_timestamp())
101
+ )
102
+ ORDER BY received_at, message_id
103
+ FOR UPDATE SKIP LOCKED
104
+ LIMIT $5`, [...scopeValues(options.scope), options.limit]);
105
+ const claimed = [];
106
+ for (const selectedRow of selected.rows) {
107
+ const token = globalThis.crypto.randomUUID();
108
+ const updated = await database.query(`UPDATE public.mesh_inbox
109
+ SET status = 'processing',
110
+ attempts = attempts + 1,
111
+ claim_worker_id = $6,
112
+ claim_token = $7,
113
+ claim_generation = claim_generation + 1,
114
+ claim_expires_at = transaction_timestamp()
115
+ + ($8::bigint * interval '1 millisecond'),
116
+ settled_at = NULL,
117
+ reason_code = NULL
118
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
119
+ AND instance_id = $4 AND message_id = $5
120
+ RETURNING *`, [
121
+ ...scopeValues(options.scope),
122
+ String(selectedRow.message_id),
123
+ options.workerId,
124
+ token,
125
+ options.leaseDurationMs,
126
+ ]);
127
+ claimed.push(await mapInbox(updated.rows[0], options.scope));
128
+ }
129
+ return Object.freeze(claimed);
130
+ });
131
+ }
132
+ async commitInboxTransition(input) {
133
+ const scope = normalizeMeshDurableScope(input.inbox.scope);
134
+ assertCommitInput(input, scope);
135
+ return this.#transaction(async (database) => {
136
+ // A snapshot row does not exist at revision zero, so a row lock alone
137
+ // cannot serialize the first compare-and-swap transition.
138
+ await database.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [scopeKey(scope)]);
139
+ const inboxResult = await database.query(`SELECT *, claim_expires_at > transaction_timestamp() AS claim_live
140
+ FROM public.mesh_inbox
141
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
142
+ AND instance_id = $4 AND message_id = $5
143
+ FOR UPDATE`, [...scopeValues(scope), input.inbox.messageId]);
144
+ if (inboxResult.rowCount !== 1 ||
145
+ !claimMatches(inboxResult.rows[0], input.inbox.claim, "processing")) {
146
+ return commitConflict("claim_lost");
147
+ }
148
+ const transition = await database.query(`SELECT 1 FROM public.mesh_journal
149
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
150
+ AND instance_id = $4 AND transition_id = $5
151
+ LIMIT 1`, [...scopeValues(scope), input.transitionId]);
152
+ if (transition.rowCount !== 0) {
153
+ return commitConflict("transition_conflict");
154
+ }
155
+ const currentResult = await database.query(`SELECT * FROM public.mesh_peer_snapshots
156
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
157
+ AND instance_id = $4
158
+ FOR UPDATE`, scopeValues(scope));
159
+ const current = currentResult.rowCount === 0
160
+ ? undefined
161
+ : await mapSnapshot(currentResult.rows[0], scope);
162
+ if ((current?.revision ?? 0) !== input.expectedSnapshotRevision) {
163
+ return commitConflict("revision_conflict");
164
+ }
165
+ const timestamp = iso((await database.query("SELECT transaction_timestamp() AS occurred_at")).rows[0].occurred_at);
166
+ let snapshot = current;
167
+ if (input.outcome === "applied") {
168
+ const state = cloneStrictJson(input.nextState);
169
+ const stateDigest = await computeMeshDurableValueDigest(state);
170
+ const revision = input.expectedSnapshotRevision + 1;
171
+ const snapshotResult = await database.query(`INSERT INTO public.mesh_peer_snapshots
172
+ (tenant_id, mesh_id, peer_id, instance_id, revision, state,
173
+ state_digest, committed_at)
174
+ VALUES ($1, $2, $3, $4, $5, $6::jsonb, $7,
175
+ transaction_timestamp())
176
+ ON CONFLICT (tenant_id, mesh_id, peer_id, instance_id)
177
+ DO UPDATE SET revision = EXCLUDED.revision,
178
+ state = EXCLUDED.state,
179
+ state_digest = EXCLUDED.state_digest,
180
+ committed_at = EXCLUDED.committed_at
181
+ RETURNING *`, [...scopeValues(scope), revision, JSON.stringify(state), stateDigest]);
182
+ snapshot = await mapSnapshot(snapshotResult.rows[0], scope);
183
+ }
184
+ const snapshotRevision = snapshot?.revision ?? 0;
185
+ const snapshotDigest = snapshot?.stateDigest ?? (await computeMeshDurableValueDigest(null));
186
+ for (const outbound of input.outbox) {
187
+ const conflict = await database.query(`SELECT 1 FROM public.mesh_outbox
188
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
189
+ AND instance_id = $4
190
+ AND (effect_id = $5 OR message_id = $6)
191
+ LIMIT 1`, [
192
+ ...scopeValues(scope),
193
+ outbound.effectId,
194
+ outbound.envelope.messageId,
195
+ ]);
196
+ if (conflict.rowCount !== 0) {
197
+ throw new CommitConflictError("outbox_conflict");
198
+ }
199
+ }
200
+ const lastJournal = await database.query(`SELECT sequence, digest FROM public.mesh_journal
201
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
202
+ AND instance_id = $4
203
+ ORDER BY sequence DESC
204
+ LIMIT 1`, scopeValues(scope));
205
+ let sequence = lastJournal.rowCount === 0
206
+ ? 0
207
+ : safeInteger(lastJournal.rows[0].sequence, "journal sequence");
208
+ let previousDigest = lastJournal.rowCount === 0
209
+ ? MESH_DURABLE_GENESIS_DIGEST
210
+ : String(lastJournal.rows[0].digest);
211
+ const drafts = input.journal.length === 0
212
+ ? [
213
+ {
214
+ entryId: input.transitionId,
215
+ kind: input.outcome === "applied"
216
+ ? "transition.applied"
217
+ : "transition.rejected",
218
+ ...(input.reasonCode === undefined
219
+ ? {}
220
+ : { reasonCode: input.reasonCode }),
221
+ },
222
+ ]
223
+ : input.journal;
224
+ const journal = [];
225
+ for (const draft of drafts) {
226
+ sequence += 1;
227
+ const entry = await createMeshDurableJournalEntry({
228
+ scope,
229
+ sequence,
230
+ previousDigest,
231
+ transitionId: input.transitionId,
232
+ inboxMessageId: input.inbox.messageId,
233
+ snapshotRevision,
234
+ snapshotDigest,
235
+ draft,
236
+ occurredAt: timestamp,
237
+ });
238
+ await database.query(`INSERT INTO public.mesh_journal
239
+ (tenant_id, mesh_id, peer_id, instance_id, sequence, entry_id,
240
+ previous_digest, digest, transition_id, inbox_message_id,
241
+ snapshot_revision, snapshot_digest, kind, reason_code,
242
+ occurred_at)
243
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12,
244
+ $13, $14, $15::timestamptz)`, [
245
+ ...scopeValues(scope),
246
+ entry.sequence,
247
+ entry.entryId,
248
+ entry.previousDigest,
249
+ entry.digest,
250
+ entry.transitionId,
251
+ entry.inboxMessageId ?? null,
252
+ entry.snapshotRevision,
253
+ entry.snapshotDigest,
254
+ entry.kind,
255
+ entry.reasonCode ?? null,
256
+ entry.occurredAt,
257
+ ]);
258
+ journal.push(entry);
259
+ previousDigest = entry.digest;
260
+ }
261
+ const outbox = [];
262
+ for (const outbound of input.outbox) {
263
+ const targetPeerId = outbound.targetPeerId ??
264
+ (outbound.envelope.audience.kind === "peer"
265
+ ? outbound.envelope.audience.peerId
266
+ : undefined);
267
+ const envelope = validateOutboundEnvelope(outbound.envelope, scope, targetPeerId);
268
+ const envelopeDigest = await digestEnvelope(envelope);
269
+ const inserted = await database.query(`INSERT INTO public.mesh_outbox
270
+ (tenant_id, mesh_id, peer_id, instance_id, effect_id, message_id,
271
+ target_peer_id, envelope, envelope_digest)
272
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9)
273
+ RETURNING *`, [
274
+ ...scopeValues(scope),
275
+ outbound.effectId,
276
+ envelope.messageId,
277
+ targetPeerId ?? null,
278
+ JSON.stringify(envelope),
279
+ envelopeDigest,
280
+ ]);
281
+ outbox.push(await mapOutbox(inserted.rows[0], scope));
282
+ }
283
+ await database.query(`UPDATE public.mesh_inbox
284
+ SET status = $6,
285
+ settled_at = transaction_timestamp(),
286
+ reason_code = $7,
287
+ claim_worker_id = NULL,
288
+ claim_token = NULL,
289
+ claim_expires_at = NULL
290
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
291
+ AND instance_id = $4 AND message_id = $5`, [
292
+ ...scopeValues(scope),
293
+ input.inbox.messageId,
294
+ input.outcome,
295
+ input.reasonCode ?? null,
296
+ ]);
297
+ return Object.freeze({
298
+ committed: true,
299
+ ...(snapshot === undefined ? {} : { snapshot }),
300
+ journal: Object.freeze(journal),
301
+ outbox: Object.freeze(outbox),
302
+ });
303
+ }).catch((error) => {
304
+ if (error instanceof CommitConflictError) {
305
+ return commitConflict(error.code);
306
+ }
307
+ throw error;
308
+ });
309
+ }
310
+ async abandonInbox(input) {
311
+ const scope = normalizeMeshDurableScope(input.inbox.scope);
312
+ const claim = input.inbox.claim;
313
+ if (!claim)
314
+ return false;
315
+ const retryAfterMs = positiveInteger(input.retryAfterMs, "retryAfterMs", 3_600_000);
316
+ if (input.reasonCode !== undefined) {
317
+ boundedReason(input.reasonCode, "reasonCode");
318
+ }
319
+ const result = await scopedDatabase(this.pool, this.#schema).query(`UPDATE public.mesh_inbox
320
+ SET status = 'pending',
321
+ available_at = transaction_timestamp()
322
+ + ($9::bigint * interval '1 millisecond'),
323
+ reason_code = $10,
324
+ claim_worker_id = NULL,
325
+ claim_token = NULL,
326
+ claim_expires_at = NULL
327
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
328
+ AND instance_id = $4 AND message_id = $5
329
+ AND status = 'processing' AND claim_worker_id = $6
330
+ AND claim_token = $7 AND claim_generation = $8`, [
331
+ ...scopeValues(scope),
332
+ input.inbox.messageId,
333
+ claim.workerId,
334
+ claim.leaseToken,
335
+ claim.generation,
336
+ retryAfterMs,
337
+ input.reasonCode ?? null,
338
+ ]);
339
+ return result.rowCount === 1;
340
+ }
341
+ async claimOutbox(input) {
342
+ const options = normalizeClaimOptions(input);
343
+ return this.#transaction(async (database) => {
344
+ const selected = await database.query(`SELECT effect_id
345
+ FROM public.mesh_outbox
346
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
347
+ AND instance_id = $4
348
+ AND (
349
+ (status = 'pending' AND available_at <= transaction_timestamp())
350
+ OR
351
+ (status = 'delivering' AND claim_expires_at <= transaction_timestamp())
352
+ )
353
+ ORDER BY created_at, effect_id
354
+ FOR UPDATE SKIP LOCKED
355
+ LIMIT $5`, [...scopeValues(options.scope), options.limit]);
356
+ const claimed = [];
357
+ for (const selectedRow of selected.rows) {
358
+ const token = globalThis.crypto.randomUUID();
359
+ const updated = await database.query(`UPDATE public.mesh_outbox
360
+ SET status = 'delivering',
361
+ attempts = attempts + 1,
362
+ claim_worker_id = $6,
363
+ claim_token = $7,
364
+ claim_generation = claim_generation + 1,
365
+ claim_expires_at = transaction_timestamp()
366
+ + ($8::bigint * interval '1 millisecond'),
367
+ settled_at = NULL,
368
+ reason_code = NULL
369
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
370
+ AND instance_id = $4 AND effect_id = $5
371
+ RETURNING *`, [
372
+ ...scopeValues(options.scope),
373
+ String(selectedRow.effect_id),
374
+ options.workerId,
375
+ token,
376
+ options.leaseDurationMs,
377
+ ]);
378
+ claimed.push(await mapOutbox(updated.rows[0], options.scope));
379
+ }
380
+ return Object.freeze(claimed);
381
+ });
382
+ }
383
+ async settleOutbox(input) {
384
+ const scope = normalizeMeshDurableScope(input.outbox.scope);
385
+ const claim = input.outbox.claim;
386
+ if (!claim)
387
+ return false;
388
+ if (!input.settlement ||
389
+ (input.settlement.disposition !== "retryable" &&
390
+ input.settlement.disposition !== "delivered" &&
391
+ input.settlement.disposition !== "permanent_rejection")) {
392
+ throw new TypeError("Mesh PostgreSQL outbox settlement is invalid");
393
+ }
394
+ if (input.settlement.reasonCode !== undefined) {
395
+ boundedReason(input.settlement.reasonCode, "reasonCode");
396
+ }
397
+ const retry = input.settlement.disposition === "retryable";
398
+ const retryAfterMs = retry
399
+ ? positiveInteger(input.settlement.retryAfterMs, "retryAfterMs", 3_600_000)
400
+ : 0;
401
+ const status = retry
402
+ ? "pending"
403
+ : input.settlement.disposition === "delivered"
404
+ ? "delivered"
405
+ : "rejected";
406
+ const result = await scopedDatabase(this.pool, this.#schema).query(`UPDATE public.mesh_outbox
407
+ SET status = $9,
408
+ available_at = CASE WHEN $9 = 'pending'
409
+ THEN transaction_timestamp()
410
+ + ($10::bigint * interval '1 millisecond')
411
+ ELSE available_at END,
412
+ settled_at = CASE WHEN $9 = 'pending'
413
+ THEN NULL ELSE transaction_timestamp() END,
414
+ reason_code = $11,
415
+ claim_worker_id = NULL,
416
+ claim_token = NULL,
417
+ claim_expires_at = NULL
418
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
419
+ AND instance_id = $4 AND effect_id = $5
420
+ AND status = 'delivering' AND claim_worker_id = $6
421
+ AND claim_token = $7 AND claim_generation = $8
422
+ AND claim_expires_at > transaction_timestamp()`, [
423
+ ...scopeValues(scope),
424
+ input.outbox.effectId,
425
+ claim.workerId,
426
+ claim.leaseToken,
427
+ claim.generation,
428
+ status,
429
+ retryAfterMs,
430
+ input.settlement.reasonCode ?? null,
431
+ ]);
432
+ return result.rowCount === 1;
433
+ }
434
+ async inspectJournal(input) {
435
+ const scope = normalizeMeshDurableScope(input.scope);
436
+ const afterSequence = nonNegativeInteger(input.afterSequence ?? 0, "afterSequence");
437
+ const limit = positiveInteger(input.limit, "limit", 10_000);
438
+ const result = await scopedDatabase(this.pool, this.#schema).query(`SELECT * FROM public.mesh_journal
439
+ WHERE tenant_id = $1 AND mesh_id = $2 AND peer_id = $3
440
+ AND instance_id = $4 AND sequence > $5
441
+ ORDER BY sequence
442
+ LIMIT $6`, [...scopeValues(scope), afterSequence, limit]);
443
+ const entries = await Promise.all(result.rows.map((row) => mapJournal(row, scope)));
444
+ return Object.freeze(entries);
445
+ }
446
+ close() {
447
+ // The pool is caller-owned.
448
+ }
449
+ async #transaction(work) {
450
+ const client = await this.pool.connect();
451
+ const database = scopedDatabase(client, this.#schema);
452
+ try {
453
+ await client.query("BEGIN");
454
+ const result = await work(database);
455
+ await client.query("COMMIT");
456
+ return result;
457
+ }
458
+ catch (error) {
459
+ await client.query("ROLLBACK").catch(() => undefined);
460
+ throw error;
461
+ }
462
+ finally {
463
+ client.release();
464
+ }
465
+ }
466
+ }
467
+ class CommitConflictError extends Error {
468
+ code;
469
+ constructor(code) {
470
+ super(code);
471
+ this.code = code;
472
+ }
473
+ }
474
+ function commitConflict(code) {
475
+ return Object.freeze({ committed: false, code });
476
+ }
477
+ function validateInboundEnvelope(input, scope) {
478
+ const result = validateSignedMeshEnvelope(input);
479
+ if (!result.ok ||
480
+ result.value.tenantId !== scope.tenantId ||
481
+ result.value.meshId !== scope.meshId ||
482
+ (result.value.audience.kind === "peer" &&
483
+ result.value.audience.peerId !== scope.peerId)) {
484
+ throw new TypeError("Mesh durable inbound envelope scope is invalid");
485
+ }
486
+ return result.value;
487
+ }
488
+ function validateOutboundEnvelope(input, scope, targetPeerId) {
489
+ const result = validateSignedMeshEnvelope(input);
490
+ if (!result.ok ||
491
+ result.value.tenantId !== scope.tenantId ||
492
+ result.value.meshId !== scope.meshId ||
493
+ result.value.sender.peerId !== scope.peerId ||
494
+ result.value.sender.instanceId !== scope.instanceId ||
495
+ (result.value.audience.kind === "peer" &&
496
+ result.value.audience.peerId !== targetPeerId) ||
497
+ (result.value.audience.kind === "mesh" && !targetPeerId)) {
498
+ throw new TypeError("Mesh durable outbound envelope scope is invalid");
499
+ }
500
+ return result.value;
501
+ }
502
+ async function digestEnvelope(envelope) {
503
+ return computeMeshDurableValueDigest(envelope);
504
+ }
505
+ async function restoreEnvelope(input) {
506
+ const canonical = canonicalizeMeshJsonBytes(input);
507
+ if (!canonical.ok) {
508
+ throw new TypeError("Persisted Mesh envelope is invalid");
509
+ }
510
+ const parsed = parseSignedMeshEnvelope(canonical.value);
511
+ if (!parsed.ok)
512
+ throw new TypeError("Persisted Mesh envelope is invalid");
513
+ return parsed.value;
514
+ }
515
+ async function mapInbox(row, scope) {
516
+ const envelope = await restoreEnvelope(row.envelope);
517
+ const envelopeDigest = await digestEnvelope(envelope);
518
+ if (envelope.messageId !== String(row.message_id) ||
519
+ envelopeDigest !== String(row.envelope_digest)) {
520
+ throw new TypeError("Persisted Mesh inbox integrity check failed");
521
+ }
522
+ const status = String(row.status);
523
+ const claim = mapClaim(row, status === "processing");
524
+ return Object.freeze({
525
+ schemaVersion: MESH_DURABILITY_SCHEMA_VERSION,
526
+ scope,
527
+ messageId: envelope.messageId,
528
+ envelope,
529
+ envelopeDigest,
530
+ status,
531
+ attempts: safeInteger(row.attempts, "inbox attempts"),
532
+ receivedAt: iso(row.received_at),
533
+ availableAt: iso(row.available_at),
534
+ ...(claim ? { claim } : {}),
535
+ ...(row.settled_at == null ? {} : { settledAt: iso(row.settled_at) }),
536
+ ...(row.reason_code == null ? {} : { reasonCode: String(row.reason_code) }),
537
+ });
538
+ }
539
+ async function mapOutbox(row, scope) {
540
+ const envelope = await restoreEnvelope(row.envelope);
541
+ const envelopeDigest = await digestEnvelope(envelope);
542
+ if (envelope.messageId !== String(row.message_id) ||
543
+ envelopeDigest !== String(row.envelope_digest)) {
544
+ throw new TypeError("Persisted Mesh outbox integrity check failed");
545
+ }
546
+ const status = String(row.status);
547
+ const claim = mapClaim(row, status === "delivering");
548
+ return Object.freeze({
549
+ schemaVersion: MESH_DURABILITY_SCHEMA_VERSION,
550
+ scope,
551
+ effectId: String(row.effect_id),
552
+ messageId: envelope.messageId,
553
+ envelope,
554
+ envelopeDigest,
555
+ ...(row.target_peer_id == null
556
+ ? {}
557
+ : { targetPeerId: String(row.target_peer_id) }),
558
+ status,
559
+ attempts: safeInteger(row.attempts, "outbox attempts"),
560
+ availableAt: iso(row.available_at),
561
+ createdAt: iso(row.created_at),
562
+ ...(claim ? { claim } : {}),
563
+ ...(row.settled_at == null ? {} : { settledAt: iso(row.settled_at) }),
564
+ ...(row.reason_code == null ? {} : { reasonCode: String(row.reason_code) }),
565
+ });
566
+ }
567
+ async function mapSnapshot(row, scope) {
568
+ const state = cloneStrictJson(row.state);
569
+ const stateDigest = await computeMeshDurableValueDigest(state);
570
+ if (stateDigest !== String(row.state_digest)) {
571
+ throw new TypeError("Persisted Mesh snapshot integrity check failed");
572
+ }
573
+ return Object.freeze({
574
+ schemaVersion: MESH_DURABILITY_SCHEMA_VERSION,
575
+ scope,
576
+ revision: positiveInteger(safeInteger(row.revision, "snapshot revision"), "snapshot revision", Number.MAX_SAFE_INTEGER),
577
+ state,
578
+ stateDigest,
579
+ committedAt: iso(row.committed_at),
580
+ });
581
+ }
582
+ async function mapJournal(row, scope) {
583
+ const entry = await createMeshDurableJournalEntry({
584
+ scope,
585
+ sequence: safeInteger(row.sequence, "journal sequence"),
586
+ previousDigest: String(row.previous_digest),
587
+ transitionId: String(row.transition_id),
588
+ ...(row.inbox_message_id == null
589
+ ? {}
590
+ : { inboxMessageId: String(row.inbox_message_id) }),
591
+ snapshotRevision: safeInteger(row.snapshot_revision, "snapshot revision"),
592
+ snapshotDigest: String(row.snapshot_digest),
593
+ draft: {
594
+ entryId: String(row.entry_id),
595
+ kind: String(row.kind),
596
+ ...(row.reason_code == null
597
+ ? {}
598
+ : { reasonCode: String(row.reason_code) }),
599
+ },
600
+ occurredAt: iso(row.occurred_at),
601
+ });
602
+ if (entry.digest !== String(row.digest)) {
603
+ throw new TypeError("Persisted Mesh journal integrity check failed");
604
+ }
605
+ return entry;
606
+ }
607
+ function mapClaim(row, required) {
608
+ if (!required)
609
+ return undefined;
610
+ if (row.claim_worker_id == null ||
611
+ row.claim_token == null ||
612
+ row.claim_expires_at == null) {
613
+ throw new TypeError("Persisted Mesh claim is incomplete");
614
+ }
615
+ return Object.freeze({
616
+ workerId: String(row.claim_worker_id),
617
+ leaseToken: String(row.claim_token),
618
+ generation: positiveInteger(safeInteger(row.claim_generation, "claim generation"), "claim generation", Number.MAX_SAFE_INTEGER),
619
+ expiresAt: iso(row.claim_expires_at),
620
+ });
621
+ }
622
+ function claimMatches(row, claim, expectedStatus) {
623
+ return (claim !== undefined &&
624
+ row.status === expectedStatus &&
625
+ row.claim_live === true &&
626
+ row.claim_worker_id === claim.workerId &&
627
+ row.claim_token === claim.leaseToken &&
628
+ safeInteger(row.claim_generation, "claim generation") === claim.generation);
629
+ }
630
+ function assertCommitInput(input, scope) {
631
+ if (input.inbox.status !== "processing" ||
632
+ !input.inbox.claim ||
633
+ !scopeEquals(scope, input.inbox.scope) ||
634
+ !Number.isSafeInteger(input.expectedSnapshotRevision) ||
635
+ input.expectedSnapshotRevision < 0 ||
636
+ !validIdentifier(input.transitionId) ||
637
+ !Array.isArray(input.journal) ||
638
+ input.journal.length > 64 ||
639
+ !Array.isArray(input.outbox) ||
640
+ input.outbox.length > 256 ||
641
+ (input.outcome === "applied" && input.nextState === undefined) ||
642
+ (input.outcome === "rejected" &&
643
+ (input.nextState !== undefined ||
644
+ input.outbox.length !== 0 ||
645
+ input.reasonCode === undefined)) ||
646
+ (input.outcome === "applied" && input.reasonCode !== undefined) ||
647
+ (input.outcome !== "applied" && input.outcome !== "rejected")) {
648
+ throw new TypeError("Mesh durable commit input is invalid");
649
+ }
650
+ if (input.reasonCode !== undefined) {
651
+ boundedReason(input.reasonCode, "reasonCode");
652
+ }
653
+ for (const draft of input.journal) {
654
+ if (!draft ||
655
+ !validIdentifier(draft.entryId) ||
656
+ !validReason(draft.kind) ||
657
+ (draft.reasonCode !== undefined && !validReason(draft.reasonCode))) {
658
+ throw new TypeError("Mesh durable journal draft is invalid");
659
+ }
660
+ }
661
+ for (const outbound of input.outbox) {
662
+ if (!outbound ||
663
+ !validIdentifier(outbound.effectId) ||
664
+ (outbound.targetPeerId !== undefined &&
665
+ !validIdentifier(outbound.targetPeerId))) {
666
+ throw new TypeError("Mesh durable outbound draft is invalid");
667
+ }
668
+ validateOutboundEnvelope(outbound.envelope, scope, outbound.targetPeerId ??
669
+ (outbound.envelope?.audience?.kind === "peer"
670
+ ? outbound.envelope.audience.peerId
671
+ : undefined));
672
+ }
673
+ }
674
+ function normalizeClaimOptions(input) {
675
+ if (!input || typeof input !== "object") {
676
+ throw new TypeError("Mesh durable claim options are required");
677
+ }
678
+ if (typeof input.workerId !== "string" ||
679
+ !/^[A-Za-z0-9][A-Za-z0-9._:@-]*$/u.test(input.workerId) ||
680
+ input.workerId.length > 256) {
681
+ throw new TypeError("Mesh durable workerId is invalid");
682
+ }
683
+ return Object.freeze({
684
+ scope: normalizeMeshDurableScope(input.scope),
685
+ workerId: input.workerId,
686
+ limit: positiveInteger(input.limit, "limit", 256),
687
+ leaseDurationMs: positiveInteger(input.leaseDurationMs, "leaseDurationMs", 3_600_000),
688
+ });
689
+ }
690
+ function cloneStrictJson(value) {
691
+ const canonical = canonicalizeMeshJsonBytes(value);
692
+ if (!canonical.ok) {
693
+ throw new TypeError("Mesh durable snapshot must be bounded strict JSON");
694
+ }
695
+ const parsed = JSON.parse(new TextDecoder().decode(canonical.value));
696
+ return deepFreezeJson(parsed);
697
+ }
698
+ function deepFreezeJson(value) {
699
+ if (Array.isArray(value)) {
700
+ for (const item of value)
701
+ deepFreezeJson(item);
702
+ return Object.freeze(value);
703
+ }
704
+ if (value !== null && typeof value === "object") {
705
+ for (const item of Object.values(value))
706
+ deepFreezeJson(item);
707
+ return Object.freeze(value);
708
+ }
709
+ return value;
710
+ }
711
+ function scopeValues(scope) {
712
+ return [scope.tenantId, scope.meshId, scope.peerId, scope.instanceId];
713
+ }
714
+ function scopeKey(scope) {
715
+ return JSON.stringify(scopeValues(scope));
716
+ }
717
+ function scopeEquals(left, right) {
718
+ return (left.tenantId === right.tenantId &&
719
+ left.meshId === right.meshId &&
720
+ left.peerId === right.peerId &&
721
+ left.instanceId === right.instanceId);
722
+ }
723
+ function iso(value) {
724
+ if (value instanceof Date)
725
+ return value.toISOString();
726
+ const parsed = new Date(String(value));
727
+ if (Number.isNaN(parsed.getTime())) {
728
+ throw new TypeError("Persisted Mesh timestamp is invalid");
729
+ }
730
+ return parsed.toISOString();
731
+ }
732
+ function safeInteger(value, label) {
733
+ const number = Number(value);
734
+ if (!Number.isSafeInteger(number) || number < 0) {
735
+ throw new RangeError(`Persisted Mesh ${label} is invalid`);
736
+ }
737
+ return number;
738
+ }
739
+ function positiveInteger(value, label, maximum) {
740
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum) {
741
+ throw new RangeError(`Mesh PostgreSQL ${label} is outside its range`);
742
+ }
743
+ return value;
744
+ }
745
+ function nonNegativeInteger(value, label) {
746
+ if (!Number.isSafeInteger(value) || value < 0) {
747
+ throw new RangeError(`Mesh PostgreSQL ${label} is outside its range`);
748
+ }
749
+ return value;
750
+ }
751
+ function validIdentifier(value) {
752
+ return (typeof value === "string" &&
753
+ value.length > 0 &&
754
+ new TextEncoder().encode(value).byteLength <= 256 &&
755
+ /^[A-Za-z0-9][A-Za-z0-9._:@-]*$/u.test(value));
756
+ }
757
+ function validReason(value) {
758
+ return (typeof value === "string" &&
759
+ value.length > 0 &&
760
+ value.length <= 128 &&
761
+ /^[a-z0-9][a-z0-9._:-]*$/u.test(value));
762
+ }
763
+ function boundedReason(value, label) {
764
+ if (!validReason(value)) {
765
+ throw new TypeError(`Mesh PostgreSQL ${label} is invalid`);
766
+ }
767
+ return value;
768
+ }
769
+ //# sourceMappingURL=repository.js.map