@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,346 @@
1
+ import { DEPLOYED_PROJECTOR_REGISTRY, projectionEventFromTagEvent, } from "./ProjectorRegistry";
2
+ import { decayedLagEstimateMs, MAX_PUBLISHED_SAFE_WINDOW_MS, PUBLISHED_SAFE_WINDOW_MS, safeWindowCeilingExceeded, safeWindowCutoffSuid, isSortableUniqueIdSafeAt, safeWindowMs, } from "../safeWindow";
3
+ import { assertSortableUniqueId, isSortableUniqueId } from "../allocator/SortableUniqueId";
4
+ /** Published V1 read-side SafeWindow. It never authorizes a commit. */
5
+ export { MAX_PUBLISHED_SAFE_WINDOW_MS, PUBLISHED_SAFE_WINDOW_MS, safeWindowCeilingExceeded, safeWindowCutoffSuid, safeWindowMs, };
6
+ const MAX_CHECKPOINT_CAS_RETRIES = 8;
7
+ export class ProjectionCheckpointCorruption extends Error {
8
+ constructor(cause) {
9
+ super("Projection checkpoint state failed validation");
10
+ this.name = "ProjectionCheckpointCorruption";
11
+ if (cause !== undefined)
12
+ this.cause = cause;
13
+ }
14
+ }
15
+ /**
16
+ * Live projection identities have independent checkpoints. A bounded worker
17
+ * pool prevents a large retained tag set from monopolizing one cron
18
+ * invocation while keeping D1/CAS pressure finite and deterministic.
19
+ */
20
+ export const MAX_LIVE_PROJECTION_CONCURRENCY = 8;
21
+ /**
22
+ * Validate the allocator fact at the one boundary that decides whether a
23
+ * safe view may advance. Callers that only inspect, rebuild, or perform an
24
+ * ordinary SafeWindow catch-up must not call this function.
25
+ */
26
+ export function validatedClosedPrefixSuid(options, consumerServiceId) {
27
+ const certificate = options.closedPrefixCertificate;
28
+ if (typeof consumerServiceId !== "string" ||
29
+ consumerServiceId.length === 0 ||
30
+ typeof certificate !== "object" ||
31
+ certificate === null ||
32
+ Array.isArray(certificate)) {
33
+ throw new Error("ordering_certificate_unavailable");
34
+ }
35
+ if (certificate.certificateVersion !== 1 ||
36
+ certificate.authority !== "allocator-transaction" ||
37
+ certificate.status !== "ready" ||
38
+ typeof certificate.serviceId !== "string" ||
39
+ certificate.serviceId.length === 0 ||
40
+ typeof certificate.allocatorLineageId !== "string" ||
41
+ certificate.allocatorLineageId.length === 0 ||
42
+ !Number.isSafeInteger(certificate.unresolvedCount) ||
43
+ certificate.unresolvedCount < 0 ||
44
+ (certificate.closedPrefixSuid !== null && !isSortableUniqueId(certificate.closedPrefixSuid))) {
45
+ throw new Error("ordering_certificate_unavailable");
46
+ }
47
+ if (options.expectedServiceId === undefined || options.expectedAllocatorLineageId === undefined) {
48
+ throw new Error("ordering_certificate_consumer_unbound");
49
+ }
50
+ if (options.expectedServiceId !== consumerServiceId || certificate.serviceId !== consumerServiceId) {
51
+ throw new Error("ordering_certificate_consumer_mismatch");
52
+ }
53
+ if (certificate.allocatorLineageId !== options.expectedAllocatorLineageId) {
54
+ throw new Error("ordering_certificate_lineage_mismatch");
55
+ }
56
+ if (options.closedPrefixSuid !== undefined &&
57
+ options.closedPrefixSuid !== certificate.closedPrefixSuid) {
58
+ throw new Error("ordering_certificate_mismatch");
59
+ }
60
+ return certificate.closedPrefixSuid;
61
+ }
62
+ /**
63
+ * Validate the existing completeness handoff independently of the allocator
64
+ * certificate. The returned bound is deliberately separate from
65
+ * `maximumSuid`, whose G44 stop condition remains an independent gate.
66
+ */
67
+ export function validatedSafeViewCoverageMaximumSuid(context, consumerServiceId) {
68
+ if (typeof consumerServiceId !== "string" ||
69
+ consumerServiceId.length === 0 ||
70
+ typeof context !== "object" ||
71
+ context === null ||
72
+ Array.isArray(context)) {
73
+ throw new Error("ordering_coverage_unavailable");
74
+ }
75
+ if (context.authority !== "g44-g62" ||
76
+ context.startOfPass !== "PROVEN" ||
77
+ context.serviceId !== consumerServiceId ||
78
+ typeof context.serviceId !== "string" ||
79
+ context.serviceId.length === 0 ||
80
+ !["FULL", "SETTLED", "BLOCK/UNSETTLED"].includes(context.kind) ||
81
+ (context.frontierSuid !== null && !isSortableUniqueId(context.frontierSuid))) {
82
+ throw new Error("ordering_coverage_unavailable");
83
+ }
84
+ if (context.kind === "FULL") {
85
+ if (context.frontierSuid !== null)
86
+ throw new Error("ordering_coverage_unavailable");
87
+ return undefined;
88
+ }
89
+ return context.frontierSuid;
90
+ }
91
+ function compareSuid(left, right) {
92
+ const leftBytes = new TextEncoder().encode(left);
93
+ const rightBytes = new TextEncoder().encode(right);
94
+ const shared = Math.min(leftBytes.length, rightBytes.length);
95
+ for (let index = 0; index < shared; index += 1) {
96
+ const difference = leftBytes[index] - rightBytes[index];
97
+ if (difference !== 0) {
98
+ return difference;
99
+ }
100
+ }
101
+ return leftBytes.length - rightBytes.length;
102
+ }
103
+ function orderViolationIncident(serviceId, event, previousSuid) {
104
+ return {
105
+ serviceId,
106
+ identityKey: `ORDER_VIOLATION|${serviceId}|${previousSuid}|${event.suid}|${event.eventId}`,
107
+ classification: "ORDER_VIOLATION",
108
+ suid: event.suid,
109
+ eventId: event.eventId,
110
+ observedAt: event.lastArrivedAt,
111
+ };
112
+ }
113
+ function stateFromCheckpoint(projector, checkpoint) {
114
+ if (checkpoint === undefined)
115
+ return projector.initialState();
116
+ try {
117
+ return projector.deserializeState(checkpoint.stateJson);
118
+ }
119
+ catch (error) {
120
+ throw new ProjectionCheckpointCorruption(error);
121
+ }
122
+ }
123
+ function projectionEventFromStored(event) {
124
+ assertSortableUniqueId(event.suid);
125
+ return {
126
+ eventId: event.eventId,
127
+ suid: event.suid,
128
+ payload: event.payload,
129
+ eventTags: event.eventTags,
130
+ eventType: event.eventType,
131
+ provenance: "g32",
132
+ };
133
+ }
134
+ function checkpointFor(serviceId, projectionId, event, state, projector, updatedAt) {
135
+ return {
136
+ serviceId,
137
+ projectionId,
138
+ lastSuid: event.suid,
139
+ stateJson: projector.serializeState(state),
140
+ version: projector.version(state),
141
+ updatedAt,
142
+ };
143
+ }
144
+ export function projectionIdFor(identity) {
145
+ return `tag-state:${identity.tagGroup}:${identity.tagContent}:${identity.tagProjector}`;
146
+ }
147
+ export { decayedLagEstimateMs };
148
+ /**
149
+ * Deterministically rebuilds one tag-state from the Tag DO's authoritative
150
+ * durable event list. This is the on-demand §5.3 catch-up path; it does not
151
+ * mutate the read-side SafeWindow checkpoint.
152
+ */
153
+ export function catchUpDurableTagState(projector, events) {
154
+ const ordered = events.map(projectionEventFromTagEvent).sort((left, right) => compareSuid(left.suid, right.suid));
155
+ let state = projector.initialState();
156
+ let previousSuid;
157
+ for (const event of ordered) {
158
+ if (previousSuid !== undefined && compareSuid(previousSuid, event.suid) >= 0) {
159
+ throw new Error("Durable tag event history was not strictly SUID ordered");
160
+ }
161
+ state = projector.apply(state, event);
162
+ previousSuid = event.suid;
163
+ }
164
+ return {
165
+ payload: projector.payload(state),
166
+ version: projector.version(state),
167
+ stateJson: projector.serializeState(state),
168
+ };
169
+ }
170
+ /**
171
+ * Polls a global SUID source for one tag/projector. Every source event advances
172
+ * the durable checkpoint, while the registered reducer changes state only for
173
+ * events whose complete durable tag membership includes the target tag.
174
+ */
175
+ export class ProjectionRuntime {
176
+ store;
177
+ registry;
178
+ constructor(store, registry = DEPLOYED_PROJECTOR_REGISTRY) {
179
+ this.store = store;
180
+ this.registry = registry;
181
+ }
182
+ async catchUp(serviceId, identity, nowMs, hooks = {}, options = {}) {
183
+ const projector = this.registry.resolve(identity.tagProjector);
184
+ if (projector === undefined) {
185
+ throw new Error(`Projector ${identity.tagProjector} is not registered`);
186
+ }
187
+ const safeViewAdvance = options.requireClosedPrefixCertificate === true;
188
+ const certifiedClosedPrefixSuid = safeViewAdvance
189
+ ? validatedClosedPrefixSuid(options, serviceId)
190
+ : undefined;
191
+ const coverageMaximumSuid = safeViewAdvance
192
+ ? validatedSafeViewCoverageMaximumSuid(options.safeViewCoverage, serviceId)
193
+ : undefined;
194
+ const dynamicLagBoundMs = await this.store.currentLagBound(serviceId, nowMs);
195
+ const windowMs = safeWindowMs(dynamicLagBoundMs);
196
+ if (safeWindowCeilingExceeded(dynamicLagBoundMs)) {
197
+ const projectionId = projectionIdFor(identity);
198
+ return {
199
+ projectorId: identity.tagProjector,
200
+ tag: identity.tag,
201
+ checkpoint: await this.store.readProjectionCheckpoint(serviceId, projectionId),
202
+ dynamicLagBoundMs,
203
+ safeWindowMs: windowMs,
204
+ indeterminate: true,
205
+ advancedSourceEvents: 0,
206
+ appliedEvents: 0,
207
+ };
208
+ }
209
+ const projectionId = projectionIdFor(identity);
210
+ for (let retry = 0; retry < MAX_CHECKPOINT_CAS_RETRIES; retry += 1) {
211
+ let checkpoint = await this.store.readProjectionCheckpoint(serviceId, projectionId);
212
+ let state = stateFromCheckpoint(projector, checkpoint);
213
+ const sourceEvents = await this.store.readAllEvents(serviceId, checkpoint?.lastSuid ?? "");
214
+ let advancedSourceEvents = 0;
215
+ let appliedEvents = 0;
216
+ let casConflict = false;
217
+ let previousSuid = checkpoint?.lastSuid;
218
+ for (const event of sourceEvents) {
219
+ if (previousSuid !== undefined && compareSuid(previousSuid, event.suid) >= 0) {
220
+ await this.store.appendDeliveryIncident(orderViolationIncident(serviceId, event, previousSuid));
221
+ throw new Error("Projection source was not strictly SUID ordered");
222
+ }
223
+ if (certifiedClosedPrefixSuid === null || (certifiedClosedPrefixSuid !== undefined && compareSuid(event.suid, certifiedClosedPrefixSuid) > 0)) {
224
+ break;
225
+ }
226
+ // A G44 BLOCK/UNSETTLED tick may still poll live projections, but it
227
+ // must remain fenced by the last settled source frontier. `null`
228
+ // deliberately permits no source advancement; it is not an empty
229
+ // unbounded frontier.
230
+ if (options.maximumSuid === null || (options.maximumSuid !== undefined && compareSuid(event.suid, options.maximumSuid) > 0)) {
231
+ break;
232
+ }
233
+ if (safeViewAdvance && (coverageMaximumSuid === null || (coverageMaximumSuid !== undefined && compareSuid(event.suid, coverageMaximumSuid) > 0))) {
234
+ break;
235
+ }
236
+ // Stop at the first unsafe source event. Because the source is SUID
237
+ // ordered, advancing past it could skip a delayed lower SUID.
238
+ assertSortableUniqueId(event.suid);
239
+ if (!isSortableUniqueIdSafeAt(event.suid, nowMs, dynamicLagBoundMs)) {
240
+ return {
241
+ projectorId: identity.tagProjector,
242
+ tag: identity.tag,
243
+ checkpoint,
244
+ dynamicLagBoundMs,
245
+ safeWindowMs: windowMs,
246
+ indeterminate: false,
247
+ advancedSourceEvents,
248
+ appliedEvents,
249
+ };
250
+ }
251
+ const appliesToTag = event.eventTags.includes(identity.tag);
252
+ const nextState = appliesToTag ? projector.apply(state, projectionEventFromStored(event)) : state;
253
+ const nextCheckpoint = checkpointFor(serviceId, projectionId, event, nextState, projector, nowMs);
254
+ await hooks.beforeCheckpoint?.(event, nextCheckpoint);
255
+ const advanced = await this.store.advanceProjectionCheckpoint({
256
+ ...nextCheckpoint,
257
+ expectedLastSuid: checkpoint?.lastSuid ?? null,
258
+ });
259
+ if (!advanced) {
260
+ casConflict = true;
261
+ break;
262
+ }
263
+ checkpoint = nextCheckpoint;
264
+ state = nextState;
265
+ previousSuid = event.suid;
266
+ advancedSourceEvents += 1;
267
+ if (appliesToTag) {
268
+ appliedEvents += 1;
269
+ }
270
+ await hooks.afterCheckpoint?.(event, nextCheckpoint);
271
+ }
272
+ if (!casConflict) {
273
+ return {
274
+ projectorId: identity.tagProjector,
275
+ tag: identity.tag,
276
+ checkpoint,
277
+ dynamicLagBoundMs,
278
+ safeWindowMs: windowMs,
279
+ indeterminate: false,
280
+ advancedSourceEvents,
281
+ appliedEvents,
282
+ };
283
+ }
284
+ }
285
+ throw new Error("Projection checkpoint did not converge after concurrent updates");
286
+ }
287
+ async pollRegistered(serviceId, nowMs, maximumSuid, closedPrefixSuid, closedPrefixCertificate, requireClosedPrefixCertificate = false, expectedServiceId, expectedAllocatorLineageId, safeViewCoverage) {
288
+ const safeViewAdvance = requireClosedPrefixCertificate === true;
289
+ if (safeViewAdvance) {
290
+ validatedClosedPrefixSuid({
291
+ closedPrefixSuid,
292
+ closedPrefixCertificate,
293
+ expectedServiceId,
294
+ expectedAllocatorLineageId,
295
+ }, serviceId);
296
+ validatedSafeViewCoverageMaximumSuid(safeViewCoverage, serviceId);
297
+ }
298
+ const tags = await this.store.listProjectionTags(serviceId);
299
+ const jobs = [];
300
+ for (const tag of tags) {
301
+ for (const projector of this.registry.registered()) {
302
+ if (tagStateIdentityForPolledTag(tag, projector.id, this.registry) !== undefined)
303
+ jobs.push({ tag, projector: projector.id });
304
+ }
305
+ }
306
+ const results = [];
307
+ let next = 0;
308
+ const worker = async () => {
309
+ for (;;) {
310
+ const index = next;
311
+ next += 1;
312
+ const job = jobs[index];
313
+ if (job === undefined)
314
+ return;
315
+ const identity = tagStateIdentityForPolledTag(job.tag, job.projector, this.registry);
316
+ if (identity !== undefined) {
317
+ const options = safeViewAdvance
318
+ ? {
319
+ maximumSuid,
320
+ closedPrefixSuid,
321
+ closedPrefixCertificate,
322
+ requireClosedPrefixCertificate: true,
323
+ expectedServiceId,
324
+ expectedAllocatorLineageId,
325
+ safeViewCoverage,
326
+ }
327
+ : { maximumSuid };
328
+ results[index] = await this.catchUp(serviceId, identity, nowMs, {}, options);
329
+ }
330
+ }
331
+ };
332
+ const workerCount = Math.min(MAX_LIVE_PROJECTION_CONCURRENCY, jobs.length);
333
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
334
+ return results;
335
+ }
336
+ }
337
+ function tagStateIdentityForPolledTag(tag, projector, registry) {
338
+ const parts = tag.split(":");
339
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
340
+ return undefined;
341
+ }
342
+ const [tagGroup, tagContent] = parts;
343
+ return registry.resolve(projector) === undefined
344
+ ? undefined
345
+ : { tag, tagGroup: tagGroup, tagContent: tagContent, tagProjector: projector };
346
+ }
@@ -0,0 +1,51 @@
1
+ import type { TagEvent } from "../tag/types";
2
+ export declare const TEST_TAG_STATE_PROJECTOR = "test-projector";
3
+ export interface ProjectionEvent {
4
+ eventId: string;
5
+ suid: string;
6
+ payload: string;
7
+ eventTags: readonly string[];
8
+ eventType: string;
9
+ provenance: "g32";
10
+ }
11
+ /**
12
+ * Projectors are TypeScript values registered at deploy time. The HTTP input
13
+ * can select only an already registered ID; it can never install code or a
14
+ * reducer dynamically.
15
+ */
16
+ export interface TagStateProjector {
17
+ readonly id: string;
18
+ readonly tagPayloadName: string;
19
+ readonly projectorVersion: string;
20
+ initialState(): unknown;
21
+ apply(state: unknown, event: ProjectionEvent): unknown;
22
+ serializeState(state: unknown): string;
23
+ deserializeState(serialized: string): unknown;
24
+ payload(state: unknown): string;
25
+ version(state: unknown): number;
26
+ }
27
+ export interface TagStateIdentity {
28
+ tag: string;
29
+ tagGroup: string;
30
+ tagContent: string;
31
+ tagProjector: string;
32
+ }
33
+ export declare class ProjectorRegistry {
34
+ private readonly projectors;
35
+ constructor(projectors: readonly TagStateProjector[]);
36
+ resolve(projectorId: string): TagStateProjector | undefined;
37
+ registered(): readonly TagStateProjector[];
38
+ }
39
+ /** The test-only default registry; production consumers compose their own domain. */
40
+ export declare const DEPLOYED_TAG_STATE_PROJECTORS: readonly TagStateProjector[];
41
+ export declare const DEPLOYED_PROJECTOR_REGISTRY: ProjectorRegistry;
42
+ /** Parse the public tag-state identifier without consulting a projector authority. */
43
+ export declare function tagStateIdentitySyntaxFrom(tagStateId: string): {
44
+ value?: TagStateIdentity;
45
+ error?: string;
46
+ };
47
+ export declare function tagStateIdentityFrom(tagStateId: string, registry?: ProjectorRegistry): {
48
+ value?: TagStateIdentity;
49
+ error?: string;
50
+ };
51
+ export declare function projectionEventFromTagEvent(event: TagEvent): ProjectionEvent;
@@ -0,0 +1,116 @@
1
+ import { assertSortableUniqueId } from "../allocator/SortableUniqueId";
2
+ export const TEST_TAG_STATE_PROJECTOR = "test-projector";
3
+ function isNonEmptyString(value) {
4
+ return typeof value === "string" && value.length > 0;
5
+ }
6
+ function base64Json(value) {
7
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
8
+ let binary = "";
9
+ for (const byte of bytes) {
10
+ binary += String.fromCharCode(byte);
11
+ }
12
+ return btoa(binary);
13
+ }
14
+ function eventHistoryStateFrom(value) {
15
+ if (!Array.isArray(value) ||
16
+ !value.every((entry) => typeof entry === "object" && entry !== null &&
17
+ isNonEmptyString(entry.eventId) &&
18
+ typeof entry.payload === "string" &&
19
+ isNonEmptyString(entry.suid))) {
20
+ throw new Error("Event-history projector durable state was malformed");
21
+ }
22
+ return value.map((entry) => {
23
+ const record = entry;
24
+ return {
25
+ eventId: record.eventId,
26
+ payload: record.payload,
27
+ suid: record.suid,
28
+ };
29
+ });
30
+ }
31
+ function eventHistoryProjector(id, tagPayloadName) {
32
+ return {
33
+ id,
34
+ tagPayloadName,
35
+ projectorVersion: "1",
36
+ initialState: () => [],
37
+ apply(state, event) {
38
+ const current = eventHistoryStateFrom(state);
39
+ return [...current, { eventId: event.eventId, payload: event.payload, suid: event.suid }];
40
+ },
41
+ serializeState(state) {
42
+ return JSON.stringify(eventHistoryStateFrom(state));
43
+ },
44
+ deserializeState(serialized) {
45
+ return eventHistoryStateFrom(JSON.parse(serialized));
46
+ },
47
+ payload(state) {
48
+ return base64Json(eventHistoryStateFrom(state));
49
+ },
50
+ version(state) {
51
+ return eventHistoryStateFrom(state).length;
52
+ },
53
+ };
54
+ }
55
+ const TEST_PROJECTOR = eventHistoryProjector(TEST_TAG_STATE_PROJECTOR, "SerializedDcbTestTagState");
56
+ export class ProjectorRegistry {
57
+ projectors;
58
+ constructor(projectors) {
59
+ const entries = new Map();
60
+ for (const projector of projectors) {
61
+ if (entries.has(projector.id)) {
62
+ throw new Error(`Projector ${projector.id} was registered more than once`);
63
+ }
64
+ entries.set(projector.id, projector);
65
+ }
66
+ this.projectors = entries;
67
+ }
68
+ resolve(projectorId) {
69
+ return this.projectors.get(projectorId);
70
+ }
71
+ registered() {
72
+ return [...this.projectors.values()];
73
+ }
74
+ }
75
+ /** The test-only default registry; production consumers compose their own domain. */
76
+ export const DEPLOYED_TAG_STATE_PROJECTORS = [
77
+ TEST_PROJECTOR,
78
+ ];
79
+ export const DEPLOYED_PROJECTOR_REGISTRY = new ProjectorRegistry(DEPLOYED_TAG_STATE_PROJECTORS);
80
+ /** Parse the public tag-state identifier without consulting a projector authority. */
81
+ export function tagStateIdentitySyntaxFrom(tagStateId) {
82
+ const parts = tagStateId.split(":");
83
+ if (parts.length !== 3 || !parts.every(isNonEmptyString)) {
84
+ return { error: "tagStateId must be group:content:projector" };
85
+ }
86
+ const [tagGroup, tagContent, tagProjector] = parts;
87
+ return {
88
+ value: {
89
+ tag: `${tagGroup}:${tagContent}`,
90
+ tagGroup: tagGroup,
91
+ tagContent: tagContent,
92
+ tagProjector: tagProjector,
93
+ },
94
+ };
95
+ }
96
+ export function tagStateIdentityFrom(tagStateId, registry = DEPLOYED_PROJECTOR_REGISTRY) {
97
+ const parsed = tagStateIdentitySyntaxFrom(tagStateId);
98
+ if (parsed.value === undefined)
99
+ return parsed;
100
+ const { tagProjector } = parsed.value;
101
+ if (registry.resolve(tagProjector) === undefined) {
102
+ return { error: "tagStateId names an unregistered projector" };
103
+ }
104
+ return parsed;
105
+ }
106
+ export function projectionEventFromTagEvent(event) {
107
+ assertSortableUniqueId(event.suid);
108
+ return {
109
+ eventId: event.eventId,
110
+ suid: event.suid,
111
+ payload: event.payload,
112
+ eventTags: event.eventTags,
113
+ eventType: event.eventType,
114
+ provenance: "g32",
115
+ };
116
+ }
@@ -0,0 +1,102 @@
1
+ import type { ProjectionStore } from "../store/types";
2
+ import type { QueryDefinition } from "./QueryRegistry";
3
+ import type { MaterializedViewListOptions, MaterializedViewListPage, MaterializedViewQueryOptions, MaterializedViewOrderingQuarantine, MaterializedViewRow, MaterializedViewWaitForState } from "../mv/MaterializedViewStore";
4
+ import type { UnsafeComposedPage } from "../mv/UnsafeWindowMaterializedView";
5
+ /** The query surface can only inspect durable source and projection facts. */
6
+ export type QueryProjectionStore = Pick<ProjectionStore, "readAllEvents" | "currentLagBound" | "listProjectionTags" | "readProjectionCheckpoint">;
7
+ /**
8
+ * SDT-G31's source-side wait lookup. A D1 implementation must distinguish a
9
+ * missing target from a contradictory target/incident; callers must never
10
+ * turn either unavailable result into a successful safe-head observation.
11
+ */
12
+ export type WaitForTargetLookup = {
13
+ readonly kind: "pending";
14
+ } | {
15
+ readonly kind: "stored";
16
+ readonly eventId: string;
17
+ readonly suid: string;
18
+ } | {
19
+ readonly kind: "unavailable";
20
+ readonly reason: "incident" | "suid-contradiction";
21
+ };
22
+ /** Indexed D1 source port used only by the d1-mv waitFor implementation. */
23
+ export interface WaitForTargetSourcePort {
24
+ readWaitForTarget(serviceId: string, suid: string): Promise<WaitForTargetLookup>;
25
+ }
26
+ /** The backing choice is typed and deploy-time; it never changes the V1 wire. */
27
+ export type QueryBacking = "memory" | "d1-mv";
28
+ export interface MaterializedViewQueryPort {
29
+ queryRows(serviceId: string, viewId: string, options?: MaterializedViewQueryOptions): Promise<MaterializedViewRow[]>;
30
+ /** Optional SDT-G23 port. D1 provides one-statement winner/count paging. */
31
+ queryRowsWithTotal?(serviceId: string, viewId: string, options?: MaterializedViewQueryOptions): Promise<UnsafeComposedPage>;
32
+ /** SDT-G55's explicit safe/unsafe list port; absent ports retain legacy behavior. */
33
+ readListPage?(serviceId: string, viewId: string, options?: MaterializedViewListOptions): Promise<MaterializedViewListPage>;
34
+ /** Target receipt is the unsafe-window wait oracle; global heads are not. */
35
+ hasTargetReceipt?(serviceId: string, viewId: string, eventId: string, suid: string): Promise<boolean>;
36
+ /** SDT-G24 active-generation finding; true means every composed read is unavailable. */
37
+ hasCheckpointAheadFinding?(serviceId: string, viewId: string): Promise<boolean>;
38
+ /** G69 fail-closed ordering gate; only the active generation can refuse safe reads. */
39
+ readOrderingQuarantine?(serviceId: string, viewId: string): Promise<MaterializedViewOrderingQuarantine | undefined>;
40
+ /** Generation snapshot paired with the final safe-read boundary check. */
41
+ readActiveGeneration?(serviceId: string, viewId: string): Promise<number | undefined>;
42
+ /**
43
+ * SDT-G31 active-generation wait facts. This deliberately combines only
44
+ * indexed point reads; its receipt is generation/definition-bound and its
45
+ * hard gates are evaluated before a wait can report success.
46
+ */
47
+ readWaitForState?(serviceId: string, viewId: string, target: {
48
+ readonly eventId?: string;
49
+ readonly suid: string;
50
+ }): Promise<MaterializedViewWaitForState>;
51
+ /** D1-backed implementations may need to verify the versioned schema first. */
52
+ initialize?: () => Promise<void>;
53
+ }
54
+ export type QueryBackingSelection = {
55
+ readonly backing: "memory";
56
+ readonly store: QueryProjectionStore;
57
+ } | {
58
+ readonly backing: "d1-mv";
59
+ readonly store: MaterializedViewQueryPort;
60
+ };
61
+ export interface QueryBackingOptions {
62
+ readonly backing: QueryBacking;
63
+ readonly memory?: QueryProjectionStore;
64
+ readonly materializedView?: MaterializedViewQueryPort;
65
+ }
66
+ /** Select a query backing without allowing an HTTP request to provide a store. */
67
+ export declare function selectQueryBacking(options: QueryBackingOptions): QueryBackingSelection;
68
+ export declare const chooseQueryBacking: typeof selectQueryBacking;
69
+ export interface ProjectedQueryEntry {
70
+ readonly eventId: string;
71
+ readonly suid: string;
72
+ readonly payload: string;
73
+ }
74
+ /** V1 SUID ordering is bytewise ordinal, not locale or parsed timestamp order. */
75
+ export declare function compareSuid(left: string, right: string): number;
76
+ export interface ProjectedQuerySnapshot {
77
+ readonly entries: readonly ProjectedQueryEntry[];
78
+ /** Greatest checkpoint observed while constructing these exact entries. */
79
+ readonly readHead: string;
80
+ }
81
+ /**
82
+ * Builds a query snapshot exclusively from durable read-side checkpoints.
83
+ * A recently appended tag with no safe checkpoint is deliberately absent,
84
+ * which keeps an already-open page window stable while SafeWindow holds it.
85
+ */
86
+ export declare function readProjectedSnapshot(store: QueryProjectionStore, serviceId: string, definition: QueryDefinition): Promise<ProjectedQuerySnapshot>;
87
+ export declare function readProjectedEntries(store: QueryProjectionStore, serviceId: string, definition: QueryDefinition): Promise<ProjectedQueryEntry[]>;
88
+ /** A requested SUID is observed only after a relevant durable source event and
89
+ * its mapped tag projector checkpoint both prove that observation. A mere
90
+ * source-row arrival is not enough to fabricate a query result.
91
+ */
92
+ export declare function projectionHasObserved(store: QueryProjectionStore, serviceId: string, definition: QueryDefinition, requestedSuid: string): Promise<boolean>;
93
+ /** Query rows through the selected backing while preserving typed paging inputs. */
94
+ export declare function readRowsFromBacking(selection: QueryBackingSelection, serviceId: string, viewId: string, definition: QueryDefinition, options?: MaterializedViewQueryOptions): Promise<ProjectedQueryEntry[]>;
95
+ /** Page in the backing when it supports composed SQL, otherwise preserve the legacy port. */
96
+ export declare function readRowsPageFromBacking(selection: QueryBackingSelection, serviceId: string, viewId: string, definition: QueryDefinition, options: MaterializedViewQueryOptions): Promise<{
97
+ readonly entries: ProjectedQueryEntry[];
98
+ readonly totalCount: number;
99
+ readonly serverPaged: boolean;
100
+ /** Present for memory pages and observed from the same checkpoints as entries. */
101
+ readonly readHead?: string;
102
+ }>;