@loomwork/event-runtime 0.0.0-stage → 0.1.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.
Files changed (153) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +95 -2
  4. package/dist/BranchRegistry.d.ts +140 -0
  5. package/dist/BranchRegistry.d.ts.map +1 -0
  6. package/dist/BranchRegistry.js +66 -0
  7. package/dist/BranchRegistry.js.map +1 -0
  8. package/dist/CoalescingEventStore.d.ts +97 -0
  9. package/dist/CoalescingEventStore.d.ts.map +1 -0
  10. package/dist/CoalescingEventStore.js +180 -0
  11. package/dist/CoalescingEventStore.js.map +1 -0
  12. package/dist/InMemoryArchiveStore.d.ts +19 -0
  13. package/dist/InMemoryArchiveStore.d.ts.map +1 -0
  14. package/dist/InMemoryArchiveStore.js +49 -0
  15. package/dist/InMemoryArchiveStore.js.map +1 -0
  16. package/dist/InMemoryAsyncEventStore.d.ts +111 -0
  17. package/dist/InMemoryAsyncEventStore.d.ts.map +1 -0
  18. package/dist/InMemoryAsyncEventStore.js +374 -0
  19. package/dist/InMemoryAsyncEventStore.js.map +1 -0
  20. package/dist/InMemoryBranchRegistry.d.ts +31 -0
  21. package/dist/InMemoryBranchRegistry.d.ts.map +1 -0
  22. package/dist/InMemoryBranchRegistry.js +167 -0
  23. package/dist/InMemoryBranchRegistry.js.map +1 -0
  24. package/dist/ProfilingEventStore.d.ts +75 -0
  25. package/dist/ProfilingEventStore.d.ts.map +1 -0
  26. package/dist/ProfilingEventStore.js +133 -0
  27. package/dist/ProfilingEventStore.js.map +1 -0
  28. package/dist/ShardPlan.d.ts +87 -0
  29. package/dist/ShardPlan.d.ts.map +1 -0
  30. package/dist/ShardPlan.js +193 -0
  31. package/dist/ShardPlan.js.map +1 -0
  32. package/dist/ShardedEventStore.d.ts +149 -0
  33. package/dist/ShardedEventStore.d.ts.map +1 -0
  34. package/dist/ShardedEventStore.js +468 -0
  35. package/dist/ShardedEventStore.js.map +1 -0
  36. package/dist/SubscriberRegistry.d.ts +59 -0
  37. package/dist/SubscriberRegistry.d.ts.map +1 -0
  38. package/dist/SubscriberRegistry.js +113 -0
  39. package/dist/SubscriberRegistry.js.map +1 -0
  40. package/dist/TenantScopedArchiveStore.d.ts +36 -0
  41. package/dist/TenantScopedArchiveStore.d.ts.map +1 -0
  42. package/dist/TenantScopedArchiveStore.js +96 -0
  43. package/dist/TenantScopedArchiveStore.js.map +1 -0
  44. package/dist/TenantScopedEventStore.d.ts +59 -0
  45. package/dist/TenantScopedEventStore.d.ts.map +1 -0
  46. package/dist/TenantScopedEventStore.js +227 -0
  47. package/dist/TenantScopedEventStore.js.map +1 -0
  48. package/dist/archive.d.ts +68 -0
  49. package/dist/archive.d.ts.map +1 -0
  50. package/dist/archive.js +207 -0
  51. package/dist/archive.js.map +1 -0
  52. package/dist/archiveSpi.d.ts +165 -0
  53. package/dist/archiveSpi.d.ts.map +1 -0
  54. package/dist/archiveSpi.js +184 -0
  55. package/dist/archiveSpi.js.map +1 -0
  56. package/dist/creationMarker.d.ts +13 -0
  57. package/dist/creationMarker.d.ts.map +1 -0
  58. package/dist/creationMarker.js +17 -0
  59. package/dist/creationMarker.js.map +1 -0
  60. package/dist/decoratorPlan.typecheck.d.ts +2 -0
  61. package/dist/decoratorPlan.typecheck.d.ts.map +1 -0
  62. package/dist/decoratorPlan.typecheck.js +41 -0
  63. package/dist/decoratorPlan.typecheck.js.map +1 -0
  64. package/dist/eventKindClass.d.ts +40 -0
  65. package/dist/eventKindClass.d.ts.map +1 -0
  66. package/dist/eventKindClass.js +99 -0
  67. package/dist/eventKindClass.js.map +1 -0
  68. package/dist/executionIdOrder.d.ts +21 -0
  69. package/dist/executionIdOrder.d.ts.map +1 -0
  70. package/dist/executionIdOrder.js +43 -0
  71. package/dist/executionIdOrder.js.map +1 -0
  72. package/dist/fencing.d.ts +21 -0
  73. package/dist/fencing.d.ts.map +1 -0
  74. package/dist/fencing.js +36 -0
  75. package/dist/fencing.js.map +1 -0
  76. package/dist/index.d.ts +39 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +126 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/installOptional.d.ts +52 -0
  81. package/dist/installOptional.d.ts.map +1 -0
  82. package/dist/installOptional.js +112 -0
  83. package/dist/installOptional.js.map +1 -0
  84. package/dist/keyUnavailable.d.ts +67 -0
  85. package/dist/keyUnavailable.d.ts.map +1 -0
  86. package/dist/keyUnavailable.js +87 -0
  87. package/dist/keyUnavailable.js.map +1 -0
  88. package/dist/leases.d.ts +177 -0
  89. package/dist/leases.d.ts.map +1 -0
  90. package/dist/leases.js +142 -0
  91. package/dist/leases.js.map +1 -0
  92. package/dist/payloadShredded.d.ts +30 -0
  93. package/dist/payloadShredded.d.ts.map +1 -0
  94. package/dist/payloadShredded.js +67 -0
  95. package/dist/payloadShredded.js.map +1 -0
  96. package/dist/scaleDecorators.bench.d.ts +2 -0
  97. package/dist/scaleDecorators.bench.d.ts.map +1 -0
  98. package/dist/scaleDecorators.bench.js +93 -0
  99. package/dist/scaleDecorators.bench.js.map +1 -0
  100. package/dist/sequenceRange.bench.d.ts +2 -0
  101. package/dist/sequenceRange.bench.d.ts.map +1 -0
  102. package/dist/sequenceRange.bench.js +46 -0
  103. package/dist/sequenceRange.bench.js.map +1 -0
  104. package/dist/sequenceRange.d.ts +10 -0
  105. package/dist/sequenceRange.d.ts.map +1 -0
  106. package/dist/sequenceRange.js +35 -0
  107. package/dist/sequenceRange.js.map +1 -0
  108. package/dist/timers.d.ts +87 -0
  109. package/dist/timers.d.ts.map +1 -0
  110. package/dist/timers.js +68 -0
  111. package/dist/timers.js.map +1 -0
  112. package/dist/types.d.ts +634 -0
  113. package/dist/types.d.ts.map +1 -0
  114. package/dist/types.js +429 -0
  115. package/dist/types.js.map +1 -0
  116. package/dist/writeConflicts.d.ts +46 -0
  117. package/dist/writeConflicts.d.ts.map +1 -0
  118. package/dist/writeConflicts.js +63 -0
  119. package/dist/writeConflicts.js.map +1 -0
  120. package/memory.d.ts +4 -0
  121. package/memory.js +13 -0
  122. package/package.json +67 -3
  123. package/sequence.d.ts +1 -0
  124. package/sequence.js +5 -0
  125. package/src/BranchRegistry.ts +161 -0
  126. package/src/CoalescingEventStore.ts +214 -0
  127. package/src/InMemoryArchiveStore.ts +69 -0
  128. package/src/InMemoryAsyncEventStore.ts +425 -0
  129. package/src/InMemoryBranchRegistry.ts +178 -0
  130. package/src/ProfilingEventStore.ts +153 -0
  131. package/src/ShardPlan.ts +235 -0
  132. package/src/ShardedEventStore.ts +557 -0
  133. package/src/SubscriberRegistry.ts +115 -0
  134. package/src/TenantScopedArchiveStore.ts +114 -0
  135. package/src/TenantScopedEventStore.ts +266 -0
  136. package/src/archive.ts +248 -0
  137. package/src/archiveSpi.ts +308 -0
  138. package/src/creationMarker.ts +15 -0
  139. package/src/decoratorPlan.typecheck.ts +52 -0
  140. package/src/eventKindClass.ts +127 -0
  141. package/src/executionIdOrder.ts +38 -0
  142. package/src/fencing.ts +31 -0
  143. package/src/index.ts +151 -0
  144. package/src/installOptional.ts +136 -0
  145. package/src/keyUnavailable.ts +98 -0
  146. package/src/leases.ts +319 -0
  147. package/src/payloadShredded.ts +63 -0
  148. package/src/scaleDecorators.bench.ts +107 -0
  149. package/src/sequenceRange.bench.ts +51 -0
  150. package/src/sequenceRange.ts +35 -0
  151. package/src/timers.ts +150 -0
  152. package/src/types.ts +857 -0
  153. package/src/writeConflicts.ts +56 -0
