@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,461 @@
1
+ import { scopeIdFor } from "../scope/ScopeName";
2
+ import { envServiceIdentity, requireServiceIdentity } from "../service/ServiceIdentityProvider";
3
+ import { CommitTrace, createTraceCorrelationId, stableTraceHash, } from "../trace/CommitTrace";
4
+ import { verifyCommitTrace } from "../trace/CommitTraceVerifier";
5
+ class RepairWorkerFailure extends Error {
6
+ }
7
+ function isObject(value) {
8
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9
+ }
10
+ function responseBody(response) {
11
+ return response.headers.get("content-type")?.includes("application/json") === true
12
+ ? response.json()
13
+ : Promise.resolve(undefined);
14
+ }
15
+ function itemKey(item) {
16
+ return `${item.attemptId}\u0000${item.eventId}\u0000${item.suid}`;
17
+ }
18
+ function hasPartialFence(facts, attemptId) {
19
+ return facts.fences.some((fence) => fence.reason === "partial_write" && fence.attemptId === attemptId);
20
+ }
21
+ function resolutionFor(facts, item) {
22
+ return facts.facts.resolutions.find((resolution) => resolution.attemptId === item.attemptId &&
23
+ resolution.eventId === item.eventId &&
24
+ resolution.suid === item.suid);
25
+ }
26
+ function auditFor(facts, item) {
27
+ return facts.facts.audits.some((audit) => audit.attemptId === item.attemptId && audit.eventId === item.eventId && audit.suid === item.suid);
28
+ }
29
+ function sortTaggedItems(items) {
30
+ return [...items].sort((left, right) => {
31
+ if (left.item.suid !== right.item.suid) {
32
+ return left.item.suid < right.item.suid ? -1 : 1;
33
+ }
34
+ const leftKey = `${left.tag}\u0000${itemKey(left.item)}`;
35
+ const rightKey = `${right.tag}\u0000${itemKey(right.item)}`;
36
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
37
+ });
38
+ }
39
+ /**
40
+ * Per-request repair coordinator. The only authority it trusts is a fresh
41
+ * Tag DO fact read; Journal records are append-only observations.
42
+ */
43
+ export class RepairWorker {
44
+ env;
45
+ exclusions;
46
+ serviceId;
47
+ hooks;
48
+ constructor(env, exclusions,
49
+ /** Tests and embedding hosts may supply a fixed identity explicitly. */
50
+ serviceId = requireServiceIdentity(envServiceIdentity(env)), hooks = {}) {
51
+ this.env = env;
52
+ this.exclusions = exclusions;
53
+ this.serviceId = serviceId;
54
+ this.hooks = hooks;
55
+ }
56
+ async execute(input) {
57
+ const executionId = crypto.randomUUID();
58
+ const trace = new CommitTrace({
59
+ schema: "sdt.commit.repair/v1",
60
+ correlationId: createTraceCorrelationId(),
61
+ serviceId: this.serviceId,
62
+ nativeTracing: this.hooks.nativeTracing,
63
+ sink: this.hooks.commitTraceSink,
64
+ clock: this.hooks.commitTraceClock,
65
+ provider: this.hooks.commitTraceProvider,
66
+ // Structural verification is an observation sink only. A malformed
67
+ // telemetry adapter can never change a repair's durable outcome.
68
+ runtimeVerifier: { verify: (snapshot) => { verifyCommitTrace(snapshot); } },
69
+ });
70
+ return trace.root("X00", {
71
+ face: "repair-root",
72
+ actorClass: "REPAIR",
73
+ actorKey: `repair:${this.serviceId}`,
74
+ attributes: { "repair.execution.id": executionId },
75
+ }, async (scope) => this.executeTraced(input, {
76
+ scope,
77
+ executionId,
78
+ leaseIds: new Map(),
79
+ }));
80
+ }
81
+ async executeTraced(input, trace) {
82
+ const workset = await this.enumerate(input.attemptIds, input.tags);
83
+ const filtered = workset.filter((entry) => input.checkpoint === undefined || entry.item.suid > input.checkpoint);
84
+ const bounded = filtered.slice(0, input.maxItems);
85
+ const checkpoint = bounded.length === 0 ? input.checkpoint ?? null : bounded[bounded.length - 1].item.suid;
86
+ if (input.mode === "dry-run") {
87
+ // Facts are intentionally read during planning so an operator can see
88
+ // which exact fences remain, while no mutation call is made.
89
+ await Promise.all([...new Set(workset.map((entry) => entry.tag))].map(async (tag, memberIndex) => {
90
+ if (trace === undefined)
91
+ return this.tagFacts(tag);
92
+ return trace.scope.fork().span("X03p", {
93
+ actorClass: "TAG",
94
+ actorKey: `tag:${this.serviceId}:${tag}`,
95
+ memberIndex,
96
+ tag,
97
+ }, async () => this.tagFacts(tag));
98
+ }));
99
+ return {
100
+ mode: input.mode,
101
+ dryRun: true,
102
+ checkpoint,
103
+ processed: 0,
104
+ rolledForward: 0,
105
+ excludedAudited: 0,
106
+ failedClosed: 0,
107
+ cleared: 0,
108
+ pending: filtered.length,
109
+ };
110
+ }
111
+ const result = {
112
+ mode: input.mode,
113
+ dryRun: false,
114
+ checkpoint,
115
+ processed: 0,
116
+ rolledForward: 0,
117
+ excludedAudited: 0,
118
+ failedClosed: 0,
119
+ cleared: 0,
120
+ pending: Math.max(0, filtered.length - bounded.length),
121
+ };
122
+ const leases = new Map();
123
+ for (const [memberIndex, entry] of bounded.entries()) {
124
+ const before = await this.tagFacts(entry.tag);
125
+ // Resume after clear-before-final-observation: no mutation is needed,
126
+ // and the current Tag fact is sufficient to reconstruct completion.
127
+ if (!hasPartialFence(before, entry.item.attemptId) && resolutionFor(before, entry.item) !== undefined && auditFor(before, entry.item)) {
128
+ if (trace !== undefined) {
129
+ // The row intentionally describes the durable-state decision only:
130
+ // no repair mutation is reached on this branch.
131
+ await trace.scope.fork().span("X03r", {
132
+ actorClass: "TAG",
133
+ actorKey: `tag:${this.serviceId}:${entry.tag}`,
134
+ attemptId: entry.item.attemptId,
135
+ memberIndex,
136
+ tag: entry.tag,
137
+ }, async () => undefined);
138
+ }
139
+ continue;
140
+ }
141
+ const leaseId = trace === undefined
142
+ ? undefined
143
+ : trace.leaseIds.get(entry.tag) ?? `repair-lease-${stableTraceHash(`${trace.executionId}:${entry.tag}`)}`;
144
+ if (trace !== undefined && leaseId !== undefined)
145
+ trace.leaseIds.set(entry.tag, leaseId);
146
+ const acquireLease = async () => this.ensureLease(entry.tag, input, workset, leases);
147
+ const needsLease = !leases.has(entry.tag);
148
+ const lease = trace === undefined || !needsLease
149
+ ? await acquireLease()
150
+ : await trace.scope.fork().span("X02", {
151
+ actorClass: "TAG",
152
+ actorKey: `tag:${this.serviceId}:${entry.tag}`,
153
+ attemptId: entry.item.attemptId,
154
+ memberIndex,
155
+ tag: entry.tag,
156
+ attributes: {
157
+ "repair.lease.id": leaseId,
158
+ },
159
+ }, acquireLease);
160
+ const executeItem = async () => {
161
+ if (input.fault === "after-lease-before-prepare") {
162
+ return { ...result, interrupted: input.fault, pending: filtered.length };
163
+ }
164
+ await this.observe(entry.tag, entry.item, lease, "PREPARED");
165
+ if (input.fault === "after-prepare-before-apply") {
166
+ return { ...result, interrupted: input.fault, pending: filtered.length };
167
+ }
168
+ let applied = await this.apply(entry.tag, entry.item, lease);
169
+ if (applied.status === 409 && applied.reason === "repair_scope_required") {
170
+ // F3 discovered after the first scan: the failed call has no side
171
+ // effect, then a fresh union makes the new work item eligible.
172
+ const refreshed = await this.enumerate(input.attemptIds, input.tags);
173
+ await this.unionScope(entry.tag, lease, refreshed.filter((candidate) => candidate.tag === entry.tag));
174
+ applied = await this.apply(entry.tag, entry.item, lease);
175
+ }
176
+ if (applied.status !== 200 || applied.branch === undefined) {
177
+ throw new RepairWorkerFailure(`Tag repair apply rejected: ${applied.reason ?? applied.status}`);
178
+ }
179
+ if (input.fault === "after-apply-before-observation") {
180
+ return { ...result, interrupted: input.fault, pending: filtered.length };
181
+ }
182
+ const facts = await this.tagFacts(entry.tag);
183
+ const durable = resolutionFor(facts, entry.item);
184
+ if (durable === undefined) {
185
+ throw new RepairWorkerFailure("Tag repair apply did not leave a durable resolution fact");
186
+ }
187
+ await this.observe(entry.tag, entry.item, lease, "VERIFIED", durable.branch);
188
+ if (input.fault === "after-verify-before-audit") {
189
+ return { ...result, interrupted: input.fault, pending: filtered.length };
190
+ }
191
+ if (durable.branch === "FAILED_CLOSED") {
192
+ result.failedClosed += 1;
193
+ result.processed += 1;
194
+ return undefined;
195
+ }
196
+ const alreadyAudited = auditFor(facts, entry.item);
197
+ if (durable.branch === "EXCLUDED_AUDITED") {
198
+ if (!alreadyAudited) {
199
+ await this.exclusions.recordExclusion({
200
+ attemptId: entry.item.attemptId,
201
+ tag: entry.tag,
202
+ eventId: entry.item.eventId,
203
+ suid: entry.item.suid,
204
+ actor: input.actor,
205
+ repairEpoch: lease.epoch,
206
+ });
207
+ }
208
+ result.excludedAudited += 1;
209
+ }
210
+ else {
211
+ result.rolledForward += 1;
212
+ }
213
+ if (!alreadyAudited) {
214
+ await this.audit(entry.tag, entry.item, lease, input.actor);
215
+ }
216
+ if (input.fault === "after-audit-before-clear") {
217
+ return { ...result, interrupted: input.fault, pending: filtered.length };
218
+ }
219
+ result.processed += 1;
220
+ return undefined;
221
+ };
222
+ const interrupted = trace === undefined
223
+ ? await executeItem()
224
+ : await trace.scope.fork().span("X03e", {
225
+ actorClass: "TAG",
226
+ actorKey: `tag:${this.serviceId}:${entry.tag}`,
227
+ attemptId: entry.item.attemptId,
228
+ memberIndex,
229
+ tag: entry.tag,
230
+ attributes: {
231
+ "repair.lease.id": leaseId,
232
+ repairEpoch: lease.epoch,
233
+ },
234
+ }, executeItem);
235
+ if (interrupted !== undefined)
236
+ return interrupted;
237
+ }
238
+ // A clear can happen only against a fresh, stable scope snapshot. The
239
+ // Tag DO rechecks every item/audit in the transaction that removes exactly
240
+ // one partial_write fence.
241
+ for (const tag of [...new Set(workset.map((entry) => entry.tag))]) {
242
+ const lease = leases.get(tag);
243
+ if (lease === undefined) {
244
+ continue;
245
+ }
246
+ const facts = await this.tagFacts(tag);
247
+ // A concurrently joined F2 must remain in the durable scope, but it
248
+ // must not block this bounded invocation from clearing its own exact
249
+ // F1 fence. Its later workset will use the same unioned authority.
250
+ const activeAttempts = [...new Set(workset
251
+ .filter((entry) => entry.tag === tag && hasPartialFence(facts, entry.item.attemptId))
252
+ .map((entry) => entry.item.attemptId))];
253
+ for (const attemptId of activeAttempts) {
254
+ const attemptScope = facts.repairScope.filter((item) => item.attemptId === attemptId);
255
+ // A bounded invocation returns its checkpoint with the fence still
256
+ // held until every item in the stable durable scope has a Tag-side
257
+ // resolution and audit. This is what makes resume safe.
258
+ if (attemptScope.some((item) => {
259
+ const resolution = resolutionFor(facts, item);
260
+ return resolution === undefined || resolution.branch === "FAILED_CLOSED" || !auditFor(facts, item);
261
+ })) {
262
+ continue;
263
+ }
264
+ const response = await this.tagPost(tag, "/repair/clear", {
265
+ owner: lease.owner,
266
+ epoch: lease.epoch,
267
+ attemptId,
268
+ scopeVersion: facts.repairScopeVersion,
269
+ });
270
+ if (!response.ok) {
271
+ const body = await responseBody(response);
272
+ throw new RepairWorkerFailure(`Tag repair clear rejected: ${body?.error ?? response.status}`);
273
+ }
274
+ result.cleared += 1;
275
+ }
276
+ }
277
+ if (input.fault === "after-clear-before-final-observation") {
278
+ return { ...result, interrupted: input.fault, pending: filtered.length };
279
+ }
280
+ // Completion display is likewise reconstructed from current Tag facts;
281
+ // an interrupted final Journal write cannot resurrect a cleared fence.
282
+ await this.recordClearedObservations(workset, leases);
283
+ return result;
284
+ }
285
+ async enumerate(attemptIds, tags) {
286
+ const result = [];
287
+ for (const attemptId of attemptIds) {
288
+ const response = await this.journalGet(attemptId, "/repair/workset");
289
+ if (!response.ok) {
290
+ const body = await responseBody(response);
291
+ throw new RepairWorkerFailure(`Repair workset rejected for ${attemptId}: ${body?.error ?? response.status}`);
292
+ }
293
+ const workset = await responseBody(response);
294
+ if (workset === undefined || !Array.isArray(workset.candidates) || !Array.isArray(workset.missingTags)) {
295
+ throw new RepairWorkerFailure(`Repair workset for ${attemptId} was malformed`);
296
+ }
297
+ for (const tag of tags) {
298
+ if (!workset.missingTags.includes(tag)) {
299
+ continue;
300
+ }
301
+ for (const candidate of workset.candidates) {
302
+ if (!candidate.tags.includes(tag)) {
303
+ continue;
304
+ }
305
+ if (candidate.allocatorLineageId === undefined) {
306
+ throw new RepairWorkerFailure(`Repair workset for ${attemptId} lacks its allocator lineage`);
307
+ }
308
+ result.push({
309
+ tag,
310
+ item: {
311
+ attemptId,
312
+ eventId: candidate.eventId,
313
+ suid: candidate.suid,
314
+ payload: candidate.payload,
315
+ eventTags: candidate.tags,
316
+ allocatorLineageId: candidate.allocatorLineageId,
317
+ eventType: candidate.eventType,
318
+ provenance: "g32",
319
+ timestamp: candidate.timestamp,
320
+ },
321
+ });
322
+ }
323
+ }
324
+ }
325
+ return sortTaggedItems(result);
326
+ }
327
+ async ensureLease(tag, input, workset, leases) {
328
+ const current = leases.get(tag);
329
+ if (current !== undefined) {
330
+ return current;
331
+ }
332
+ const scope = await this.activeScope(tag, workset);
333
+ if (scope.length === 0) {
334
+ throw new RepairWorkerFailure(`No durable partial_write fence remains for ${tag}`);
335
+ }
336
+ const acquire = await this.tagPost(tag, "/repair/acquire", { owner: input.owner, scope });
337
+ const body = await responseBody(acquire);
338
+ let lease;
339
+ if (acquire.status === 201 && typeof body?.epoch === "number") {
340
+ lease = { owner: input.owner, epoch: body.epoch };
341
+ }
342
+ else {
343
+ const facts = await this.tagFacts(tag);
344
+ if (facts.repairOwner !== input.owner || facts.repairLeaseUntil === null) {
345
+ throw new RepairWorkerFailure(`Repair lease is held by another owner for ${tag}: ${body?.error ?? acquire.status}`);
346
+ }
347
+ const renewed = await this.tagPost(tag, "/repair/renew", {
348
+ owner: input.owner,
349
+ epoch: facts.highestRepairEpoch,
350
+ });
351
+ if (!renewed.ok) {
352
+ const rejected = await responseBody(renewed);
353
+ throw new RepairWorkerFailure(`Repair lease cannot be renewed for ${tag}: ${rejected?.error ?? renewed.status}`);
354
+ }
355
+ lease = { owner: input.owner, epoch: facts.highestRepairEpoch };
356
+ }
357
+ await this.unionScope(tag, lease, workset.filter((entry) => entry.tag === tag));
358
+ leases.set(tag, lease);
359
+ return lease;
360
+ }
361
+ async activeScope(tag, workset) {
362
+ const facts = await this.tagFacts(tag);
363
+ const items = workset
364
+ .filter((entry) => entry.tag === tag && hasPartialFence(facts, entry.item.attemptId))
365
+ .map((entry) => entry.item);
366
+ return [...new Map(items.map((item) => [itemKey(item), item])).values()];
367
+ }
368
+ async unionScope(tag, lease, workset) {
369
+ const scope = await this.activeScope(tag, workset);
370
+ if (scope.length === 0) {
371
+ return;
372
+ }
373
+ const response = await this.tagPost(tag, "/repair/scope-union", { ...lease, scope });
374
+ if (!response.ok) {
375
+ const body = await responseBody(response);
376
+ throw new RepairWorkerFailure(`Repair scope union rejected: ${body?.error ?? response.status}`);
377
+ }
378
+ }
379
+ async apply(tag, item, lease) {
380
+ const response = await this.tagPost(tag, "/repair/apply", { ...lease, item });
381
+ const body = await responseBody(response);
382
+ return { status: response.status, reason: body?.reason ?? body?.error, branch: body?.status };
383
+ }
384
+ async audit(tag, item, lease, actor) {
385
+ const response = await this.tagPost(tag, "/repair/audit", { ...lease, item, actor });
386
+ if (!response.ok) {
387
+ const body = await responseBody(response);
388
+ throw new RepairWorkerFailure(`Repair audit rejected: ${body?.error ?? response.status}`);
389
+ }
390
+ }
391
+ async observe(tag, item, lease, phase, branch) {
392
+ const response = await this.journalPost(item.attemptId, "/repair/observation", {
393
+ owner: lease.owner,
394
+ epoch: lease.epoch,
395
+ tag,
396
+ attemptId: item.attemptId,
397
+ eventId: item.eventId,
398
+ suid: item.suid,
399
+ phase,
400
+ ...(branch === undefined ? {} : { branch }),
401
+ });
402
+ if (!response.ok) {
403
+ const body = await responseBody(response);
404
+ throw new RepairWorkerFailure(`Repair observation rejected: ${body?.error ?? response.status}`);
405
+ }
406
+ }
407
+ async recordClearedObservations(workset, leases) {
408
+ for (const entry of workset) {
409
+ const facts = await this.tagFacts(entry.tag);
410
+ const durable = resolutionFor(facts, entry.item);
411
+ if (durable !== undefined && durable.branch !== "FAILED_CLOSED" && auditFor(facts, entry.item) && !hasPartialFence(facts, entry.item.attemptId)) {
412
+ const lease = leases.get(entry.tag) ?? { owner: durable.owner, epoch: durable.epoch };
413
+ await this.observe(entry.tag, entry.item, lease, "CLEARED", durable.branch);
414
+ }
415
+ }
416
+ }
417
+ journalFor(attemptId) {
418
+ return this.env.JOURNAL.get(scopeIdFor(this.env.JOURNAL, {
419
+ serviceId: this.serviceId,
420
+ doClass: "journal",
421
+ identity: attemptId,
422
+ }));
423
+ }
424
+ tagFor(tag) {
425
+ return this.env.TAG.get(scopeIdFor(this.env.TAG, {
426
+ serviceId: this.serviceId,
427
+ doClass: "tag",
428
+ identity: tag,
429
+ }));
430
+ }
431
+ journalGet(attemptId, path) {
432
+ return this.journalFor(attemptId).fetch(`https://repair.internal${path}`);
433
+ }
434
+ journalPost(attemptId, path, body) {
435
+ return this.journalFor(attemptId).fetch(`https://repair.internal${path}`, {
436
+ method: "POST",
437
+ headers: { "content-type": "application/json" },
438
+ body: JSON.stringify(body),
439
+ });
440
+ }
441
+ async tagFacts(tag) {
442
+ const response = await this.tagFor(tag).fetch(`https://repair.internal/repair/facts?__tag=${encodeURIComponent(tag)}`);
443
+ if (!response.ok) {
444
+ const body = await responseBody(response);
445
+ throw new RepairWorkerFailure(`Tag facts rejected for ${tag}: ${body?.error ?? response.status}`);
446
+ }
447
+ const facts = await responseBody(response);
448
+ if (facts === undefined || facts.tag !== tag || !isObject(facts)) {
449
+ throw new RepairWorkerFailure(`Tag facts were malformed for ${tag}`);
450
+ }
451
+ return facts;
452
+ }
453
+ tagPost(tag, path, body) {
454
+ return this.tagFor(tag).fetch(`https://repair.internal${path}?__tag=${encodeURIComponent(tag)}`, {
455
+ method: "POST",
456
+ headers: { "content-type": "application/json" },
457
+ body: JSON.stringify(body),
458
+ });
459
+ }
460
+ }
461
+ export { RepairWorkerFailure };
@@ -0,0 +1,20 @@
1
+ /** Published read-side SafeWindow bounds from the SDT-G11 ruling. */
2
+ export declare const PUBLISHED_SAFE_WINDOW_MS = 20000;
3
+ export declare const MAX_PUBLISHED_SAFE_WINDOW_MS = 120000;
4
+ /**
5
+ * A lag sample is a current estimate, not a high-water mark. The estimate
6
+ * therefore loses one millisecond for every millisecond it has been idle and
7
+ * never falls below the published floor.
8
+ */
9
+ export declare function decayedLagEstimateMs(estimateMs: number, observedAtMs: number, nowMs: number): number;
10
+ /** Clamp a valid estimate to the wire-visible SafeWindow ceiling. */
11
+ export declare function safeWindowMs(dynamicLagBoundMs: number): number;
12
+ export declare function safeWindowCeilingExceeded(dynamicLagBoundMs: number): boolean;
13
+ /**
14
+ * C# parity utility only: build the SafeWindow frontier in .NET ticks rather
15
+ * than substituting the historical 5-second helper or a wall-clock string.
16
+ * The minimum random suffix makes equality include every event at the cutoff
17
+ * tick; callers compare only validated 30-digit SUIDs.
18
+ */
19
+ export declare function safeWindowCutoffSuid(nowMs: number, dynamicLagBoundMs: number): string;
20
+ export declare function isSortableUniqueIdSafeAt(suid: string, nowMs: number, dynamicLagBoundMs: number): boolean;
@@ -0,0 +1,36 @@
1
+ /** Published read-side SafeWindow bounds from the SDT-G11 ruling. */
2
+ export const PUBLISHED_SAFE_WINDOW_MS = 20_000;
3
+ export const MAX_PUBLISHED_SAFE_WINDOW_MS = 120_000;
4
+ /**
5
+ * A lag sample is a current estimate, not a high-water mark. The estimate
6
+ * therefore loses one millisecond for every millisecond it has been idle and
7
+ * never falls below the published floor.
8
+ */
9
+ export function decayedLagEstimateMs(estimateMs, observedAtMs, nowMs) {
10
+ const elapsed = Math.max(0, nowMs - observedAtMs);
11
+ return Math.max(0, estimateMs - elapsed);
12
+ }
13
+ /** Clamp a valid estimate to the wire-visible SafeWindow ceiling. */
14
+ export function safeWindowMs(dynamicLagBoundMs) {
15
+ return Math.min(MAX_PUBLISHED_SAFE_WINDOW_MS, Math.max(PUBLISHED_SAFE_WINDOW_MS, dynamicLagBoundMs));
16
+ }
17
+ export function safeWindowCeilingExceeded(dynamicLagBoundMs) {
18
+ return dynamicLagBoundMs > MAX_PUBLISHED_SAFE_WINDOW_MS;
19
+ }
20
+ /**
21
+ * C# parity utility only: build the SafeWindow frontier in .NET ticks rather
22
+ * than substituting the historical 5-second helper or a wall-clock string.
23
+ * The minimum random suffix makes equality include every event at the cutoff
24
+ * tick; callers compare only validated 30-digit SUIDs.
25
+ */
26
+ export function safeWindowCutoffSuid(nowMs, dynamicLagBoundMs) {
27
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0)
28
+ throw new Error("SafeWindow now must be a non-negative safe integer");
29
+ const cutoffMs = Math.max(0, nowMs - safeWindowMs(dynamicLagBoundMs));
30
+ return formatSortableUniqueId(unixMsToDotNetTicks(cutoffMs), 0n);
31
+ }
32
+ export function isSortableUniqueIdSafeAt(suid, nowMs, dynamicLagBoundMs) {
33
+ assertSortableUniqueId(suid);
34
+ return compareSortableUniqueId(suid, safeWindowCutoffSuid(nowMs, dynamicLagBoundMs)) <= 0;
35
+ }
36
+ import { assertSortableUniqueId, compareSortableUniqueId, formatSortableUniqueId, unixMsToDotNetTicks, } from "./allocator/SortableUniqueId";
@@ -0,0 +1,22 @@
1
+ import { type ServiceIdentityProvider, type ServiceIdentityRequestOptions } from "../service/ServiceIdentityProvider";
2
+ export type ScopedControlRoute = "bootstrap" | "tag" | "operator-bootstrap";
3
+ export interface ScopedControlRouteInput {
4
+ readonly request: Request;
5
+ readonly provider: ServiceIdentityProvider;
6
+ readonly pathServiceId: string;
7
+ readonly route: ScopedControlRoute;
8
+ readonly requestOptions?: ServiceIdentityRequestOptions;
9
+ }
10
+ export type ScopedControlRouteResult = Readonly<{
11
+ readonly serviceId: string;
12
+ }> | Readonly<{
13
+ readonly response: Response;
14
+ }>;
15
+ /** A typed platform-fault response; no control route may invent an identity. */
16
+ export declare function scopeIdentityMissingResponse(): Response;
17
+ /**
18
+ * Checks a path-owned identity before the caller can obtain a Durable Object
19
+ * stub. Logs intentionally retain only the failure class and route, never the
20
+ * configured deployment identity.
21
+ */
22
+ export declare function enforceControlRouteScope(input: ScopedControlRouteInput): ScopedControlRouteResult;
@@ -0,0 +1,36 @@
1
+ import { ServiceIdentityMissingError, } from "../service/ServiceIdentityProvider";
2
+ function response(status, code, error) {
3
+ return new Response(JSON.stringify({ code, error }), {
4
+ status,
5
+ headers: { "content-type": "application/json; charset=utf-8" },
6
+ });
7
+ }
8
+ /** A typed platform-fault response; no control route may invent an identity. */
9
+ export function scopeIdentityMissingResponse() {
10
+ return response(503, "scope.identity_missing", "A deployment service identity is required for this scope");
11
+ }
12
+ /**
13
+ * Checks a path-owned identity before the caller can obtain a Durable Object
14
+ * stub. Logs intentionally retain only the failure class and route, never the
15
+ * configured deployment identity.
16
+ */
17
+ export function enforceControlRouteScope(input) {
18
+ let actual;
19
+ try {
20
+ actual = input.provider.forRequest(input.request, input.requestOptions).serviceId;
21
+ }
22
+ catch (caught) {
23
+ if (caught instanceof ServiceIdentityMissingError) {
24
+ console.error({ schema: "sdt.scope/v1", code: "scope.identity_missing", route: input.route });
25
+ return { response: scopeIdentityMissingResponse() };
26
+ }
27
+ throw caught;
28
+ }
29
+ if (input.pathServiceId !== actual) {
30
+ console.warn({ schema: "sdt.scope/v1", code: "scope.mismatch", route: input.route });
31
+ return {
32
+ response: response(403, "scope.mismatch", "The path service identity is not authorized for this deployment"),
33
+ };
34
+ }
35
+ return { serviceId: actual };
36
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The one durable-object physical-name grammar used by the runtime.
3
+ *
4
+ * A scope name is deliberately not an application protocol value: C-0 makes
5
+ * old Durable Object instances unreachable when this grammar changes. The
6
+ * service, class, and identity parts remain individually inspectable while
7
+ * `/` is the only grammar separator.
8
+ */
9
+ export declare const DURABLE_OBJECT_SCOPE_CLASSES: readonly ["tag", "tag-state", "allocator", "bootstrap", "journal"];
10
+ export type DurableObjectScopeClass = (typeof DURABLE_OBJECT_SCOPE_CLASSES)[number];
11
+ export interface DurableObjectScope {
12
+ readonly serviceId: string;
13
+ readonly doClass: DurableObjectScopeClass;
14
+ readonly identity: string;
15
+ }
16
+ export declare class ScopeNameError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ /** The existing deploy-time SDT_SERVICE_ID grammar, shared by scope names. */
20
+ export declare function isScopeServiceId(value: unknown): value is string;
21
+ /** Builds the canonical `${serviceId}/${doClass}/${identity}` physical name. */
22
+ export declare function buildScopeName(scope: DurableObjectScope): string;
23
+ /** Parses and fully validates one canonical Durable Object physical name. */
24
+ export declare function parseScopeName(name: string): DurableObjectScope;
25
+ /**
26
+ * Keep the platform namespace call in this module so all runtime call sites
27
+ * first pass through the one public grammar above.
28
+ */
29
+ export interface ScopeNameNamespace<Id> {
30
+ idFromName(name: string): Id;
31
+ }
32
+ export declare function scopeIdFor<Id>(namespace: ScopeNameNamespace<Id>, scope: DurableObjectScope): Id;
33
+ /** A stable component encoding inside the single opaque tag-state identity. */
34
+ export declare function tagStateScopeIdentity(tag: string, projectorId: string): string;