@sekiban/dcb-runtime 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +86 -0
  2. package/README.md +13 -0
  3. package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
  4. package/dist/allocator/AllocatorDurableObject.js +658 -0
  5. package/dist/allocator/IssuanceLedger.d.ts +188 -0
  6. package/dist/allocator/IssuanceLedger.js +608 -0
  7. package/dist/allocator/IssuanceReconciler.d.ts +41 -0
  8. package/dist/allocator/IssuanceReconciler.js +191 -0
  9. package/dist/allocator/OrderClock.d.ts +41 -0
  10. package/dist/allocator/OrderClock.js +92 -0
  11. package/dist/allocator/SortableUniqueId.d.ts +46 -0
  12. package/dist/allocator/SortableUniqueId.js +156 -0
  13. package/dist/allocator/types.d.ts +52 -0
  14. package/dist/allocator/types.js +1 -0
  15. package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
  16. package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
  17. package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
  18. package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
  19. package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
  20. package/dist/bootstrap/OperatorBootstrap.js +104 -0
  21. package/dist/bootstrap/manifest.d.ts +9 -0
  22. package/dist/bootstrap/manifest.js +115 -0
  23. package/dist/bootstrap/types.d.ts +75 -0
  24. package/dist/bootstrap/types.js +1 -0
  25. package/dist/cli/OperatorRepairCli.d.ts +11 -0
  26. package/dist/cli/OperatorRepairCli.js +108 -0
  27. package/dist/cloudflare.d.ts +164 -0
  28. package/dist/cloudflare.js +19487 -0
  29. package/dist/commit/CommitWorker.d.ts +101 -0
  30. package/dist/commit/CommitWorker.js +921 -0
  31. package/dist/commit/types.d.ts +25 -0
  32. package/dist/commit/types.js +13 -0
  33. package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
  34. package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
  35. package/dist/completeness/types.d.ts +85 -0
  36. package/dist/completeness/types.js +6 -0
  37. package/dist/composition.d.ts +123 -0
  38. package/dist/composition.js +237 -0
  39. package/dist/cosmos.d.ts +11 -0
  40. package/dist/cosmos.js +1411 -0
  41. package/dist/d1-mv.d.ts +8 -0
  42. package/dist/d1-mv.js +2001 -0
  43. package/dist/d1.d.ts +16 -0
  44. package/dist/d1.js +3418 -0
  45. package/dist/diagnostics/G60DurableHop.d.ts +99 -0
  46. package/dist/diagnostics/G60DurableHop.js +248 -0
  47. package/dist/diagnostics/G65Admission.d.ts +23 -0
  48. package/dist/diagnostics/G65Admission.js +43 -0
  49. package/dist/diagnostics/G65DirectRing.d.ts +33 -0
  50. package/dist/diagnostics/G65DirectRing.js +85 -0
  51. package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
  52. package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
  53. package/dist/downstream/DeliveryCore.d.ts +127 -0
  54. package/dist/downstream/DeliveryCore.js +304 -0
  55. package/dist/downstream/Doorbell.d.ts +43 -0
  56. package/dist/downstream/Doorbell.js +137 -0
  57. package/dist/downstream/DownstreamAdapter.d.ts +52 -0
  58. package/dist/downstream/DownstreamAdapter.js +299 -0
  59. package/dist/downstream/ExclusionLookup.d.ts +36 -0
  60. package/dist/downstream/ExclusionLookup.js +50 -0
  61. package/dist/downstream/InconsistencyDetector.d.ts +27 -0
  62. package/dist/downstream/InconsistencyDetector.js +82 -0
  63. package/dist/downstream/OutboxDrain.d.ts +35 -0
  64. package/dist/downstream/OutboxDrain.js +126 -0
  65. package/dist/downstream/types.d.ts +77 -0
  66. package/dist/downstream/types.js +64 -0
  67. package/dist/eventIdentity.d.ts +30 -0
  68. package/dist/eventIdentity.js +49 -0
  69. package/dist/eventRecord.d.ts +26 -0
  70. package/dist/eventRecord.js +53 -0
  71. package/dist/g42-probe.d.ts +9 -0
  72. package/dist/g42-probe.js +9 -0
  73. package/dist/http/SerializedQueryWorker.d.ts +58 -0
  74. package/dist/http/SerializedQueryWorker.js +497 -0
  75. package/dist/http/commitResponse.d.ts +46 -0
  76. package/dist/http/commitResponse.js +80 -0
  77. package/dist/http/testServiceId.d.ts +17 -0
  78. package/dist/http/testServiceId.js +14 -0
  79. package/dist/index.d.ts +112 -0
  80. package/dist/index.js +19926 -0
  81. package/dist/journal/JournalDurableObject.d.ts +30 -0
  82. package/dist/journal/JournalDurableObject.js +481 -0
  83. package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
  84. package/dist/journal/JournalFirstTouchProbe.js +375 -0
  85. package/dist/journal/types.d.ts +116 -0
  86. package/dist/journal/types.js +52 -0
  87. package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
  88. package/dist/mv/MaterializedViewCatchUp.js +353 -0
  89. package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
  90. package/dist/mv/MaterializedViewRuntime.js +328 -0
  91. package/dist/mv/MaterializedViewStore.d.ts +257 -0
  92. package/dist/mv/MaterializedViewStore.js +875 -0
  93. package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
  94. package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
  95. package/dist/mv/index.d.ts +8 -0
  96. package/dist/mv/index.js +2357 -0
  97. package/dist/projection/LiveProjectionWorker.d.ts +86 -0
  98. package/dist/projection/LiveProjectionWorker.js +240 -0
  99. package/dist/projection/ProjectionRuntime.d.ts +113 -0
  100. package/dist/projection/ProjectionRuntime.js +346 -0
  101. package/dist/projection/ProjectorRegistry.d.ts +51 -0
  102. package/dist/projection/ProjectorRegistry.js +116 -0
  103. package/dist/query/ProjectionQueryStore.d.ts +102 -0
  104. package/dist/query/ProjectionQueryStore.js +185 -0
  105. package/dist/query/QueryRegistry.d.ts +22 -0
  106. package/dist/query/QueryRegistry.js +36 -0
  107. package/dist/read/SerializedReadWorker.d.ts +36 -0
  108. package/dist/read/SerializedReadWorker.js +225 -0
  109. package/dist/repair/RepairWorker.d.ts +72 -0
  110. package/dist/repair/RepairWorker.js +461 -0
  111. package/dist/safeWindow.d.ts +20 -0
  112. package/dist/safeWindow.js +36 -0
  113. package/dist/scope/ControlRouteScope.d.ts +22 -0
  114. package/dist/scope/ControlRouteScope.js +36 -0
  115. package/dist/scope/ScopeName.d.ts +34 -0
  116. package/dist/scope/ScopeName.js +67 -0
  117. package/dist/service/ServiceIdentityProvider.d.ts +35 -0
  118. package/dist/service/ServiceIdentityProvider.js +56 -0
  119. package/dist/store/CosmosEventStore.d.ts +126 -0
  120. package/dist/store/CosmosEventStore.js +950 -0
  121. package/dist/store/D1EventStore.d.ts +111 -0
  122. package/dist/store/D1EventStore.js +1111 -0
  123. package/dist/store/PostgresEventStore.d.ts +35 -0
  124. package/dist/store/PostgresEventStore.js +811 -0
  125. package/dist/store/provider.d.ts +25 -0
  126. package/dist/store/provider.js +32 -0
  127. package/dist/store/types.d.ts +205 -0
  128. package/dist/store/types.js +18 -0
  129. package/dist/tag/EventDigest.d.ts +40 -0
  130. package/dist/tag/EventDigest.js +158 -0
  131. package/dist/tag/TagDurableObject.d.ts +286 -0
  132. package/dist/tag/TagDurableObject.js +4071 -0
  133. package/dist/tag/TagSqlMeasurement.d.ts +26 -0
  134. package/dist/tag/TagSqlMeasurement.js +44 -0
  135. package/dist/tag/TagSqlSchema.d.ts +29 -0
  136. package/dist/tag/TagSqlSchema.js +211 -0
  137. package/dist/tag/types.d.ts +173 -0
  138. package/dist/tag/types.js +3 -0
  139. package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
  140. package/dist/tagstate/TagStateDurableObject.js +499 -0
  141. package/dist/trace/AttributionRatio.d.ts +12 -0
  142. package/dist/trace/AttributionRatio.js +68 -0
  143. package/dist/trace/CloudflareTracing.d.ts +7 -0
  144. package/dist/trace/CloudflareTracing.js +9 -0
  145. package/dist/trace/CommitTrace.d.ts +405 -0
  146. package/dist/trace/CommitTrace.js +1008 -0
  147. package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
  148. package/dist/trace/CommitTraceConsoleSink.js +114 -0
  149. package/dist/trace/CommitTraceVerifier.d.ts +22 -0
  150. package/dist/trace/CommitTraceVerifier.js +295 -0
  151. package/dist/trace/ObservationStream.d.ts +118 -0
  152. package/dist/trace/ObservationStream.js +148 -0
  153. package/package.json +71 -0
