@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,497 @@
1
+ import { DEPLOYED_PROJECTOR_REGISTRY, } from "../projection/ProjectorRegistry";
2
+ import { safeWindowCeilingExceeded, safeWindowMs } from "../projection/ProjectionRuntime";
3
+ import { projectionHasObserved, readRowsPageFromBacking, selectQueryBacking, compareSuid, } from "../query/ProjectionQueryStore";
4
+ import { D1MaterializedViewStore } from "../mv/MaterializedViewStore";
5
+ import { DEPLOYED_QUERY_REGISTRY, } from "../query/QueryRegistry";
6
+ import { assertSortableUniqueId } from "../allocator/SortableUniqueId";
7
+ import { envServiceIdentity, requestServiceIdentity, } from "../service/ServiceIdentityProvider";
8
+ function json(body, status = 200) {
9
+ return new Response(JSON.stringify(body), {
10
+ status,
11
+ headers: { "content-type": "application/json; charset=utf-8" },
12
+ });
13
+ }
14
+ function error(status, code, message) {
15
+ return json({ error: message, code }, status);
16
+ }
17
+ function isObject(value) {
18
+ return typeof value === "object" && value !== null && !Array.isArray(value);
19
+ }
20
+ function isNonEmptyString(value) {
21
+ return typeof value === "string" && value.length > 0;
22
+ }
23
+ function requestStore(env, provider) {
24
+ // A postgres client is a request-scoped I/O object in workerd. Retaining it
25
+ // across fetch handlers turns a later list-query into a cross-request I/O
26
+ // violation, so the read-only query path intentionally creates one per HTTP
27
+ // request rather than sharing the projection worker's scheduled-poll store.
28
+ return provider.create(env);
29
+ }
30
+ function parseRequest(value) {
31
+ if (!isObject(value) || !isNonEmptyString(value.queryType) || typeof value.queryParamsJson !== "string") {
32
+ return { error: "queryType and queryParamsJson are required" };
33
+ }
34
+ let queryParams;
35
+ try {
36
+ queryParams = JSON.parse(value.queryParamsJson);
37
+ }
38
+ catch {
39
+ return { error: "queryParamsJson must contain a JSON document" };
40
+ }
41
+ if (value.waitForSortableUniqueId !== undefined && !isNonEmptyString(value.waitForSortableUniqueId)) {
42
+ return { error: "waitForSortableUniqueId must be a non-empty string when present" };
43
+ }
44
+ if (value.waitForSortableUniqueId !== undefined) {
45
+ try {
46
+ assertSortableUniqueId(value.waitForSortableUniqueId);
47
+ }
48
+ catch {
49
+ return { error: "waitForSortableUniqueId must be a 30-digit SortableUniqueId" };
50
+ }
51
+ }
52
+ return {
53
+ value: {
54
+ queryType: value.queryType,
55
+ queryParams,
56
+ waitForSortableUniqueId: value.waitForSortableUniqueId,
57
+ },
58
+ };
59
+ }
60
+ function paginationFrom(value) {
61
+ if (!isObject(value)) {
62
+ return { error: "list-query queryParamsJson must contain an object" };
63
+ }
64
+ const currentPage = value.PageNumber ?? 1;
65
+ const pageSize = value.PageSize ?? 20;
66
+ const newestFirst = value.NewestFirst ?? false;
67
+ const consistency = value.consistency ?? "safe";
68
+ if (typeof currentPage !== "number" || !Number.isSafeInteger(currentPage) || currentPage < 1 ||
69
+ typeof pageSize !== "number" || !Number.isSafeInteger(pageSize) || pageSize < 1 ||
70
+ typeof newestFirst !== "boolean" ||
71
+ (consistency !== "safe" && consistency !== "unsafe")) {
72
+ return { error: "PageNumber and PageSize must be positive integers, NewestFirst must be a boolean, and consistency must be safe or unsafe" };
73
+ }
74
+ return { value: { currentPage, pageSize, newestFirst, consistency } };
75
+ }
76
+ function decodePayload(entry) {
77
+ try {
78
+ // G32 projections carry the exact decoded JSON text from dcb_events.
79
+ // Do not revive the historical atob path: it would reinterpret already
80
+ // persisted bytes and hide a storage-format regression.
81
+ return JSON.parse(entry.payload);
82
+ }
83
+ catch {
84
+ // A projection value can still be a deliberately opaque string. The
85
+ // commit admission boundary has already guaranteed event payload JSON.
86
+ return entry.payload;
87
+ }
88
+ }
89
+ function realSleep(milliseconds) {
90
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
91
+ }
92
+ /**
93
+ * G31 reserves a final loop slot so a healthy pending 120s waiter does not
94
+ * stop polling before its absolute deadline: 126 loop slots / at most 126
95
+ * source+MV probes plus one final confirmation. Backoff is 25, 50, 100, 200,
96
+ * 400, 800, then 1000ms. A normal pending waiter therefore makes 26 paired
97
+ * probes at the 20s floor and 126 at the 120s ceiling; a mandatory
98
+ * final-success recheck raises the global maximum to 127 paired probes / 254
99
+ * point-read statements. The cap
100
+ * also makes a stalled test clock deterministic instead of issuing unbounded
101
+ * reads.
102
+ */
103
+ export const D1_WAIT_MAX_ITERATIONS = 126;
104
+ export const D1_WAIT_MAX_POINT_READS = 254;
105
+ const D1_WAIT_MAX_PROBES = D1_WAIT_MAX_POINT_READS / 2;
106
+ const D1_WAIT_INITIAL_BACKOFF_MS = 25;
107
+ const D1_WAIT_MAX_BACKOFF_MS = 1_000;
108
+ function isWaitForTargetSourcePort(value) {
109
+ return typeof value.readWaitForTarget === "function";
110
+ }
111
+ function d1WaitSucceeded(facts) {
112
+ if (facts.unavailable || facts.target.kind !== "stored")
113
+ return false;
114
+ // Receipt and safe-head are separate success branches. In particular, a
115
+ // head without a unique source target can never satisfy this condition.
116
+ return facts.targetReceipt || (facts.activeGeneration !== undefined &&
117
+ compareSuid(facts.safeContiguousHead, facts.target.suid) >= 0);
118
+ }
119
+ async function readD1WaitFacts(source, materializedView, serviceId, viewId, requestedSuid) {
120
+ const target = await source.readWaitForTarget(serviceId, requestedSuid);
121
+ // SUID collision/lineage mismatch is an incident gate before any success
122
+ // evaluation. A contradictory two-row source target is equivalently
123
+ // unavailable, rather than a candidate for safe-head aliasing.
124
+ if (target.kind === "unavailable") {
125
+ return { target, unavailable: true, targetReceipt: false, safeContiguousHead: "", activeGeneration: undefined };
126
+ }
127
+ if (materializedView.readWaitForState === undefined) {
128
+ throw new Error("D1 materialized-view waitFor state port is not configured");
129
+ }
130
+ const state = await materializedView.readWaitForState(serviceId, viewId, {
131
+ ...(target.kind === "stored" ? { eventId: target.eventId } : {}),
132
+ suid: requestedSuid,
133
+ });
134
+ const unavailable = state.checkpointAhead || state.rebuildRequired || state.poison;
135
+ return {
136
+ target,
137
+ unavailable,
138
+ targetReceipt: state.targetReceipt,
139
+ safeContiguousHead: state.safeContiguousHead,
140
+ activeGeneration: state.activeGeneration,
141
+ };
142
+ }
143
+ function d1WaitBackoff(iteration) {
144
+ return Math.min(D1_WAIT_MAX_BACKOFF_MS, D1_WAIT_INITIAL_BACKOFF_MS * 2 ** Math.min(iteration, 6));
145
+ }
146
+ /**
147
+ * A non-normal flapping success proof can exhaust the global read budget
148
+ * before the deadline. Preserve the published deadline in that case without
149
+ * issuing another point read. The progress check keeps an injected frozen
150
+ * clock deterministic in the test seam.
151
+ */
152
+ async function sleepToD1WaitDeadline(deadline, sleep, now) {
153
+ let observedAt = now();
154
+ while (observedAt < deadline) {
155
+ await sleep(Math.min(D1_WAIT_MAX_BACKOFF_MS, deadline - observedAt));
156
+ const advancedAt = now();
157
+ if (advancedAt <= observedAt)
158
+ return;
159
+ observedAt = advancedAt;
160
+ }
161
+ }
162
+ async function waitForD1Projection(source, materializedView, serviceId, viewId, requestedSuid, requestStartedAt, options) {
163
+ const now = options.now ?? Date.now;
164
+ // The lag estimate and deadline are both anchored at request start. Later
165
+ // samples must not stretch a request's published SafeWindow.
166
+ const dynamicLagBoundMs = await source.currentLagBound(serviceId, requestStartedAt);
167
+ const deadline = requestStartedAt + safeWindowMs(dynamicLagBoundMs);
168
+ const sleep = options.sleep ?? realSleep;
169
+ let probes = 0;
170
+ const probe = async () => {
171
+ if (probes >= D1_WAIT_MAX_PROBES)
172
+ return undefined;
173
+ probes += 1;
174
+ return readD1WaitFacts(source, materializedView, serviceId, viewId, requestedSuid);
175
+ };
176
+ for (let iteration = 0; iteration < D1_WAIT_MAX_ITERATIONS; iteration += 1) {
177
+ // A sleep that resumes exactly at the boundary gets one final proof and,
178
+ // when ready, its mandatory confirmation. A late resumption gets no
179
+ // post-deadline probe, so sleep overhead cannot widen the SafeWindow.
180
+ if (now() > deadline)
181
+ return "timeout";
182
+ const facts = await probe();
183
+ if (facts === undefined) {
184
+ await sleepToD1WaitDeadline(deadline, sleep, now);
185
+ return "timeout";
186
+ }
187
+ if (facts.unavailable)
188
+ return "unavailable";
189
+ // An over-ceiling estimate remains indeterminate after (and only after)
190
+ // the initial incident gate. It must not be bypassed by a ready receipt.
191
+ if (safeWindowCeilingExceeded(dynamicLagBoundMs))
192
+ return "timeout";
193
+ if (d1WaitSucceeded(facts)) {
194
+ // A state can change between the first proof and response creation.
195
+ // Re-read incident/rebuild/poison and the generation-bound receipt just
196
+ // before success; this is deliberately not a cached boolean.
197
+ const confirmed = await probe();
198
+ if (confirmed === undefined) {
199
+ await sleepToD1WaitDeadline(deadline, sleep, now);
200
+ return "timeout";
201
+ }
202
+ if (confirmed.unavailable)
203
+ return "unavailable";
204
+ if (d1WaitSucceeded(confirmed))
205
+ return "visible";
206
+ }
207
+ if (now() >= deadline) {
208
+ return "timeout";
209
+ }
210
+ if (probes >= D1_WAIT_MAX_PROBES || iteration + 1 >= D1_WAIT_MAX_ITERATIONS) {
211
+ await sleepToD1WaitDeadline(deadline, sleep, now);
212
+ return "timeout";
213
+ }
214
+ await sleep(Math.min(d1WaitBackoff(iteration), Math.max(1, deadline - now())));
215
+ }
216
+ return "timeout";
217
+ }
218
+ async function waitForProjection(store, serviceId, definition, requestedSuid, options) {
219
+ const now = options.now ?? Date.now;
220
+ const dynamicLagBoundMs = await store.currentLagBound(serviceId, now());
221
+ // A bound above the published ceiling is indeterminate. Do not turn that
222
+ // state into an empty success by clamping it and waiting forever.
223
+ if (safeWindowCeilingExceeded(dynamicLagBoundMs)) {
224
+ return false;
225
+ }
226
+ const deadline = now() + safeWindowMs(dynamicLagBoundMs);
227
+ const sleep = options.sleep ?? realSleep;
228
+ const pollIntervalMs = Math.max(1, options.pollIntervalMs ?? 25);
229
+ while (now() < deadline) {
230
+ if (await projectionHasObserved(store, serviceId, definition, requestedSuid)) {
231
+ return true;
232
+ }
233
+ await sleep(Math.min(pollIntervalMs, deadline - now()));
234
+ }
235
+ return false;
236
+ }
237
+ function endpointFromPath(path) {
238
+ if (path === "/api/sekiban/serialized/query") {
239
+ return "query";
240
+ }
241
+ if (path === "/api/sekiban/serialized/list-query") {
242
+ return "list-query";
243
+ }
244
+ return undefined;
245
+ }
246
+ function resultResponse(endpoint, entries, pagination, totalCount = entries.length, serverPaged = false, readHead) {
247
+ if (endpoint === "query") {
248
+ return json({ resultJson: JSON.stringify({ count: entries.length }) });
249
+ }
250
+ const page = pagination;
251
+ const offset = (page.currentPage - 1) * page.pageSize;
252
+ const items = (serverPaged ? entries : entries.slice(offset, offset + page.pageSize)).map(decodePayload);
253
+ return json({
254
+ itemsJson: JSON.stringify(items),
255
+ totalCount,
256
+ totalPages: totalCount === 0 ? 0 : Math.ceil(totalCount / page.pageSize),
257
+ currentPage: page.currentPage,
258
+ pageSize: page.pageSize,
259
+ ...(readHead === undefined ? {} : { readHead }),
260
+ });
261
+ }
262
+ function pageReadHead(entries, page, serverPaged, safeHead) {
263
+ if (page.consistency === "safe")
264
+ return safeHead;
265
+ const offset = (page.currentPage - 1) * page.pageSize;
266
+ const visible = serverPaged ? entries : entries.slice(offset, offset + page.pageSize);
267
+ return visible.reduce((head, entry) => compareSuid(entry.suid, head) > 0 ? entry.suid : head, "");
268
+ }
269
+ /**
270
+ * V1 §5.4/§5.5 HTTP surface. It never starts a projection catch-up or writes
271
+ * a source/checkpoint row: the query sees only the durable read-side snapshot.
272
+ */
273
+ export async function handleSerializedQuery(request, env, options = {}) {
274
+ const requestStartedAt = (options.now ?? Date.now)();
275
+ const endpoint = endpointFromPath(new URL(request.url).pathname);
276
+ const serviceId = requestServiceIdentity(request, options.serviceIdentityProvider ?? envServiceIdentity(env), {
277
+ allowG11Verification: env.G11_VERIFICATION_ENABLED === "true",
278
+ });
279
+ if (endpoint === undefined || request.method !== "POST") {
280
+ return error(404, "query_route_not_found", "Query routes require POST");
281
+ }
282
+ let rawBody;
283
+ try {
284
+ rawBody = await request.json();
285
+ }
286
+ catch {
287
+ return error(400, "validation_error", "Query request must be JSON");
288
+ }
289
+ const parsed = parseRequest(rawBody);
290
+ if (parsed.value === undefined) {
291
+ return error(400, "validation_error", parsed.error ?? "Invalid query request");
292
+ }
293
+ const registry = options.registry ?? DEPLOYED_QUERY_REGISTRY;
294
+ const definition = registry.resolve(parsed.value.queryType);
295
+ if (definition === undefined || definition.endpoint !== endpoint) {
296
+ return error(400, "validation_error", "queryType is not mapped for this endpoint");
297
+ }
298
+ if (!definition.enabled || (options.projectors ?? DEPLOYED_PROJECTOR_REGISTRY).resolve(definition.tagProjector) === undefined) {
299
+ return error(503, "projection_unavailable", "The mapped query projection is unavailable");
300
+ }
301
+ const pagination = endpoint === "list-query" ? paginationFrom(parsed.value.queryParams) : { value: undefined };
302
+ if (pagination.value === undefined && pagination.error !== undefined) {
303
+ return error(400, "validation_error", pagination.error);
304
+ }
305
+ let d1MaterializedView;
306
+ let d1SafeRead = false;
307
+ const viewId = definition.materializedViewId ?? definition.tagProjector;
308
+ try {
309
+ const backing = options.queryBacking ?? "memory";
310
+ let requestStoreValue;
311
+ let waitStore = options.store;
312
+ let selection;
313
+ let safeReadGeneration;
314
+ if (backing === "d1-mv") {
315
+ const materializedView = options.materializedViewQueryPort ??
316
+ (env.D1_MV === undefined ? undefined : new D1MaterializedViewStore(env.D1_MV));
317
+ if (materializedView === undefined) {
318
+ return error(503, "projection_unavailable", "The D1 materialized-view query projection is unavailable");
319
+ }
320
+ d1MaterializedView = materializedView;
321
+ await materializedView.initialize?.();
322
+ selection = selectQueryBacking({ backing, materializedView });
323
+ const orderingQuarantine = await materializedView.readOrderingQuarantine?.(serviceId, viewId);
324
+ const safeRead = pagination.value?.consistency !== "unsafe";
325
+ d1SafeRead = safeRead;
326
+ if (safeRead && orderingQuarantine !== undefined) {
327
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
328
+ }
329
+ if (safeRead)
330
+ safeReadGeneration = await materializedView.readActiveGeneration?.(serviceId, viewId);
331
+ if (await materializedView.hasCheckpointAheadFinding?.(serviceId, viewId)) {
332
+ return error(503, "projection_unavailable", "The D1 materialized-view query projection is unavailable");
333
+ }
334
+ // Waiting still needs the durable source/checkpoint facts. Only create
335
+ // the normal source store when the request actually asks to wait.
336
+ if (parsed.value.waitForSortableUniqueId !== undefined && waitStore === undefined) {
337
+ if (options.storeProvider === undefined) {
338
+ return error(503, "projection_unavailable", "A query projection store is not configured");
339
+ }
340
+ requestStoreValue = requestStore(env, options.storeProvider);
341
+ await requestStoreValue.initialize();
342
+ waitStore = requestStoreValue;
343
+ }
344
+ }
345
+ else {
346
+ if (options.store === undefined && options.storeProvider === undefined) {
347
+ return error(503, "projection_unavailable", "A query projection store is not configured");
348
+ }
349
+ requestStoreValue = options.store === undefined
350
+ ? requestStore(env, options.storeProvider)
351
+ : undefined;
352
+ waitStore = options.store ?? requestStoreValue;
353
+ if (requestStoreValue !== undefined) {
354
+ await requestStoreValue.initialize();
355
+ }
356
+ selection = selectQueryBacking({ backing, memory: waitStore });
357
+ }
358
+ if (parsed.value.waitForSortableUniqueId !== undefined) {
359
+ let waitResult;
360
+ if (selection.backing === "d1-mv") {
361
+ if (waitStore === undefined || d1MaterializedView === undefined || !isWaitForTargetSourcePort(waitStore)) {
362
+ return error(503, "projection_unavailable", "The D1 materialized-view waitFor source is unavailable");
363
+ }
364
+ waitResult = await waitForD1Projection(waitStore, d1MaterializedView, serviceId, definition.materializedViewId ?? definition.tagProjector, parsed.value.waitForSortableUniqueId, requestStartedAt, options);
365
+ }
366
+ else if (waitStore === undefined || !await waitForProjection(waitStore, serviceId, definition, parsed.value.waitForSortableUniqueId, options)) {
367
+ waitResult = "timeout";
368
+ }
369
+ else {
370
+ waitResult = "visible";
371
+ }
372
+ if (waitResult === "unavailable") {
373
+ const waitQuarantine = d1SafeRead && d1MaterializedView !== undefined
374
+ ? await d1MaterializedView.readOrderingQuarantine?.(serviceId, viewId)
375
+ : undefined;
376
+ if (waitQuarantine !== undefined) {
377
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
378
+ }
379
+ return error(503, "projection_unavailable", "The mapped query projection is unavailable");
380
+ }
381
+ if (waitResult !== "visible") {
382
+ const timeoutQuarantine = d1SafeRead && d1MaterializedView !== undefined
383
+ ? await d1MaterializedView.readOrderingQuarantine?.(serviceId, viewId)
384
+ : undefined;
385
+ if (timeoutQuarantine !== undefined) {
386
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
387
+ }
388
+ return error(504, "timeout", "Projection did not reach the requested sortableUniqueId within the published SafeWindow; refresh this read to inspect current state");
389
+ }
390
+ // Quarantine can be created while waitFor is polling. Re-check the
391
+ // active generation at the response boundary; an early check alone can
392
+ // return a 200 after the ordering detector has already opened a gate.
393
+ if (d1SafeRead && d1MaterializedView !== undefined) {
394
+ const boundaryQuarantine = await d1MaterializedView.readOrderingQuarantine?.(serviceId, viewId);
395
+ if (boundaryQuarantine !== undefined) {
396
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
397
+ }
398
+ const boundaryGeneration = await d1MaterializedView.readActiveGeneration?.(serviceId, viewId);
399
+ if (boundaryGeneration !== undefined && safeReadGeneration !== undefined && boundaryGeneration !== safeReadGeneration) {
400
+ return error(503, "projection_unavailable", "The mapped query projection changed generation while the safe wait was running; retry the read");
401
+ }
402
+ safeReadGeneration = boundaryGeneration ?? safeReadGeneration;
403
+ }
404
+ }
405
+ if (d1SafeRead && parsed.value.waitForSortableUniqueId === undefined && d1MaterializedView !== undefined) {
406
+ const boundaryQuarantine = await d1MaterializedView.readOrderingQuarantine?.(serviceId, viewId);
407
+ if (boundaryQuarantine !== undefined) {
408
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
409
+ }
410
+ const boundaryGeneration = await d1MaterializedView.readActiveGeneration?.(serviceId, viewId);
411
+ if (boundaryGeneration !== undefined && safeReadGeneration !== undefined && boundaryGeneration !== safeReadGeneration) {
412
+ return error(503, "projection_unavailable", "The mapped query projection changed generation while the safe read was being prepared; retry the read");
413
+ }
414
+ safeReadGeneration = boundaryGeneration ?? safeReadGeneration;
415
+ }
416
+ const requestedPage = pagination.value;
417
+ if (requestedPage !== undefined && selection.backing === "d1-mv" && selection.store.readListPage !== undefined) {
418
+ const page = await selection.store.readListPage(serviceId, viewId, {
419
+ ...(safeReadGeneration === undefined ? {} : { generation: safeReadGeneration }),
420
+ limit: requestedPage.pageSize,
421
+ offset: (requestedPage.currentPage - 1) * requestedPage.pageSize,
422
+ ...(requestedPage.newestFirst ? { descending: true } : {}),
423
+ consistency: requestedPage.consistency,
424
+ });
425
+ if (d1SafeRead && d1MaterializedView !== undefined) {
426
+ const pageQuarantine = await d1MaterializedView.readOrderingQuarantine?.(serviceId, viewId);
427
+ if (pageQuarantine !== undefined) {
428
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
429
+ }
430
+ const pageGeneration = await d1MaterializedView.readActiveGeneration?.(serviceId, viewId);
431
+ if (pageGeneration !== undefined && safeReadGeneration !== undefined && pageGeneration !== safeReadGeneration) {
432
+ return error(503, "projection_unavailable", "The mapped query projection changed generation while the safe page was being read; retry the read");
433
+ }
434
+ }
435
+ const entries = page.rows.map((row) => ({
436
+ eventId: isObject(row.value) && typeof row.value.eventId === "string" && row.value.eventId.length > 0 ? row.value.eventId : row.rowKey,
437
+ suid: row.sourceSuid,
438
+ payload: JSON.stringify(row.value),
439
+ }));
440
+ if (requestedPage.consistency === "unsafe") {
441
+ options.afterUnsafeRead?.({
442
+ serviceId,
443
+ viewId,
444
+ entries,
445
+ observedAt: Date.now(),
446
+ });
447
+ }
448
+ return resultResponse(endpoint, entries, requestedPage, page.totalCount, true, page.readHead);
449
+ }
450
+ const supportsServerPaging = selection.backing === "d1-mv" && selection.store.queryRowsWithTotal !== undefined;
451
+ const page = await readRowsPageFromBacking(selection, serviceId, viewId, definition, requestedPage === undefined || !supportsServerPaging
452
+ ? { limit: null, ...(safeReadGeneration === undefined ? {} : { generation: safeReadGeneration }) }
453
+ : {
454
+ ...(safeReadGeneration === undefined ? {} : { generation: safeReadGeneration }),
455
+ limit: requestedPage.pageSize,
456
+ offset: (requestedPage.currentPage - 1) * requestedPage.pageSize,
457
+ ...(requestedPage.newestFirst ? { descending: true } : {}),
458
+ });
459
+ if (d1SafeRead && d1MaterializedView !== undefined) {
460
+ const pageQuarantine = await d1MaterializedView.readOrderingQuarantine?.(serviceId, viewId);
461
+ if (pageQuarantine !== undefined) {
462
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
463
+ }
464
+ const pageGeneration = await d1MaterializedView.readActiveGeneration?.(serviceId, viewId);
465
+ if (pageGeneration !== undefined && safeReadGeneration !== undefined && pageGeneration !== safeReadGeneration) {
466
+ return error(503, "projection_unavailable", "The mapped query projection changed generation while the safe page was being read; retry the read");
467
+ }
468
+ }
469
+ if (requestedPage?.consistency === "unsafe") {
470
+ options.afterUnsafeRead?.({
471
+ serviceId,
472
+ viewId,
473
+ entries: page.entries,
474
+ observedAt: Date.now(),
475
+ });
476
+ }
477
+ return resultResponse(endpoint, page.entries, pagination.value, page.totalCount, page.serverPaged, requestedPage === undefined ? undefined : pageReadHead(page.entries, requestedPage, page.serverPaged, page.readHead));
478
+ }
479
+ catch {
480
+ // A wait/read can race the detector or a generation transition. If the
481
+ // ordering quarantine is already durable, preserve its typed public
482
+ // contract instead of collapsing the race into generic unavailability.
483
+ if (d1SafeRead && d1MaterializedView !== undefined) {
484
+ try {
485
+ const caughtQuarantine = await d1MaterializedView.readOrderingQuarantine?.(serviceId, viewId);
486
+ if (caughtQuarantine !== undefined) {
487
+ return error(503, "projection_ordering_quarantined", "The mapped query projection is quarantined for a source-ordering incident; rebuild and promote the affected generation");
488
+ }
489
+ }
490
+ catch {
491
+ // Preserve the existing generic unavailable response if the
492
+ // quarantine probe itself is unavailable.
493
+ }
494
+ }
495
+ return error(503, "projection_unavailable", "The mapped query projection is unavailable");
496
+ }
497
+ }
@@ -0,0 +1,46 @@
1
+ import type { JournalRecord, ReconciliationInput } from "../journal/types";
2
+ export interface WrittenEventResponse {
3
+ payload: string;
4
+ sortableUniqueIdValue: string;
5
+ id: string;
6
+ eventMetadata: {
7
+ causationId: string;
8
+ correlationId: string;
9
+ executedUser: string;
10
+ };
11
+ tags: string[];
12
+ eventPayloadName: string;
13
+ }
14
+ export interface TagWriteResultResponse {
15
+ tag: string;
16
+ version: number;
17
+ writtenAt: string;
18
+ }
19
+ export interface CompleteCommitResponse {
20
+ writtenEvents: WrittenEventResponse[];
21
+ tagWriteResults: TagWriteResultResponse[];
22
+ duration: string;
23
+ }
24
+ export interface CommitHttpMapping {
25
+ status: number;
26
+ body: unknown;
27
+ }
28
+ export interface ActualTagRecord {
29
+ tag: string;
30
+ events: Array<{
31
+ eventId: string;
32
+ payload: string;
33
+ }>;
34
+ }
35
+ /**
36
+ * Purely derives the requery facts from durable tag snapshots. It is shared by
37
+ * the response-layer tests so outcome classification is never inferred from a
38
+ * Journal state alone.
39
+ */
40
+ export declare function requeryFactsFromTagRecords(record: Pick<JournalRecord, "candidates" | "allTags" | "reconciliation">, tagRecords: ActualTagRecord[]): ReconciliationInput;
41
+ /**
42
+ * Pure V1 terminal-outcome mapping. `undefined` intentionally means that a
43
+ * nonterminal/abandoned Journal has no application outcome to serialize.
44
+ */
45
+ export declare function mapTerminalCommitOutcome(record: Pick<JournalRecord, "state" | "allTags" | "reconciliation">, complete?: CompleteCommitResponse): CommitHttpMapping | undefined;
46
+ export declare function durationSince(startedAtMs: number): string;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Purely derives the requery facts from durable tag snapshots. It is shared by
3
+ * the response-layer tests so outcome classification is never inferred from a
4
+ * Journal state alone.
5
+ */
6
+ export function requeryFactsFromTagRecords(record, tagRecords) {
7
+ const recordsByTag = new Map(tagRecords.map((entry) => [entry.tag, entry.events]));
8
+ const contains = (tag, eventId, payload) => (recordsByTag.get(tag) ?? []).some((event) => event.eventId === eventId && event.payload === payload);
9
+ const missingTags = record.allTags.filter((tag) => record.candidates
10
+ .filter((candidate) => candidate.tags.includes(tag))
11
+ .some((candidate) => !contains(tag, candidate.eventId, candidate.payload)));
12
+ return {
13
+ allocatorVector: record.reconciliation?.allocatorVector,
14
+ records: record.candidates.map((candidate) => ({
15
+ eventId: candidate.eventId,
16
+ payload: candidate.payload,
17
+ present: candidate.tags.some((tag) => contains(tag, candidate.eventId, candidate.payload)),
18
+ })),
19
+ failureCause: record.reconciliation?.failureCause ?? "write-failure",
20
+ missingTags,
21
+ };
22
+ }
23
+ function failureMapping(cause) {
24
+ if (cause === "reservation-timeout") {
25
+ return {
26
+ status: 504,
27
+ body: { error: "serialized commit timed out", code: "timeout" },
28
+ };
29
+ }
30
+ return {
31
+ status: 500,
32
+ body: { error: "serialized commit failed before writing requested records", code: "internal_error" },
33
+ };
34
+ }
35
+ /**
36
+ * Pure V1 terminal-outcome mapping. `undefined` intentionally means that a
37
+ * nonterminal/abandoned Journal has no application outcome to serialize.
38
+ */
39
+ export function mapTerminalCommitOutcome(record, complete) {
40
+ switch (record.state) {
41
+ case "COMPLETE":
42
+ return complete === undefined ? undefined : { status: 200, body: complete };
43
+ case "REFUSED":
44
+ return {
45
+ status: 400,
46
+ body: {
47
+ error: "serialized commit was refused by a consistency reservation",
48
+ code: "consistency_conflict",
49
+ },
50
+ };
51
+ case "FAILED":
52
+ return failureMapping(record.reconciliation?.failureCause);
53
+ case "PARTIAL": {
54
+ const reconciliation = record.reconciliation;
55
+ const writtenEventIds = reconciliation?.records.filter((entry) => entry.present).map((entry) => entry.eventId) ?? [];
56
+ const failedEventIds = reconciliation?.records.filter((entry) => !entry.present).map((entry) => entry.eventId) ?? [];
57
+ const missingTags = reconciliation?.missingTags ?? [];
58
+ return {
59
+ status: 500,
60
+ body: {
61
+ error: "serialized commit partially failed",
62
+ code: "partial_write",
63
+ partial: {
64
+ writtenEventIds,
65
+ failedEventIds,
66
+ writtenTags: record.allTags.filter((tag) => !missingTags.includes(tag)),
67
+ missingTags,
68
+ eventsDeleted: false,
69
+ retryable: false,
70
+ },
71
+ },
72
+ };
73
+ }
74
+ default:
75
+ return undefined;
76
+ }
77
+ }
78
+ export function durationSince(startedAtMs) {
79
+ return `PT${Math.max(0, Date.now() - startedAtMs) / 1_000}S`;
80
+ }
@@ -0,0 +1,17 @@
1
+ import { G11_SERVICE_ID_HEADER, TEST_SERVICE_ID_HEADER } from "../service/ServiceIdentityProvider";
2
+ export { G11_SERVICE_ID_HEADER, TEST_SERVICE_ID_HEADER };
3
+ export interface ServiceIdRequestOptions {
4
+ /**
5
+ * Explicitly enabled only by an authenticated deployment-verification
6
+ * lane. The default is fail-closed for production/public requests.
7
+ */
8
+ readonly allowG11Verification?: boolean;
9
+ /** Server-side deployment identity; never sourced from a request header. */
10
+ readonly configuredServiceId?: string;
11
+ }
12
+ /**
13
+ * Compatibility entry point for existing hosts. New composition uses the
14
+ * ServiceIdentityProvider seam directly.
15
+ */
16
+ export declare function requireConfiguredServiceId(value: string | undefined): string;
17
+ export declare function serviceIdForRequest(request: Request, options?: ServiceIdRequestOptions): string;
@@ -0,0 +1,14 @@
1
+ import { G11_SERVICE_ID_HEADER, TEST_SERVICE_ID_HEADER, injectableServiceIdentity, requireServiceIdentity, requestServiceIdentity, } from "../service/ServiceIdentityProvider";
2
+ export { G11_SERVICE_ID_HEADER, TEST_SERVICE_ID_HEADER };
3
+ /**
4
+ * Compatibility entry point for existing hosts. New composition uses the
5
+ * ServiceIdentityProvider seam directly.
6
+ */
7
+ export function requireConfiguredServiceId(value) {
8
+ return requireServiceIdentity(injectableServiceIdentity(value));
9
+ }
10
+ export function serviceIdForRequest(request, options = {}) {
11
+ return requestServiceIdentity(request, injectableServiceIdentity(options.configuredServiceId), {
12
+ allowG11Verification: options.allowG11Verification,
13
+ });
14
+ }