@@ -0,0 +1,68 @@
1
+ import { type EventEnvelope } from "@loomwork/core";
2
+ import { type ArchiveExecutionPage, type ArchiveSegment, type ArchiveSegmentKind, type ArchiveStore } from "./archiveSpi";
3
+ /**
4
+ * The digest of a run of envelopes: SHA-256 (hex) over their canonical JSON (sorted keys, so a
5
+ * backend that reorders object keys on a JSON round trip, such as Postgres `jsonb`, still agrees).
6
+ */
7
+ export declare function segmentDigest(envelopes: readonly EventEnvelope[]): string;
8
+ /** Build a segment from the envelopes a compaction removes. `envelopes` must be non-empty and ascending. */
9
+ export declare function buildSegment(envelopes: readonly EventEnvelope[], incarnation: string, from?: number, through?: number, kind?: ArchiveSegmentKind): ArchiveSegment;
10
+ /** `payload.sender` after the sender was erased (sealed under their subject and shredded, or redacted). */
11
+ export declare const SENDER_ERASED: Readonly<{
12
+ erased: true;
13
+ }>;
14
+ /**
15
+ * The erased-sender marker carrying a STABLE, NON-IDENTIFYING per-sender reference (ADR 0139 N3): the sealed
16
+ * sender's opaque handle, or a random one a plaintext redaction minted per erased subject. Two messages
17
+ * from one erased sender still share it and two different erased senders do not, so the owner's dedupe
18
+ * (which keyed on the identity before the erasure) keeps telling them apart; it names nobody.
19
+ */
20
+ export declare function senderErasedMarker(ref?: string): {
21
+ erased: true;
22
+ ref?: string;
23
+ };
24
+ /** Whether a stored `sender` value is the {@link SENDER_ERASED} marker. */
25
+ export declare function isSenderErased(v: unknown): boolean;
26
+ /**
27
+ * The envelope with an inbox message's `payload.sender` masked (`null`). The digest of a RESERVED
28
+ * stream's segment is taken over this form, so the one thing an erasure may change afterwards - the
29
+ * sender's identity, sealed under THEIR subject and shredded with it, or redacted in a plaintext archive
30
+ * (a recorded redaction, ADR 0139 D12) - does not turn into a digest mismatch on the audit read. Every
31
+ * other byte stays covered, and a live envelope is compared with its archived copy by
32
+ * {@link sameReservedEnvelope}, which still tells two DIFFERENT live senders apart.
33
+ */
34
+ export declare function maskSender(e: EventEnvelope): EventEnvelope;
35
+ /** {@link segmentDigest} over {@link maskSender}-masked envelopes (the digest of a reserved-stream segment). */
36
+ export declare function reservedSegmentDigest(envelopes: readonly EventEnvelope[]): string;
37
+ /**
38
+ * ADR 0139 N7 - the digest of the SENDERS of a run of reserved-stream envelopes (SHA-256 over the canonical
39
+ * JSON of each envelope's `payload.sender`, `null` for one without). A plaintext reserved segment carries it
40
+ * beside its (sender-masked) `digest`, so the one field an erasure may redact still has bit-rot detection;
41
+ * a redaction recomputes it in the same atomic replace.
42
+ */
43
+ export declare function senderDigestOf(envelopes: readonly EventEnvelope[]): string;
44
+ /** {@link buildSegment} with the reserved-stream digest. */
45
+ export declare function buildReservedSegment(envelopes: readonly EventEnvelope[], incarnation: string, from?: number, through?: number): ArchiveSegment;
46
+ /** {@link assertSegmentDigest} for a reserved-stream segment. */
47
+ export declare function assertReservedSegmentDigest(executionId: string, segment: ArchiveSegment): void;
48
+ /**
49
+ * Are `live` and `archived` the same inbox envelope? Equal masked digests, and the senders either equal
50
+ * or at least one of them erased (an erased sender is the same fact with its identity removed).
51
+ */
52
+ export declare function sameReservedEnvelope(live: EventEnvelope, archived: EventEnvelope): boolean;
53
+ /** One page of ids from an ascending list, keyset by exclusive cursor (shared by the backends' tests and memory store). */
54
+ export declare function pageIds(sorted: readonly string[], opts?: {
55
+ cursor?: string;
56
+ limit?: number;
57
+ }): ArchiveExecutionPage;
58
+ /** Throws {@link ArchiveDigestError} when `segment`'s envelopes do not hash to its digest. */
59
+ export declare function assertSegmentDigest(executionId: string, segment: ArchiveSegment): void;
60
+ /**
61
+ * The ids of the reserved streams of `chainId` the archive holds segments for (keyset prefix scans, ADR
62
+ * 0139): the chain's inbox (legacy, per-topic, registry) and quarantine streams, and the late runs split
63
+ * from them. The late keys all sit under one prefix, so their scan walks every late run of every chain
64
+ * (they exist only after a raced send or a node without an archive floor: a handful at most); the listing
65
+ * is paged by 200.
66
+ */
67
+ export declare function archivedReservedStreams(archive: Pick<ArchiveStore, "listExecutions">, chainId: string): Promise<string[]>;
68
+ //# sourceMappingURL=archive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EAElB,MAAM,cAAc,CAAC;AAyBtB;;;GAGG;AACH,wBAAgB,aAAa,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAOzE;AAED,4GAA4G;AAC5G,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,aAAa,EAAE,EACnC,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,MAA+B,EACrC,OAAO,GAAE,MAAkD,EAC3D,IAAI,CAAC,EAAE,kBAAkB,GACxB,cAAc,CAUhB;AAID,2GAA2G;AAC3G,eAAO,MAAM,aAAa;;EAA2C,CAAC;AAEtE;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAE/E;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAElD;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,aAAa,GAAG,aAAa,CAK1D;AAED,gHAAgH;AAChH,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAa1E;AAED,4DAA4D;AAC5D,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,SAAS,aAAa,EAAE,EACnC,WAAW,EAAE,MAAM,EACnB,IAAI,GAAE,MAA+B,EACrC,OAAO,GAAE,MAAkD,GAC1D,cAAc,CAMhB;AAED,iEAAiE;AACjE,wBAAgB,2BAA2B,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CA2B9F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,EAAE,aAAa,GAAG,OAAO,CAM1F;AAED,2HAA2H;AAC3H,wBAAgB,OAAO,CACrB,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7C,oBAAoB,CAUtB;AAED,8FAA8F;AAC9F,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAWtF;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,gBAAgB,CAAC,EAC7C,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,MAAM,EAAE,CAAC,CAwBnB"}
@@ -0,0 +1,207 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SENDER_ERASED = void 0;
4
+ exports.segmentDigest = segmentDigest;
5
+ exports.buildSegment = buildSegment;
6
+ exports.senderErasedMarker = senderErasedMarker;
7
+ exports.isSenderErased = isSenderErased;
8
+ exports.maskSender = maskSender;
9
+ exports.reservedSegmentDigest = reservedSegmentDigest;
10
+ exports.senderDigestOf = senderDigestOf;
11
+ exports.buildReservedSegment = buildReservedSegment;
12
+ exports.assertReservedSegmentDigest = assertReservedSegmentDigest;
13
+ exports.sameReservedEnvelope = sameReservedEnvelope;
14
+ exports.pageIds = pageIds;
15
+ exports.assertSegmentDigest = assertSegmentDigest;
16
+ exports.archivedReservedStreams = archivedReservedStreams;
17
+ const node_crypto_1 = require("node:crypto");
18
+ const core_1 = require("@loomwork/core");
19
+ const archiveSpi_1 = require("./archiveSpi");
20
+ const executionIdOrder_1 = require("./executionIdOrder");
21
+ const types_1 = require("./types");
22
+ /**
23
+ * The JSON form of the envelopes, which is what every backend stores and returns (a `Date` becomes
24
+ * its ISO string, `toJSON` runs, `undefined` members drop). Digesting the round-tripped value makes
25
+ * the digest the same on the write side and on a read that came back through any JSON store, and a
26
+ * value that cannot be JSON (circular, BigInt, a nesting JSON cannot hold) a typed refusal at
27
+ * archive time instead of a `TypeError` mid-compaction or a false digest mismatch on audit.
28
+ */
29
+ function jsonForm(envelopes) {
30
+ try {
31
+ return JSON.parse(JSON.stringify(envelopes));
32
+ }
33
+ catch (err) {
34
+ throw new archiveSpi_1.ArchiveUnserializableError(err);
35
+ }
36
+ }
37
+ /**
38
+ * The digest of a run of envelopes: SHA-256 (hex) over their canonical JSON (sorted keys, so a
39
+ * backend that reorders object keys on a JSON round trip, such as Postgres `jsonb`, still agrees).
40
+ */
41
+ function segmentDigest(envelopes) {
42
+ const form = jsonForm(envelopes);
43
+ try {
44
+ return (0, node_crypto_1.createHash)("sha256").update((0, core_1.canonicalJson)(form)).digest("hex");
45
+ }
46
+ catch (err) {
47
+ throw new archiveSpi_1.ArchiveUnserializableError(err);
48
+ }
49
+ }
50
+ /** Build a segment from the envelopes a compaction removes. `envelopes` must be non-empty and ascending. */
51
+ function buildSegment(envelopes, incarnation, from = envelopes[0].sequence, through = envelopes[envelopes.length - 1].sequence, kind) {
52
+ return {
53
+ from,
54
+ through,
55
+ count: envelopes.length,
56
+ digest: segmentDigest(envelopes),
57
+ incarnation,
58
+ ...(kind !== undefined && kind !== "events" && { kind }),
59
+ envelopes,
60
+ };
61
+ }
62
+ // ── ADR 0139 D12 - the sender field of an archived inbox envelope is the one erasable part ──────────
63
+ /** `payload.sender` after the sender was erased (sealed under their subject and shredded, or redacted). */
64
+ exports.SENDER_ERASED = Object.freeze({ erased: true });
65
+ /**
66
+ * The erased-sender marker carrying a STABLE, NON-IDENTIFYING per-sender reference (ADR 0139 N3): the sealed
67
+ * sender's opaque handle, or a random one a plaintext redaction minted per erased subject. Two messages
68
+ * from one erased sender still share it and two different erased senders do not, so the owner's dedupe
69
+ * (which keyed on the identity before the erasure) keeps telling them apart; it names nobody.
70
+ */
71
+ function senderErasedMarker(ref) {
72
+ return ref === undefined ? { erased: true } : { erased: true, ref };
73
+ }
74
+ /** Whether a stored `sender` value is the {@link SENDER_ERASED} marker. */
75
+ function isSenderErased(v) {
76
+ return typeof v === "object" && v !== null && v.erased === true;
77
+ }
78
+ /**
79
+ * The envelope with an inbox message's `payload.sender` masked (`null`). The digest of a RESERVED
80
+ * stream's segment is taken over this form, so the one thing an erasure may change afterwards - the
81
+ * sender's identity, sealed under THEIR subject and shredded with it, or redacted in a plaintext archive
82
+ * (a recorded redaction, ADR 0139 D12) - does not turn into a digest mismatch on the audit read. Every
83
+ * other byte stays covered, and a live envelope is compared with its archived copy by
84
+ * {@link sameReservedEnvelope}, which still tells two DIFFERENT live senders apart.
85
+ */
86
+ function maskSender(e) {
87
+ if (e.eventType !== "INBOX_MESSAGE")
88
+ return e;
89
+ const p = e.payload;
90
+ if (p === null || typeof p !== "object" || p.sender === undefined)
91
+ return e;
92
+ return { ...e, payload: { ...p, sender: null } };
93
+ }
94
+ /** {@link segmentDigest} over {@link maskSender}-masked envelopes (the digest of a reserved-stream segment). */
95
+ function reservedSegmentDigest(envelopes) {
96
+ return segmentDigest(envelopes.map(maskSender));
97
+ }
98
+ /**
99
+ * ADR 0139 N7 - the digest of the SENDERS of a run of reserved-stream envelopes (SHA-256 over the canonical
100
+ * JSON of each envelope's `payload.sender`, `null` for one without). A plaintext reserved segment carries it
101
+ * beside its (sender-masked) `digest`, so the one field an erasure may redact still has bit-rot detection;
102
+ * a redaction recomputes it in the same atomic replace.
103
+ */
104
+ function senderDigestOf(envelopes) {
105
+ const senders = envelopes.map((e) => e.eventType === "INBOX_MESSAGE"
106
+ ? (e.payload?.sender ?? null)
107
+ : null);
108
+ try {
109
+ return (0, node_crypto_1.createHash)("sha256")
110
+ .update((0, core_1.canonicalJson)(JSON.parse(JSON.stringify(senders))))
111
+ .digest("hex");
112
+ }
113
+ catch (err) {
114
+ throw new archiveSpi_1.ArchiveUnserializableError(err);
115
+ }
116
+ }
117
+ /** {@link buildSegment} with the reserved-stream digest. */
118
+ function buildReservedSegment(envelopes, incarnation, from = envelopes[0].sequence, through = envelopes[envelopes.length - 1].sequence) {
119
+ return {
120
+ ...buildSegment(envelopes, incarnation, from, through),
121
+ digest: reservedSegmentDigest(envelopes),
122
+ senderDigest: senderDigestOf(envelopes),
123
+ };
124
+ }
125
+ /** {@link assertSegmentDigest} for a reserved-stream segment. */
126
+ function assertReservedSegmentDigest(executionId, segment) {
127
+ // The sender digest is verified ONLY WHEN PRESENT: a segment written before ARCHIVE_STORE_SPI 1.1.0 (or by
128
+ // an archive that does not persist the field) has none, and still reads - its sender field simply has no
129
+ // bit-rot detection of its own (ADR 0139 D19; pre-1.1.0 compatibility path, not a downgrade to exploit:
130
+ // a writer of the archive could equally rewrite the segment).
131
+ const actual = reservedSegmentDigest(segment.envelopes);
132
+ if (segment.senderDigest !== undefined &&
133
+ senderDigestOf(segment.envelopes) !== segment.senderDigest) {
134
+ throw new archiveSpi_1.ArchiveDigestError(executionId, segment.from, segment.through, `sender:${segment.senderDigest}`, `sender:${senderDigestOf(segment.envelopes)}`);
135
+ }
136
+ if (actual !== segment.digest || segment.envelopes.length !== segment.count) {
137
+ throw new archiveSpi_1.ArchiveDigestError(executionId, segment.from, segment.through, segment.digest, actual);
138
+ }
139
+ }
140
+ /**
141
+ * Are `live` and `archived` the same inbox envelope? Equal masked digests, and the senders either equal
142
+ * or at least one of them erased (an erased sender is the same fact with its identity removed).
143
+ */
144
+ function sameReservedEnvelope(live, archived) {
145
+ if (segmentDigest([maskSender(live)]) !== segmentDigest([maskSender(archived)]))
146
+ return false;
147
+ const a = live.payload?.sender;
148
+ const b = archived.payload?.sender;
149
+ if (isSenderErased(a) || isSenderErased(b))
150
+ return true;
151
+ return segmentDigest([{ ...live, payload: a }]) === segmentDigest([{ ...archived, payload: b }]);
152
+ }
153
+ /** One page of ids from an ascending list, keyset by exclusive cursor (shared by the backends' tests and memory store). */
154
+ function pageIds(sorted, opts = {}) {
155
+ const limit = opts.limit ?? 500;
156
+ const start = opts.cursor === undefined
157
+ ? 0
158
+ : sorted.findIndex((id) => (0, executionIdOrder_1.compareExecutionIds)(id, opts.cursor) > 0);
159
+ if (start === -1)
160
+ return { executionIds: [] };
161
+ const page = sorted.slice(start, start + limit);
162
+ const more = start + limit < sorted.length;
163
+ return more ? { executionIds: page, nextCursor: page[page.length - 1] } : { executionIds: page };
164
+ }
165
+ /** Throws {@link ArchiveDigestError} when `segment`'s envelopes do not hash to its digest. */
166
+ function assertSegmentDigest(executionId, segment) {
167
+ const actual = segmentDigest(segment.envelopes);
168
+ if (actual !== segment.digest || segment.envelopes.length !== segment.count) {
169
+ throw new archiveSpi_1.ArchiveDigestError(executionId, segment.from, segment.through, segment.digest, actual);
170
+ }
171
+ }
172
+ /**
173
+ * The ids of the reserved streams of `chainId` the archive holds segments for (keyset prefix scans, ADR
174
+ * 0139): the chain's inbox (legacy, per-topic, registry) and quarantine streams, and the late runs split
175
+ * from them. The late keys all sit under one prefix, so their scan walks every late run of every chain
176
+ * (they exist only after a raced send or a node without an archive floor: a handful at most); the listing
177
+ * is paged by 200.
178
+ */
179
+ async function archivedReservedStreams(archive, chainId) {
180
+ const found = [];
181
+ const own = [`${types_1.INBOX_STREAM_PREFIX}${chainId}`, `${types_1.QUARANTINE_STREAM_PREFIX}${chainId}`];
182
+ const prefixes = [...own, ...own.map((p) => `${types_1.RESERVED_LATE_PREFIX}${p}`)];
183
+ for (const prefix of prefixes) {
184
+ // The cursor is EXCLUSIVE and the stream whose id IS the prefix must be listed, so start one
185
+ // character short of it and skip what sorts before the prefix.
186
+ let cursor = prefix.slice(0, -1);
187
+ for (;;) {
188
+ const page = await archive.listExecutions({ cursor, limit: 200 });
189
+ let past = false;
190
+ for (const id of page.executionIds) {
191
+ if (!id.startsWith(prefix)) {
192
+ if ((0, executionIdOrder_1.compareExecutionIds)(id, prefix) < 0)
193
+ continue;
194
+ past = true;
195
+ break;
196
+ }
197
+ if ((0, types_1.isReservedStream)(id) && (0, types_1.archiveChainOf)(id) === chainId)
198
+ found.push(id);
199
+ }
200
+ if (past || page.nextCursor === undefined || page.nextCursor === cursor)
201
+ break;
202
+ cursor = page.nextCursor;
203
+ }
204
+ }
205
+ return found;
206
+ }
207
+ //# sourceMappingURL=archive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive.js","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,165 @@
1
+ import type { EventEnvelope } from "@loomwork/core";
2
+ /**
3
+ * SPI version of {@link ArchiveStore} (ADR 0138). Pinned by `runArchiveStoreConformance`.
4
+ *
5
+ * 1.0.0: `append` / `headers` / `read` / `listExecutions` (paged) / optional `deleteExecution` /
6
+ * `close`; every segment header carries the log `incarnation` it belongs to and an optional `kind`.
7
+ *
8
+ * 1.1.0 (ADR 0139 N1/N7, additive): the optional `replaceSegments(key, expected, next)`, an ATOMIC
9
+ * compare-and-swap of a key's whole segment list (a concurrent `append` makes it fail with
10
+ * {@link ArchiveConflictError}; nothing is written), and the optional header field `senderDigest`
11
+ * (a digest of the senders of a PLAINTEXT reserved-stream segment, which the digest itself masks). A
12
+ * 1.0.0 store conforms; the erasure that needs `replaceSegments` refuses an archive without it.
13
+ */
14
+ export declare const ARCHIVE_STORE_SPI_VERSION = "1.1.0";
15
+ /**
16
+ * What a segment holds. Absent / `"events"`: the exact envelopes a compaction removed. `"legacy-checkpoint"`:
17
+ * ONE synthetic envelope carrying a checkpoint (snapshot) written before the archive existed, byte-exact:
18
+ * its folded state is the only copy of the settled steps' inputs the pre-0138 compactions removed.
19
+ */
20
+ export type ArchiveSegmentKind = "events" | "legacy-checkpoint";
21
+ /** The `eventType` of the synthetic envelope inside a `"legacy-checkpoint"` segment. */
22
+ export declare const ARCHIVE_LEGACY_CHECKPOINT_EVENT = "ARCHIVE_LEGACY_CHECKPOINT";
23
+ /** What a segment says about itself without its envelopes. */
24
+ export interface ArchiveSegmentHeader {
25
+ /** The lowest sequence this segment covers (inclusive). */
26
+ readonly from: number;
27
+ /** The highest sequence it covers (inclusive): the compaction checkpoint's sequence. */
28
+ readonly through: number;
29
+ /** How many envelopes it holds (sequences inside `[from, through]` a compaction kept are absent). */
30
+ readonly count: number;
31
+ /** {@link segmentDigest} of the envelopes, in order. Stored and returned verbatim. */
32
+ readonly digest: string;
33
+ /**
34
+ * The incarnation of the log this segment came from: the {@link segmentDigest} of its bootstrap
35
+ * event (sequence 0, which survives every compaction; the id alone is `<executionId>-0` and says
36
+ * nothing). A segment of another incarnation under the same execution id (an id REUSED after a
37
+ * delete) is refused with {@link ArchiveIncarnationError}, never inherited. Two runs whose
38
+ * bootstrap envelopes are byte-identical (same input, same millisecond) are not told apart here;
39
+ * their later events then disagree, which the compaction and the audit read catch byte-for-byte.
40
+ */
41
+ readonly incarnation: string;
42
+ /** Absent means `"events"`. Stored and returned verbatim. */
43
+ readonly kind?: ArchiveSegmentKind;
44
+ /**
45
+ * SPI 1.1.0 (ADR 0139 N7), PLAINTEXT reserved-stream segments only: the sender digest of the
46
+ * envelopes' senders. The segment `digest` masks `payload.sender` (an erasure may redact it), so
47
+ * without this the sender field would have no bit-rot detection; a redaction recomputes it in the same
48
+ * atomic {@link ArchiveStore.replaceSegments}. Stored and returned verbatim; absent on every other segment
49
+ * (a sealing archive authenticates the sender with AEAD instead and never stores it).
50
+ */
51
+ readonly senderDigest?: string;
52
+ }
53
+ /** One contiguous range of removed envelopes. */
54
+ export interface ArchiveSegment extends ArchiveSegmentHeader {
55
+ /** The exact envelopes the log held, ascending by sequence. */
56
+ readonly envelopes: readonly EventEnvelope[];
57
+ /**
58
+ * Opaque sealed form. A sealing decorator (`EncryptingArchiveStore`) stores its ciphertext here
59
+ * and an EMPTY `envelopes`; a plain backend persists it verbatim and never looks inside.
60
+ */
61
+ readonly sealed?: unknown;
62
+ }
63
+ /**
64
+ * Where compaction puts the events it removes (ADR 0138). Append-only: there is no per-segment
65
+ * delete, only {@link ArchiveStore.deleteExecution} for the existing whole-execution retention path.
66
+ *
67
+ * Contract (pinned by the conformance kit):
68
+ * - `append` is IDEMPOTENT for an identical segment (same `from`, `through`, `digest`): a crash
69
+ * between "archive" and "truncate" is retried by re-compaction, and the retry must not fail.
70
+ * - Any other segment must start strictly after the last one's `through`; an overlap or a rewind
71
+ * rejects with {@link ArchiveConflictError} and writes NOTHING.
72
+ * - `headers` and `read` return segments in ascending `from`. `read` returns every segment that
73
+ * overlaps the range, WHOLE (the caller trims), exactly what was appended.
74
+ * - An unknown execution reads as `[]`.
75
+ */
76
+ export interface ArchiveStore {
77
+ append(executionId: string, segment: ArchiveSegment): Promise<void>;
78
+ /** The segment headers, ascending. Cheap: no envelopes are read. */
79
+ headers(executionId: string): Promise<ArchiveSegmentHeader[]>;
80
+ /**
81
+ * One page of the execution ids that hold at least one segment, ascending in code point order,
82
+ * keyset-paged like `EventStore.listExecutionsPage`: pass the previous page's `nextCursor`
83
+ * (exclusive); no `nextCursor` means the end. Lets a sweep enumerate what the archive cites.
84
+ */
85
+ listExecutions(opts?: {
86
+ cursor?: string;
87
+ limit?: number;
88
+ }): Promise<ArchiveExecutionPage>;
89
+ read(executionId: string, range?: {
90
+ readonly from?: number;
91
+ readonly through?: number;
92
+ }): Promise<ArchiveSegment[]>;
93
+ /** OPTIONAL. The whole-execution delete of the retention path (`LogLifecycle.applyRetention`). */
94
+ deleteExecution?(executionId: string): Promise<void>;
95
+ /**
96
+ * OPTIONAL (SPI 1.1.0, ADR 0139 N1). Atomically replace ALL of `executionId`'s segments with `next`, IF
97
+ * its current headers are exactly `expected` (ascending, as {@link ArchiveStore.headers} returned
98
+ * them, `senderDigest` included): one compare-and-swap, so a concurrent writer (a trim on another node
99
+ * appending a segment after the caller read) makes it reject with {@link ArchiveConflictError} and
100
+ * write NOTHING, and the caller re-reads and retries. Used by erasure to redact a sender in place; the
101
+ * `next` segments must be ascending and non-overlapping, and of the incarnation `expected` holds.
102
+ */
103
+ replaceSegments?(executionId: string, expected: readonly ArchiveSegmentHeader[], next: readonly ArchiveSegment[]): Promise<void>;
104
+ close(): Promise<void>;
105
+ }
106
+ /** One page of {@link ArchiveStore.listExecutions}. */
107
+ export interface ArchiveExecutionPage {
108
+ readonly executionIds: string[];
109
+ readonly nextCursor?: string;
110
+ }
111
+ /** The names of the OPTIONAL {@link ArchiveStore} methods, for the decorator plan. */
112
+ export declare const OPTIONAL_ARCHIVE_STORE_METHODS: readonly ["deleteExecution", "replaceSegments"];
113
+ /** A segment that overlaps or rewinds past what the archive already holds (nothing was written). */
114
+ export declare class ArchiveConflictError extends Error {
115
+ readonly executionId: string;
116
+ readonly attempted: ArchiveSegmentHeader;
117
+ readonly lastThrough: number;
118
+ /** Set by {@link ArchiveStore.replaceSegments}: why the compare-and-swap failed. */
119
+ readonly replaceReason?: string | undefined;
120
+ readonly retryable = false;
121
+ constructor(executionId: string, attempted: ArchiveSegmentHeader, lastThrough: number,
122
+ /** Set by {@link ArchiveStore.replaceSegments}: why the compare-and-swap failed. */
123
+ replaceReason?: string | undefined);
124
+ }
125
+ /**
126
+ * The archive holds segments of another incarnation of this execution id (the id was deleted and
127
+ * REUSED): appending to or reading them as this run's history would mix two runs. Nothing was
128
+ * written. Never retryable.
129
+ */
130
+ export declare class ArchiveIncarnationError extends Error {
131
+ readonly executionId: string;
132
+ readonly archived: string;
133
+ readonly current: string;
134
+ readonly retryable = false;
135
+ constructor(executionId: string, archived: string, current: string);
136
+ }
137
+ /** A segment's envelopes cannot be serialized to JSON (circular, BigInt, too deep): it cannot be archived or verified. */
138
+ export declare class ArchiveUnserializableError extends Error {
139
+ readonly retryable = false;
140
+ constructor(cause: unknown);
141
+ }
142
+ /** The envelopes of a segment do not hash to the digest it recorded: the archive was altered. */
143
+ export declare class ArchiveDigestError extends Error {
144
+ readonly executionId: string;
145
+ readonly from: number;
146
+ readonly through: number;
147
+ readonly expected: string;
148
+ readonly actual: string;
149
+ readonly retryable = false;
150
+ constructor(executionId: string, from: number, through: number, expected: string, actual: string);
151
+ }
152
+ export declare const headerOf: (s: ArchiveSegmentHeader) => ArchiveSegmentHeader;
153
+ /**
154
+ * The shared `replaceSegments` rule for a backend, given the headers stored now: throws
155
+ * {@link ArchiveConflictError} unless they are exactly `expected`, and a `RangeError` for a `next` list
156
+ * that is not ascending, non-overlapping and well-formed. Nothing is written by this check.
157
+ */
158
+ export declare function assertReplaceable(executionId: string, stored: readonly ArchiveSegmentHeader[], expected: readonly ArchiveSegmentHeader[], next: readonly ArchiveSegmentHeader[]): void;
159
+ /**
160
+ * The shared `append` rule for a backend, given the headers already stored (ascending): `"noop"`
161
+ * for an exact repeat of a stored segment, `"append"` when the segment starts after the last one,
162
+ * else throws {@link ArchiveConflictError}.
163
+ */
164
+ export declare function classifyAppend(executionId: string, stored: readonly ArchiveSegmentHeader[], segment: ArchiveSegmentHeader): "append" | "noop";
165
+ //# sourceMappingURL=archiveSpi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archiveSpi.d.ts","sourceRoot":"","sources":["../src/archiveSpi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAYpD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,UAAU,CAAC;AAEjD;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,mBAAmB,CAAC;AAEhE,wFAAwF;AACxF,eAAO,MAAM,+BAA+B,8BAA8B,CAAC;AAE3E,8DAA8D;AAC9D,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wFAAwF;IACxF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,qGAAqG;IACrG,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,IAAI,CAAC,EAAE,kBAAkB,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,iDAAiD;AACjD,MAAM,WAAW,cAAe,SAAQ,oBAAoB;IAC1D,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,oEAAoE;IACpE,OAAO,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAC9D;;;;OAIG;IACH,cAAc,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAC1F,IAAI,CACF,WAAW,EAAE,MAAM,EACnB,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAC5D,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;IAC7B,kGAAkG;IAClG,eAAe,CAAC,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrD;;;;;;;OAOG;IACH,eAAe,CAAC,CACd,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,IAAI,EAAE,SAAS,cAAc,EAAE,GAC9B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,uDAAuD;AACvD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,sFAAsF;AACtF,eAAO,MAAM,8BAA8B,iDAAkD,CAAC;AAE9F,oGAAoG;AACpG,qBAAa,oBAAqB,SAAQ,KAAK;IAG3C,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,SAAS,EAAE,oBAAoB;IACxC,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM;IANjC,QAAQ,CAAC,SAAS,SAAS;gBAEhB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,oBAAoB,EAC/B,WAAW,EAAE,MAAM;IAC5B,oFAAoF;IAC3E,aAAa,CAAC,EAAE,MAAM,YAAA;CAUlC;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAG9C,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM;IAJ1B,QAAQ,CAAC,SAAS,SAAS;gBAEhB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM;CAQ3B;AAED,0HAA0H;AAC1H,qBAAa,0BAA2B,SAAQ,KAAK;IACnD,QAAQ,CAAC,SAAS,SAAS;gBACf,KAAK,EAAE,OAAO;CAQ3B;AAED,iGAAiG;AACjG,qBAAa,kBAAmB,SAAQ,KAAK;IAGzC,QAAQ,CAAC,WAAW,EAAE,MAAM;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;IANzB,QAAQ,CAAC,SAAS,SAAS;gBAEhB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAQ1B;AAED,eAAO,MAAM,QAAQ,GAAI,GAAG,oBAAoB,KAAG,oBAQjD,CAAC;AAWH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,oBAAoB,EAAE,EACvC,QAAQ,EAAE,SAAS,oBAAoB,EAAE,EACzC,IAAI,EAAE,SAAS,oBAAoB,EAAE,GACpC,IAAI,CA0CN;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,oBAAoB,EAAE,EACvC,OAAO,EAAE,oBAAoB,GAC5B,QAAQ,GAAG,MAAM,CAsBnB"}
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.headerOf = exports.ArchiveDigestError = exports.ArchiveUnserializableError = exports.ArchiveIncarnationError = exports.ArchiveConflictError = exports.OPTIONAL_ARCHIVE_STORE_METHODS = exports.ARCHIVE_LEGACY_CHECKPOINT_EVENT = exports.ARCHIVE_STORE_SPI_VERSION = void 0;
4
+ exports.assertReplaceable = assertReplaceable;
5
+ exports.classifyAppend = classifyAppend;
6
+ // ── ADR 0138 — the compaction archive SPI ────────────────────────────────────────────────────────
7
+ //
8
+ // `LogLifecycle.compact` removes the prefix of a log below its checkpoint. Under ADR 0138 it MOVES
9
+ // that prefix: the removed envelopes are appended here first, then truncated. Nothing in loom
10
+ // deletes a fact the log held, so an auditor can recover `archive ∪ surviving log == original log`.
11
+ //
12
+ // The archive is deliberately NOT an EventStore and NOT the content store: `readRange` of the log
13
+ // never sees it (replay and resume cannot read it), no sweep enumerates it, and per-subject erasure
14
+ // is a property of ONE store (a sealing wrapper over the same KeyStore as the event store).
15
+ /**
16
+ * SPI version of {@link ArchiveStore} (ADR 0138). Pinned by `runArchiveStoreConformance`.
17
+ *
18
+ * 1.0.0: `append` / `headers` / `read` / `listExecutions` (paged) / optional `deleteExecution` /
19
+ * `close`; every segment header carries the log `incarnation` it belongs to and an optional `kind`.
20
+ *
21
+ * 1.1.0 (ADR 0139 N1/N7, additive): the optional `replaceSegments(key, expected, next)`, an ATOMIC
22
+ * compare-and-swap of a key's whole segment list (a concurrent `append` makes it fail with
23
+ * {@link ArchiveConflictError}; nothing is written), and the optional header field `senderDigest`
24
+ * (a digest of the senders of a PLAINTEXT reserved-stream segment, which the digest itself masks). A
25
+ * 1.0.0 store conforms; the erasure that needs `replaceSegments` refuses an archive without it.
26
+ */
27
+ exports.ARCHIVE_STORE_SPI_VERSION = "1.1.0";
28
+ /** The `eventType` of the synthetic envelope inside a `"legacy-checkpoint"` segment. */
29
+ exports.ARCHIVE_LEGACY_CHECKPOINT_EVENT = "ARCHIVE_LEGACY_CHECKPOINT";
30
+ /** The names of the OPTIONAL {@link ArchiveStore} methods, for the decorator plan. */
31
+ exports.OPTIONAL_ARCHIVE_STORE_METHODS = ["deleteExecution", "replaceSegments"];
32
+ /** A segment that overlaps or rewinds past what the archive already holds (nothing was written). */
33
+ class ArchiveConflictError extends Error {
34
+ executionId;
35
+ attempted;
36
+ lastThrough;
37
+ replaceReason;
38
+ retryable = false;
39
+ constructor(executionId, attempted, lastThrough,
40
+ /** Set by {@link ArchiveStore.replaceSegments}: why the compare-and-swap failed. */
41
+ replaceReason) {
42
+ super(replaceReason !== undefined
43
+ ? `archive "${executionId}": replaceSegments refused - ${replaceReason}. Nothing was written; re-read the headers and retry.`
44
+ : `archive "${executionId}": segment [${attempted.from}..${attempted.through}] does not start after the archive's last sequence ${lastThrough}, and is not an exact repeat of an archived segment. Segments are append-only and contiguous; nothing was written.`);
45
+ this.executionId = executionId;
46
+ this.attempted = attempted;
47
+ this.lastThrough = lastThrough;
48
+ this.replaceReason = replaceReason;
49
+ this.name = "ArchiveConflictError";
50
+ Object.setPrototypeOf(this, ArchiveConflictError.prototype);
51
+ }
52
+ }
53
+ exports.ArchiveConflictError = ArchiveConflictError;
54
+ /**
55
+ * The archive holds segments of another incarnation of this execution id (the id was deleted and
56
+ * REUSED): appending to or reading them as this run's history would mix two runs. Nothing was
57
+ * written. Never retryable.
58
+ */
59
+ class ArchiveIncarnationError extends Error {
60
+ executionId;
61
+ archived;
62
+ current;
63
+ retryable = false;
64
+ constructor(executionId, archived, current) {
65
+ super(`archive "${executionId}": the archive holds segments of a DIFFERENT run (incarnation ${archived}) than the log's (${current}) — the execution id was reused after its earlier run was deleted. The old run's archive was not removed with it, so it is not this run's history and is neither read as such nor extended. Remove the previous run's segments with the retention path (ArchiveStore.deleteExecution), or use a fresh execution id.`);
66
+ this.executionId = executionId;
67
+ this.archived = archived;
68
+ this.current = current;
69
+ this.name = "ArchiveIncarnationError";
70
+ Object.setPrototypeOf(this, ArchiveIncarnationError.prototype);
71
+ }
72
+ }
73
+ exports.ArchiveIncarnationError = ArchiveIncarnationError;
74
+ /** A segment's envelopes cannot be serialized to JSON (circular, BigInt, too deep): it cannot be archived or verified. */
75
+ class ArchiveUnserializableError extends Error {
76
+ retryable = false;
77
+ constructor(cause) {
78
+ super(`archive: the envelopes cannot be serialized to JSON (${cause instanceof Error ? cause.message : String(cause)}) — an archived event must be plain JSON data, byte-exact.`, { cause });
79
+ this.name = "ArchiveUnserializableError";
80
+ Object.setPrototypeOf(this, ArchiveUnserializableError.prototype);
81
+ }
82
+ }
83
+ exports.ArchiveUnserializableError = ArchiveUnserializableError;
84
+ /** The envelopes of a segment do not hash to the digest it recorded: the archive was altered. */
85
+ class ArchiveDigestError extends Error {
86
+ executionId;
87
+ from;
88
+ through;
89
+ expected;
90
+ actual;
91
+ retryable = false;
92
+ constructor(executionId, from, through, expected, actual) {
93
+ super(`archive "${executionId}": segment [${from}..${through}] does not match its recorded digest (expected ${expected}, computed ${actual}) — the archived events were altered or corrupted.`);
94
+ this.executionId = executionId;
95
+ this.from = from;
96
+ this.through = through;
97
+ this.expected = expected;
98
+ this.actual = actual;
99
+ this.name = "ArchiveDigestError";
100
+ Object.setPrototypeOf(this, ArchiveDigestError.prototype);
101
+ }
102
+ }
103
+ exports.ArchiveDigestError = ArchiveDigestError;
104
+ const headerOf = (s) => ({
105
+ from: s.from,
106
+ through: s.through,
107
+ count: s.count,
108
+ digest: s.digest,
109
+ incarnation: s.incarnation,
110
+ ...(s.kind !== undefined && { kind: s.kind }),
111
+ ...(s.senderDigest !== undefined && { senderDigest: s.senderDigest }),
112
+ });
113
+ exports.headerOf = headerOf;
114
+ const sameHeader = (a, b) => a.from === b.from &&
115
+ a.through === b.through &&
116
+ a.count === b.count &&
117
+ a.digest === b.digest &&
118
+ a.incarnation === b.incarnation &&
119
+ (a.kind ?? "events") === (b.kind ?? "events") &&
120
+ (a.senderDigest ?? null) === (b.senderDigest ?? null);
121
+ /**
122
+ * The shared `replaceSegments` rule for a backend, given the headers stored now: throws
123
+ * {@link ArchiveConflictError} unless they are exactly `expected`, and a `RangeError` for a `next` list
124
+ * that is not ascending, non-overlapping and well-formed. Nothing is written by this check.
125
+ */
126
+ function assertReplaceable(executionId, stored, expected, next) {
127
+ const attempted = next[0] ?? expected[0] ?? stored[0] ?? { from: 0, through: 0 };
128
+ const last = stored[stored.length - 1]?.through ?? -1;
129
+ if (stored.length !== expected.length) {
130
+ throw new ArchiveConflictError(executionId, attempted, last, `the key now holds ${stored.length} segment(s), the caller read ${expected.length}`);
131
+ }
132
+ for (let i = 0; i < stored.length; i++) {
133
+ if (!sameHeader(stored[i], expected[i])) {
134
+ throw new ArchiveConflictError(executionId, attempted, last, `segment ${i} [${stored[i].from}..${stored[i].through}] changed since the caller read it`);
135
+ }
136
+ }
137
+ let prev = Number.NEGATIVE_INFINITY;
138
+ for (const h of next) {
139
+ if (!Number.isSafeInteger(h.from) || !Number.isSafeInteger(h.through) || h.from > h.through) {
140
+ throw new RangeError(`archive segment [${h.from}..${h.through}] is malformed`);
141
+ }
142
+ if (h.from <= prev) {
143
+ throw new RangeError(`replaceSegments: segments must be ascending and non-overlapping ([${h.from}..${h.through}] follows ${prev})`);
144
+ }
145
+ prev = h.through;
146
+ }
147
+ const incarnation = expected[0]?.incarnation;
148
+ if (incarnation !== undefined) {
149
+ for (const h of next) {
150
+ if (h.incarnation !== incarnation) {
151
+ throw new ArchiveIncarnationError(executionId, incarnation, h.incarnation);
152
+ }
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * The shared `append` rule for a backend, given the headers already stored (ascending): `"noop"`
158
+ * for an exact repeat of a stored segment, `"append"` when the segment starts after the last one,
159
+ * else throws {@link ArchiveConflictError}.
160
+ */
161
+ function classifyAppend(executionId, stored, segment) {
162
+ if (!Number.isSafeInteger(segment.from) || !Number.isSafeInteger(segment.through)) {
163
+ throw new RangeError("archive segment bounds must be safe integers");
164
+ }
165
+ if (segment.from > segment.through) {
166
+ throw new RangeError(`archive segment [${segment.from}..${segment.through}] is inverted`);
167
+ }
168
+ for (const h of stored) {
169
+ if (h.incarnation !== segment.incarnation) {
170
+ throw new ArchiveIncarnationError(executionId, h.incarnation, segment.incarnation);
171
+ }
172
+ }
173
+ for (const h of stored) {
174
+ if (h.from === segment.from && h.through === segment.through && h.digest === segment.digest) {
175
+ return "noop";
176
+ }
177
+ }
178
+ const last = stored[stored.length - 1];
179
+ if (last !== undefined && segment.from <= last.through) {
180
+ throw new ArchiveConflictError(executionId, segment, last.through);
181
+ }
182
+ return "append";
183
+ }
184
+ //# sourceMappingURL=archiveSpi.js.map