@@ -0,0 +1,191 @@
1
+ import { scopeIdFor } from "../scope/ScopeName";
2
+ import { DEFAULT_WRITER_EPOCH, ISSUANCE_NEVER_CONTACTED_GRACE_MS, ISSUANCE_RECOVERY_KEY, UNRESOLVED_INDEX_PREFIX, countUnresolvedEntries, envelopeKey, parseIndexEntry, syncIssuanceRecoveryAlarm, targetKey, } from "./IssuanceLedger";
3
+ const RECONCILE_BATCH_LIMIT = 8;
4
+ const RECONCILE_RETRY_MS = 1_000;
5
+ export async function inspectTagTarget(env, serviceId, tag, envelope) {
6
+ const url = new URL("https://tag.internal/__internal/g77/inspect-target");
7
+ url.searchParams.set("__serviceId", serviceId);
8
+ url.searchParams.set("__tag", tag);
9
+ const response = await env.TAG.get(scopeIdFor(env.TAG, {
10
+ serviceId,
11
+ doClass: "tag",
12
+ identity: tag,
13
+ })).fetch(new Request(url, {
14
+ method: "POST",
15
+ headers: { "content-type": "application/json" },
16
+ body: JSON.stringify({
17
+ eventId: envelope.eventId,
18
+ suid: envelope.suid,
19
+ canonicalTargetTags: envelope.canonicalTargetTags,
20
+ pinnedWriterEpoch: envelope.pinnedWriterEpoch,
21
+ attemptId: envelope.attemptId,
22
+ }),
23
+ }));
24
+ if (response.status === 503)
25
+ return { kind: "unreachable" };
26
+ if (response.status === 409)
27
+ return { kind: "not-terminal-yet" };
28
+ if (response.status !== 200)
29
+ return { kind: "unreachable" };
30
+ const body = await response.json();
31
+ if (body.inspectionStatus === "absent-never-contacted") {
32
+ return { kind: "absent-never-contacted" };
33
+ }
34
+ if (body.inspectionStatus === "absent-but-unfenced") {
35
+ return { kind: "absent-but-unfenced" };
36
+ }
37
+ if (body.terminalStatus !== "installed-and-covered" &&
38
+ body.terminalStatus !== "absent-and-irrevocably-fenced") {
39
+ return { kind: "not-terminal-yet" };
40
+ }
41
+ return { kind: "terminal", result: body };
42
+ }
43
+ export async function fenceAbsentTarget(env, serviceId, tag, envelope) {
44
+ const url = new URL("https://tag.internal/__internal/g77/fence-absent-target");
45
+ url.searchParams.set("__serviceId", serviceId);
46
+ url.searchParams.set("__tag", tag);
47
+ const response = await env.TAG.get(scopeIdFor(env.TAG, {
48
+ serviceId,
49
+ doClass: "tag",
50
+ identity: tag,
51
+ })).fetch(new Request(url, {
52
+ method: "POST",
53
+ headers: { "content-type": "application/json" },
54
+ body: JSON.stringify({
55
+ attemptId: envelope.attemptId,
56
+ epoch: envelope.pinnedWriterEpoch ?? DEFAULT_WRITER_EPOCH,
57
+ }),
58
+ }));
59
+ return response.status >= 200 && response.status < 300;
60
+ }
61
+ function tagObservationFromInspect(result) {
62
+ if (result.terminalStatus === "installed-and-covered") {
63
+ return {
64
+ terminalStatus: "installed-and-covered",
65
+ ...(result.obligationDigest !== undefined ? { obligationDigest: result.obligationDigest } : {}),
66
+ };
67
+ }
68
+ return {
69
+ terminalStatus: "absent-and-irrevocably-fenced",
70
+ ...(result.tombstoneEpoch !== undefined ? { tombstoneEpoch: result.tombstoneEpoch } : {}),
71
+ };
72
+ }
73
+ export function tagObservationMatchesInspect(submitted, result) {
74
+ if (submitted.terminalStatus !== result.terminalStatus)
75
+ return false;
76
+ if (submitted.terminalStatus === "installed-and-covered") {
77
+ return submitted.obligationDigest === result.obligationDigest;
78
+ }
79
+ return submitted.tombstoneEpoch === result.tombstoneEpoch;
80
+ }
81
+ export async function verifyTargetResolutionEvidence(env, serviceId, evidence, envelope) {
82
+ if (evidence.pinnedWriterEpoch !== envelope.pinnedWriterEpoch) {
83
+ throw new Error("resolution evidence pinned writer epoch does not match envelope");
84
+ }
85
+ const outcome = await inspectTagTarget(env, serviceId, evidence.tag, envelope);
86
+ if (outcome.kind !== "terminal") {
87
+ throw new Error("tag observation is not verified by live inspect");
88
+ }
89
+ if (!tagObservationMatchesInspect(evidence.tagObservation, outcome.result)) {
90
+ throw new Error("tag observation does not match live inspect");
91
+ }
92
+ if (evidence.tagObservation.terminalStatus === "absent-and-irrevocably-fenced" &&
93
+ envelope.pinnedWriterEpoch > (evidence.tagObservation.tombstoneEpoch ?? -1)) {
94
+ throw new Error("fenced observation violates writer epoch rule");
95
+ }
96
+ }
97
+ export async function submitTargetResolution(env, serviceId, evidence) {
98
+ const response = await env.ALLOCATOR.get(scopeIdFor(env.ALLOCATOR, {
99
+ serviceId,
100
+ doClass: "allocator",
101
+ identity: "allocator",
102
+ })).fetch(new Request("https://allocator.internal/__internal/g77/resolve-target", {
103
+ method: "POST",
104
+ headers: { "content-type": "application/json" },
105
+ body: JSON.stringify(evidence),
106
+ }));
107
+ return response.status >= 200 && response.status < 300;
108
+ }
109
+ export async function reconcileIssuanceBatch(storage, env, serviceId, resolveTarget, options) {
110
+ let processed = 0;
111
+ const now = Date.now();
112
+ const listed = await storage.list({
113
+ prefix: UNRESOLVED_INDEX_PREFIX,
114
+ limit: RECONCILE_BATCH_LIMIT,
115
+ });
116
+ for (const [key] of listed) {
117
+ const parsed = parseIndexEntry(key, UNRESOLVED_INDEX_PREFIX);
118
+ if (parsed === undefined)
119
+ continue;
120
+ const envelope = await storage.get(envelopeKey(parsed.attemptId, parsed.candidateIndex));
121
+ if (envelope === undefined)
122
+ continue;
123
+ for (const tag of envelope.canonicalTargetTags) {
124
+ const target = await storage.get(targetKey(parsed.attemptId, parsed.candidateIndex, tag));
125
+ if (target === undefined || target.status !== "pending")
126
+ continue;
127
+ let outcome = await inspectTagTarget(env, serviceId, tag, envelope);
128
+ if (outcome.kind === "unreachable" || outcome.kind === "not-terminal-yet") {
129
+ continue;
130
+ }
131
+ if (outcome.kind === "absent-never-contacted") {
132
+ const issuedAtMs = envelope.issuedAtMs ?? 0;
133
+ if (options?.bypassNeverContactedGrace !== true
134
+ && now - issuedAtMs < ISSUANCE_NEVER_CONTACTED_GRACE_MS) {
135
+ continue;
136
+ }
137
+ }
138
+ if (outcome.kind === "absent-never-contacted" || outcome.kind === "absent-but-unfenced") {
139
+ const fenced = await fenceAbsentTarget(env, serviceId, tag, envelope);
140
+ if (!fenced)
141
+ continue;
142
+ outcome = await inspectTagTarget(env, serviceId, tag, envelope);
143
+ }
144
+ if (outcome.kind !== "terminal")
145
+ continue;
146
+ const evidence = {
147
+ serviceId: envelope.serviceId,
148
+ allocatorLineageId: envelope.allocatorLineageId,
149
+ attemptId: envelope.attemptId,
150
+ candidateIndex: envelope.candidateIndex,
151
+ suid: envelope.suid,
152
+ tag,
153
+ identityDigest: envelope.identityDigest,
154
+ pinnedWriterEpoch: envelope.pinnedWriterEpoch,
155
+ tagObservation: tagObservationFromInspect(outcome.result),
156
+ };
157
+ const submitted = resolveTarget !== undefined
158
+ ? await resolveTarget(evidence)
159
+ : "ALLOCATOR" in env && env.ALLOCATOR !== undefined
160
+ ? await submitTargetResolution(env, serviceId, evidence)
161
+ : false;
162
+ if (submitted)
163
+ processed += 1;
164
+ }
165
+ }
166
+ const existingSchedule = await storage.get(ISSUANCE_RECOVERY_KEY);
167
+ const attempts = (existingSchedule?.attempts ?? 0) + 1;
168
+ const unresolvedCount = await countUnresolvedEntries(storage);
169
+ if (unresolvedCount === 0) {
170
+ if (existingSchedule !== undefined) {
171
+ await storage.put(ISSUANCE_RECOVERY_KEY, {
172
+ ...existingSchedule,
173
+ attempts,
174
+ });
175
+ }
176
+ await storage.deleteAlarm();
177
+ return { processed, rearmAt: now };
178
+ }
179
+ const retryAt = now + RECONCILE_RETRY_MS;
180
+ const scheduled = existingSchedule?.nextDueAt;
181
+ const nextDueAt = scheduled !== undefined && scheduled > now
182
+ ? Math.min(scheduled, retryAt)
183
+ : retryAt;
184
+ await storage.put(ISSUANCE_RECOVERY_KEY, {
185
+ nextDueAt,
186
+ cursor: null,
187
+ attempts,
188
+ });
189
+ await syncIssuanceRecoveryAlarm(storage, now);
190
+ return { processed, rearmAt: nextDueAt };
191
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The only allocator time seam. A tick is a Unix millisecond; conversion to
3
+ * the .NET tick representation happens exactly once at allocation admission.
4
+ */
5
+ export interface OrderClock {
6
+ tick(): bigint;
7
+ }
8
+ export declare class OrderClockReadError extends Error {
9
+ readonly code: "ALLOCATOR_ORDER_CLOCK_FAILED";
10
+ constructor(message: string, options?: ErrorOptions);
11
+ }
12
+ export declare const systemOrderClock: OrderClock;
13
+ /** Compatibility export names now denote the C# fixed-width 30-digit format. */
14
+ export declare const ORDER_SUID_DIGITS: number;
15
+ export declare const ORDER_SUID_PREFIX = "";
16
+ export declare const ORDER_SUID_LIMIT: bigint;
17
+ /** @deprecated Use formatSortableUniqueId. The ordinal is a .NET tick value. */
18
+ export declare function encodeOrderOrdinal(value: bigint): string;
19
+ /** @deprecated Use assertSortableUniqueId(value).ticks. */
20
+ export declare function decodeOrderOrdinal(value: string): bigint;
21
+ export interface OrderAllocationRange {
22
+ /** Logical .NET tick allocation base. */
23
+ readonly base: bigint;
24
+ readonly baseTicks: bigint;
25
+ readonly physicalTicks: bigint;
26
+ readonly observedTicks: bigint | null;
27
+ readonly watermark: string;
28
+ readonly suids: readonly string[];
29
+ }
30
+ export interface OrderAllocationOptions {
31
+ /** Deterministic test injection; production omits this and uses crypto. */
32
+ readonly suffixes?: readonly bigint[];
33
+ }
34
+ /**
35
+ * Pure range oracle used by the DO and deterministic fixtures. Watermark
36
+ * authority is only its leading nineteen tick digits; its random suffix is
37
+ * retained durably for replay but never participates in the monotone base.
38
+ */
39
+ export declare function allocateOrderRange(watermark: string | null, count: number, clockTick: bigint, options?: OrderAllocationOptions): OrderAllocationRange;
40
+ /** Diagnostic only; it derives from allocated ticks, never a second clock. */
41
+ export declare function diagnosticAllocatedAt(ticks: bigint): string;
@@ -0,0 +1,92 @@
1
+ import { DOTNET_MAX_TICKS, SORTABLE_UNIQUE_ID_DIGITS, SortableUniqueIdError, assertSortableUniqueId, cryptoRandomSortableUniqueIdSuffix, dotNetTicksToUnixMs, formatSortableUniqueId, unixMsToDotNetTicks, } from "./SortableUniqueId";
2
+ export class OrderClockReadError extends Error {
3
+ code = "ALLOCATOR_ORDER_CLOCK_FAILED";
4
+ constructor(message, options) {
5
+ super(message, options);
6
+ this.name = "OrderClockReadError";
7
+ }
8
+ }
9
+ export const systemOrderClock = Object.freeze({
10
+ tick() {
11
+ const milliseconds = Date.now();
12
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) {
13
+ throw new OrderClockReadError("System Unix millisecond clock returned an invalid value");
14
+ }
15
+ return BigInt(milliseconds);
16
+ },
17
+ });
18
+ /** Compatibility export names now denote the C# fixed-width 30-digit format. */
19
+ export const ORDER_SUID_DIGITS = SORTABLE_UNIQUE_ID_DIGITS;
20
+ export const ORDER_SUID_PREFIX = "";
21
+ export const ORDER_SUID_LIMIT = DOTNET_MAX_TICKS + 1n;
22
+ /** @deprecated Use formatSortableUniqueId. The ordinal is a .NET tick value. */
23
+ export function encodeOrderOrdinal(value) {
24
+ return formatSortableUniqueId(value, 0n);
25
+ }
26
+ /** @deprecated Use assertSortableUniqueId(value).ticks. */
27
+ export function decodeOrderOrdinal(value) {
28
+ try {
29
+ return assertSortableUniqueId(value).ticks;
30
+ }
31
+ catch (error) {
32
+ const detail = error instanceof Error ? error.message : "invalid SortableUniqueId";
33
+ throw new OrderClockReadError(detail, { cause: error });
34
+ }
35
+ }
36
+ function asOrderClockError(error) {
37
+ if (error instanceof OrderClockReadError)
38
+ return error;
39
+ const detail = error instanceof Error ? error.message : "SortableUniqueId allocation failed";
40
+ return new OrderClockReadError(detail, { cause: error });
41
+ }
42
+ /**
43
+ * Pure range oracle used by the DO and deterministic fixtures. Watermark
44
+ * authority is only its leading nineteen tick digits; its random suffix is
45
+ * retained durably for replay but never participates in the monotone base.
46
+ */
47
+ export function allocateOrderRange(watermark, count, clockTick, options = {}) {
48
+ if (!Number.isSafeInteger(count) || count <= 0) {
49
+ throw new OrderClockReadError("An order allocation requires a positive candidate count");
50
+ }
51
+ try {
52
+ const physicalTicks = unixMsToDotNetTicks(clockTick);
53
+ const observedTicks = watermark === null ? null : assertSortableUniqueId(watermark).ticks;
54
+ const baseTicks = observedTicks === null || physicalTicks > observedTicks + 1n
55
+ ? physicalTicks
56
+ : observedTicks + 1n;
57
+ const lastTicks = baseTicks + BigInt(count) - 1n;
58
+ if (lastTicks > DOTNET_MAX_TICKS) {
59
+ throw new SortableUniqueIdError("SUID_RANGE_EXHAUSTED", "Allocator SortableUniqueId range is exhausted before write");
60
+ }
61
+ if (options.suffixes !== undefined && options.suffixes.length !== count) {
62
+ throw new OrderClockReadError("A deterministic SortableUniqueId suffix is required for every candidate");
63
+ }
64
+ const suids = Array.from({ length: count }, (_, index) => formatSortableUniqueId(baseTicks + BigInt(index), options.suffixes?.[index] ?? cryptoRandomSortableUniqueIdSuffix()));
65
+ return Object.freeze({
66
+ base: baseTicks,
67
+ baseTicks,
68
+ physicalTicks,
69
+ observedTicks,
70
+ watermark: suids.at(-1),
71
+ suids: Object.freeze(suids),
72
+ });
73
+ }
74
+ catch (error) {
75
+ if (error instanceof SortableUniqueIdError)
76
+ throw error;
77
+ throw asOrderClockError(error);
78
+ }
79
+ }
80
+ /** Diagnostic only; it derives from allocated ticks, never a second clock. */
81
+ export function diagnosticAllocatedAt(ticks) {
82
+ try {
83
+ const milliseconds = dotNetTicksToUnixMs(ticks);
84
+ if (milliseconds <= BigInt(Number.MAX_SAFE_INTEGER)) {
85
+ return new Date(Number(milliseconds)).toISOString();
86
+ }
87
+ }
88
+ catch {
89
+ // Preserve a non-authoritative diagnostic for values beyond Date's range.
90
+ }
91
+ return `ticks:${ticks.toString()}`;
92
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Sekiban.Dcb SortableUniqueId parity primitives.
3
+ *
4
+ * A value is deliberately represented as its canonical 30 ASCII decimal
5
+ * characters. The first nineteen characters are .NET DateTime ticks and
6
+ * the final eleven are the allocator-owned random suffix. Keeping both
7
+ * components as bigint avoids accidental IEEE-754 rounding at large clocks.
8
+ */
9
+ export declare const SORTABLE_UNIQUE_ID_TICKS_DIGITS = 19;
10
+ export declare const SORTABLE_UNIQUE_ID_RANDOM_DIGITS = 11;
11
+ export declare const SORTABLE_UNIQUE_ID_DIGITS: number;
12
+ export declare const DOTNET_UNIX_EPOCH_TICKS = 621355968000000000n;
13
+ export declare const DOTNET_MAX_TICKS = 3155378975999999999n;
14
+ export declare const DOTNET_TICKS_PER_MILLISECOND = 10000n;
15
+ export declare const SORTABLE_UNIQUE_ID_RANDOM_LIMIT: bigint;
16
+ export declare const MAX_UNIX_MILLISECONDS: bigint;
17
+ export type SortableUniqueIdErrorCode = "SUID_INVALID" | "SUID_LEGACY_FORMAT_RETIRED" | "SUID_TICKS_INVALID" | "SUID_RANDOM_INVALID" | "SUID_UNIX_MILLISECONDS_INVALID" | "SUID_RANGE_EXHAUSTED" | "SUID_RANDOM_UNAVAILABLE";
18
+ export declare class SortableUniqueIdError extends Error {
19
+ readonly code: SortableUniqueIdErrorCode;
20
+ constructor(code: SortableUniqueIdErrorCode, message: string, options?: ErrorOptions);
21
+ }
22
+ export interface ParsedSortableUniqueId {
23
+ readonly value: string;
24
+ readonly ticks: bigint;
25
+ readonly random: bigint;
26
+ }
27
+ /**
28
+ * Test-only observation seam for the single retired-SUID decision point.
29
+ * Production never installs an observer; the validator still fail-closes
30
+ * before any durable actor is acquired.
31
+ */
32
+ export declare function observeLegacySortableUniqueIdDecision<T>(observer: (value: string) => void, run: () => Promise<T> | T): Promise<T>;
33
+ /** Convert an exact Unix-millisecond value to C# DateTime ticks. */
34
+ export declare function unixMsToDotNetTicks(value: number | string | bigint): bigint;
35
+ /** Inverse C# parity helper; fractional 100ns ticks truncate toward zero. */
36
+ export declare function dotNetTicksToUnixMs(ticks: bigint): bigint;
37
+ export declare function assertDotNetTicks(ticks: bigint): bigint;
38
+ export declare function assertSortableUniqueId(value: string): ParsedSortableUniqueId;
39
+ export declare function isSortableUniqueId(value: unknown): value is string;
40
+ export declare function formatSortableUniqueId(ticks: bigint, random: bigint): string;
41
+ /**
42
+ * C# parity requires a crypto-derived, non-negative 31-bit suffix. The
43
+ * modulo is retained literally because it is part of the required contract.
44
+ */
45
+ export declare function cryptoRandomSortableUniqueIdSuffix(): bigint;
46
+ export declare function compareSortableUniqueId(left: string, right: string): number;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Sekiban.Dcb SortableUniqueId parity primitives.
3
+ *
4
+ * A value is deliberately represented as its canonical 30 ASCII decimal
5
+ * characters. The first nineteen characters are .NET DateTime ticks and
6
+ * the final eleven are the allocator-owned random suffix. Keeping both
7
+ * components as bigint avoids accidental IEEE-754 rounding at large clocks.
8
+ */
9
+ export const SORTABLE_UNIQUE_ID_TICKS_DIGITS = 19;
10
+ export const SORTABLE_UNIQUE_ID_RANDOM_DIGITS = 11;
11
+ export const SORTABLE_UNIQUE_ID_DIGITS = SORTABLE_UNIQUE_ID_TICKS_DIGITS + SORTABLE_UNIQUE_ID_RANDOM_DIGITS;
12
+ export const DOTNET_UNIX_EPOCH_TICKS = 621355968000000000n;
13
+ export const DOTNET_MAX_TICKS = 3155378975999999999n;
14
+ export const DOTNET_TICKS_PER_MILLISECOND = 10000n;
15
+ export const SORTABLE_UNIQUE_ID_RANDOM_LIMIT = 10n ** BigInt(SORTABLE_UNIQUE_ID_RANDOM_DIGITS);
16
+ export const MAX_UNIX_MILLISECONDS = (DOTNET_MAX_TICKS - DOTNET_UNIX_EPOCH_TICKS) / DOTNET_TICKS_PER_MILLISECOND;
17
+ export class SortableUniqueIdError extends Error {
18
+ code;
19
+ constructor(code, message, options) {
20
+ super(message, options);
21
+ this.code = code;
22
+ this.name = "SortableUniqueIdError";
23
+ }
24
+ }
25
+ /**
26
+ * A G32 ingress must not silently treat an old prefixed SUID as merely an
27
+ * arbitrary malformed string. Keeping this classification at the one
28
+ * shared validator makes the retired-format boundary observable to every
29
+ * ingress without creating a compatibility lane.
30
+ */
31
+ const LEGACY_PREFIXED_SUID = /^suid-[0-9]+$/;
32
+ const LEGACY_DECISION_OBSERVER = Symbol.for("@sekiban/dcb-runtime/legacy-sortable-unique-id-observer");
33
+ function legacyDecisionObserver() {
34
+ return globalThis[LEGACY_DECISION_OBSERVER];
35
+ }
36
+ /**
37
+ * Test-only observation seam for the single retired-SUID decision point.
38
+ * Production never installs an observer; the validator still fail-closes
39
+ * before any durable actor is acquired.
40
+ */
41
+ export async function observeLegacySortableUniqueIdDecision(observer, run) {
42
+ const globals = globalThis;
43
+ const previous = globals[LEGACY_DECISION_OBSERVER];
44
+ globals[LEGACY_DECISION_OBSERVER] = observer;
45
+ try {
46
+ return await run();
47
+ }
48
+ finally {
49
+ globals[LEGACY_DECISION_OBSERVER] = previous;
50
+ }
51
+ }
52
+ function canonicalDecimal(value, name) {
53
+ if (!/^(?:0|[1-9][0-9]*)$/.test(value)) {
54
+ throw new SortableUniqueIdError("SUID_UNIX_MILLISECONDS_INVALID", `${name} must be canonical ASCII decimal text`);
55
+ }
56
+ return BigInt(value);
57
+ }
58
+ function asUnixMilliseconds(value) {
59
+ if (typeof value === "number") {
60
+ if (!Number.isSafeInteger(value) || value < 0) {
61
+ throw new SortableUniqueIdError("SUID_UNIX_MILLISECONDS_INVALID", "Unix milliseconds must be a non-negative safe integer");
62
+ }
63
+ return BigInt(value);
64
+ }
65
+ if (typeof value === "string")
66
+ return canonicalDecimal(value, "Unix milliseconds");
67
+ if (value < 0n) {
68
+ throw new SortableUniqueIdError("SUID_UNIX_MILLISECONDS_INVALID", "Unix milliseconds must be non-negative");
69
+ }
70
+ return value;
71
+ }
72
+ /** Convert an exact Unix-millisecond value to C# DateTime ticks. */
73
+ export function unixMsToDotNetTicks(value) {
74
+ const milliseconds = asUnixMilliseconds(value);
75
+ // BigInt conversion intentionally happens before multiplying. Do not
76
+ // collapse this into Number arithmetic: M3 covers the 2^53 boundary.
77
+ const ticks = milliseconds * DOTNET_TICKS_PER_MILLISECOND + DOTNET_UNIX_EPOCH_TICKS;
78
+ if (ticks < 0n || ticks > DOTNET_MAX_TICKS) {
79
+ throw new SortableUniqueIdError("SUID_TICKS_INVALID", "Unix milliseconds are outside the .NET DateTime tick range");
80
+ }
81
+ return ticks;
82
+ }
83
+ /** Inverse C# parity helper; fractional 100ns ticks truncate toward zero. */
84
+ export function dotNetTicksToUnixMs(ticks) {
85
+ assertDotNetTicks(ticks);
86
+ if (ticks < DOTNET_UNIX_EPOCH_TICKS) {
87
+ throw new SortableUniqueIdError("SUID_TICKS_INVALID", "SortableUniqueId ticks predate the Unix epoch");
88
+ }
89
+ return (ticks - DOTNET_UNIX_EPOCH_TICKS) / DOTNET_TICKS_PER_MILLISECOND;
90
+ }
91
+ export function assertDotNetTicks(ticks) {
92
+ // C# uses the fixed-width `TickFormatter` when it serializes a tick. A
93
+ // numeric tick before the year in which DateTime.Ticks reaches 10^18 is
94
+ // therefore intentionally shorter than nineteen digits before formatting.
95
+ // Validate the numeric .NET range here and the exact 19-character field at
96
+ // the string ingress below.
97
+ if (ticks < 0n || ticks > DOTNET_MAX_TICKS) {
98
+ throw new SortableUniqueIdError("SUID_TICKS_INVALID", "SortableUniqueId ticks are outside the .NET DateTime range");
99
+ }
100
+ return ticks;
101
+ }
102
+ export function assertSortableUniqueId(value) {
103
+ if (typeof value === "string" && LEGACY_PREFIXED_SUID.test(value)) {
104
+ legacyDecisionObserver()?.(value);
105
+ throw new SortableUniqueIdError("SUID_LEGACY_FORMAT_RETIRED", "Legacy prefixed SortableUniqueId is retired; G32 requires exactly 30 ASCII decimal digits");
106
+ }
107
+ if (typeof value !== "string" || !new RegExp(`^[0-9]{${SORTABLE_UNIQUE_ID_DIGITS}}$`).test(value)) {
108
+ throw new SortableUniqueIdError("SUID_INVALID", "SortableUniqueId must be exactly 30 ASCII decimal digits");
109
+ }
110
+ const ticksText = value.slice(0, SORTABLE_UNIQUE_ID_TICKS_DIGITS);
111
+ const randomText = value.slice(SORTABLE_UNIQUE_ID_TICKS_DIGITS);
112
+ // Keep the 19-character field check separate from the numeric check. In
113
+ // particular, `0006213559680000000` is the valid Unix-epoch tick field and
114
+ // must not be rejected merely because BigInt removes its leading zero.
115
+ const ticks = assertDotNetTicks(BigInt(ticksText));
116
+ const random = BigInt(randomText);
117
+ if (random < 0n || random >= SORTABLE_UNIQUE_ID_RANDOM_LIMIT) {
118
+ throw new SortableUniqueIdError("SUID_RANDOM_INVALID", "SortableUniqueId random suffix is outside its 11-digit range");
119
+ }
120
+ return Object.freeze({ value, ticks, random });
121
+ }
122
+ export function isSortableUniqueId(value) {
123
+ if (typeof value !== "string")
124
+ return false;
125
+ try {
126
+ assertSortableUniqueId(value);
127
+ return true;
128
+ }
129
+ catch {
130
+ return false;
131
+ }
132
+ }
133
+ export function formatSortableUniqueId(ticks, random) {
134
+ assertDotNetTicks(ticks);
135
+ if (random < 0n || random >= SORTABLE_UNIQUE_ID_RANDOM_LIMIT) {
136
+ throw new SortableUniqueIdError("SUID_RANDOM_INVALID", "SortableUniqueId random suffix is outside its 11-digit range");
137
+ }
138
+ return `${ticks.toString().padStart(SORTABLE_UNIQUE_ID_TICKS_DIGITS, "0")}${random.toString().padStart(SORTABLE_UNIQUE_ID_RANDOM_DIGITS, "0")}`;
139
+ }
140
+ /**
141
+ * C# parity requires a crypto-derived, non-negative 31-bit suffix. The
142
+ * modulo is retained literally because it is part of the required contract.
143
+ */
144
+ export function cryptoRandomSortableUniqueIdSuffix() {
145
+ if (typeof globalThis.crypto?.getRandomValues !== "function") {
146
+ throw new SortableUniqueIdError("SUID_RANDOM_UNAVAILABLE", "crypto.getRandomValues is required for SortableUniqueId allocation");
147
+ }
148
+ const words = new Uint32Array(1);
149
+ globalThis.crypto.getRandomValues(words);
150
+ return BigInt(words[0] & 0x7fff_ffff) % SORTABLE_UNIQUE_ID_RANDOM_LIMIT;
151
+ }
152
+ export function compareSortableUniqueId(left, right) {
153
+ const leftValue = assertSortableUniqueId(left).value;
154
+ const rightValue = assertSortableUniqueId(right).value;
155
+ return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
156
+ }
@@ -0,0 +1,52 @@
1
+ export interface AllocationCandidate {
2
+ candidateIndex: number;
3
+ eventId: string;
4
+ }
5
+ export interface AllocatedCandidate extends AllocationCandidate {
6
+ suid: string;
7
+ }
8
+ /**
9
+ * Allocator-issued authority for a closed SUID prefix.
10
+ *
11
+ * The issuance and reconciliation protocol that produces this value belongs
12
+ * to the allocator closure slice. Projection consumers treat the value as a
13
+ * cached, consumer-bound fact; they never manufacture one from a high-water
14
+ * mark or fetch one as part of an ordinary read.
15
+ */
16
+ export interface ClosedPrefixCertificate {
17
+ readonly certificateVersion: 1;
18
+ readonly authority: "allocator-transaction";
19
+ readonly status: "ready" | "unreconciled";
20
+ readonly allocatorLineageId: string;
21
+ readonly serviceId: string;
22
+ readonly closedPrefixSuid: string | null;
23
+ readonly unresolvedCount: number;
24
+ readonly generatedAt: number;
25
+ readonly migrationProofId: string | null;
26
+ }
27
+ /**
28
+ * The durable, attempt-keyed allocation result. Candidates are always ordered
29
+ * by candidateIndex, rather than by the order in which a caller sent them.
30
+ */
31
+ export interface AllocationVector {
32
+ attemptId: string;
33
+ /** Durable allocator lineage token; it changes when the allocator DO is recreated. */
34
+ allocatorLineageId: string;
35
+ candidates: AllocatedCandidate[];
36
+ allocatedAt: string;
37
+ }
38
+ /** The persisted service head; it is the allocated watermark. */
39
+ export interface AllocatorState {
40
+ schemaVersion: 5;
41
+ allocatorLineageId: string;
42
+ allocatedWatermark: string | null;
43
+ bootstrapSeed: {
44
+ importId: string;
45
+ leaseEpoch: number;
46
+ highWatermark: string;
47
+ } | null;
48
+ /** Durable rate-limit key for rollback warnings; it is not allocation authority. */
49
+ lastRollbackWarningFingerprint?: string | null;
50
+ /** Service identity bound at first commit-scoped allocation; reconciler authority. */
51
+ serviceId?: string;
52
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { type NativeTracing } from "../trace/CommitTrace";
2
+ interface BootstrapCoordinatorEnv {
3
+ TAG: DurableObjectNamespace;
4
+ ALLOCATOR: DurableObjectNamespace;
5
+ }
6
+ /** Per-service authority and linearization point for bootstrap and normal writes. */
7
+ export declare class BootstrapCoordinatorDurableObject implements DurableObject {
8
+ private readonly ctx;
9
+ private readonly env;
10
+ private readonly nativeTracing;
11
+ /** Constructor-scoped observation only; never persisted or used for control. */
12
+ private readonly activation;
13
+ constructor(ctx: DurableObjectState, env: BootstrapCoordinatorEnv, nativeTracing?: NativeTracing);
14
+ fetch(request: Request): Promise<Response>;
15
+ private traceCommand;
16
+ private control;
17
+ private valid;
18
+ private plan;
19
+ private chunks;
20
+ private import;
21
+ private verify;
22
+ private ready;
23
+ /** Operator abort retains the durable failed plan for audit/retry; it never clears target state. */
24
+ private abort;
25
+ private route;
26
+ private command;
27
+ }
28
+ export {};