@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.
- package/LICENSE +86 -0
- package/README.md +13 -0
- package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
- package/dist/allocator/AllocatorDurableObject.js +658 -0
- package/dist/allocator/IssuanceLedger.d.ts +188 -0
- package/dist/allocator/IssuanceLedger.js +608 -0
- package/dist/allocator/IssuanceReconciler.d.ts +41 -0
- package/dist/allocator/IssuanceReconciler.js +191 -0
- package/dist/allocator/OrderClock.d.ts +41 -0
- package/dist/allocator/OrderClock.js +92 -0
- package/dist/allocator/SortableUniqueId.d.ts +46 -0
- package/dist/allocator/SortableUniqueId.js +156 -0
- package/dist/allocator/types.d.ts +52 -0
- package/dist/allocator/types.js +1 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
- package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
- package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
- package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
- package/dist/bootstrap/OperatorBootstrap.js +104 -0
- package/dist/bootstrap/manifest.d.ts +9 -0
- package/dist/bootstrap/manifest.js +115 -0
- package/dist/bootstrap/types.d.ts +75 -0
- package/dist/bootstrap/types.js +1 -0
- package/dist/cli/OperatorRepairCli.d.ts +11 -0
- package/dist/cli/OperatorRepairCli.js +108 -0
- package/dist/cloudflare.d.ts +164 -0
- package/dist/cloudflare.js +19487 -0
- package/dist/commit/CommitWorker.d.ts +101 -0
- package/dist/commit/CommitWorker.js +921 -0
- package/dist/commit/types.d.ts +25 -0
- package/dist/commit/types.js +13 -0
- package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
- package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
- package/dist/completeness/types.d.ts +85 -0
- package/dist/completeness/types.js +6 -0
- package/dist/composition.d.ts +123 -0
- package/dist/composition.js +237 -0
- package/dist/cosmos.d.ts +11 -0
- package/dist/cosmos.js +1411 -0
- package/dist/d1-mv.d.ts +8 -0
- package/dist/d1-mv.js +2001 -0
- package/dist/d1.d.ts +16 -0
- package/dist/d1.js +3418 -0
- package/dist/diagnostics/G60DurableHop.d.ts +99 -0
- package/dist/diagnostics/G60DurableHop.js +248 -0
- package/dist/diagnostics/G65Admission.d.ts +23 -0
- package/dist/diagnostics/G65Admission.js +43 -0
- package/dist/diagnostics/G65DirectRing.d.ts +33 -0
- package/dist/diagnostics/G65DirectRing.js +85 -0
- package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
- package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
- package/dist/downstream/DeliveryCore.d.ts +127 -0
- package/dist/downstream/DeliveryCore.js +304 -0
- package/dist/downstream/Doorbell.d.ts +43 -0
- package/dist/downstream/Doorbell.js +137 -0
- package/dist/downstream/DownstreamAdapter.d.ts +52 -0
- package/dist/downstream/DownstreamAdapter.js +299 -0
- package/dist/downstream/ExclusionLookup.d.ts +36 -0
- package/dist/downstream/ExclusionLookup.js +50 -0
- package/dist/downstream/InconsistencyDetector.d.ts +27 -0
- package/dist/downstream/InconsistencyDetector.js +82 -0
- package/dist/downstream/OutboxDrain.d.ts +35 -0
- package/dist/downstream/OutboxDrain.js +126 -0
- package/dist/downstream/types.d.ts +77 -0
- package/dist/downstream/types.js +64 -0
- package/dist/eventIdentity.d.ts +30 -0
- package/dist/eventIdentity.js +49 -0
- package/dist/eventRecord.d.ts +26 -0
- package/dist/eventRecord.js +53 -0
- package/dist/g42-probe.d.ts +9 -0
- package/dist/g42-probe.js +9 -0
- package/dist/http/SerializedQueryWorker.d.ts +58 -0
- package/dist/http/SerializedQueryWorker.js +497 -0
- package/dist/http/commitResponse.d.ts +46 -0
- package/dist/http/commitResponse.js +80 -0
- package/dist/http/testServiceId.d.ts +17 -0
- package/dist/http/testServiceId.js +14 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +19926 -0
- package/dist/journal/JournalDurableObject.d.ts +30 -0
- package/dist/journal/JournalDurableObject.js +481 -0
- package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
- package/dist/journal/JournalFirstTouchProbe.js +375 -0
- package/dist/journal/types.d.ts +116 -0
- package/dist/journal/types.js +52 -0
- package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
- package/dist/mv/MaterializedViewCatchUp.js +353 -0
- package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
- package/dist/mv/MaterializedViewRuntime.js +328 -0
- package/dist/mv/MaterializedViewStore.d.ts +257 -0
- package/dist/mv/MaterializedViewStore.js +875 -0
- package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
- package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
- package/dist/mv/index.d.ts +8 -0
- package/dist/mv/index.js +2357 -0
- package/dist/projection/LiveProjectionWorker.d.ts +86 -0
- package/dist/projection/LiveProjectionWorker.js +240 -0
- package/dist/projection/ProjectionRuntime.d.ts +113 -0
- package/dist/projection/ProjectionRuntime.js +346 -0
- package/dist/projection/ProjectorRegistry.d.ts +51 -0
- package/dist/projection/ProjectorRegistry.js +116 -0
- package/dist/query/ProjectionQueryStore.d.ts +102 -0
- package/dist/query/ProjectionQueryStore.js +185 -0
- package/dist/query/QueryRegistry.d.ts +22 -0
- package/dist/query/QueryRegistry.js +36 -0
- package/dist/read/SerializedReadWorker.d.ts +36 -0
- package/dist/read/SerializedReadWorker.js +225 -0
- package/dist/repair/RepairWorker.d.ts +72 -0
- package/dist/repair/RepairWorker.js +461 -0
- package/dist/safeWindow.d.ts +20 -0
- package/dist/safeWindow.js +36 -0
- package/dist/scope/ControlRouteScope.d.ts +22 -0
- package/dist/scope/ControlRouteScope.js +36 -0
- package/dist/scope/ScopeName.d.ts +34 -0
- package/dist/scope/ScopeName.js +67 -0
- package/dist/service/ServiceIdentityProvider.d.ts +35 -0
- package/dist/service/ServiceIdentityProvider.js +56 -0
- package/dist/store/CosmosEventStore.d.ts +126 -0
- package/dist/store/CosmosEventStore.js +950 -0
- package/dist/store/D1EventStore.d.ts +111 -0
- package/dist/store/D1EventStore.js +1111 -0
- package/dist/store/PostgresEventStore.d.ts +35 -0
- package/dist/store/PostgresEventStore.js +811 -0
- package/dist/store/provider.d.ts +25 -0
- package/dist/store/provider.js +32 -0
- package/dist/store/types.d.ts +205 -0
- package/dist/store/types.js +18 -0
- package/dist/tag/EventDigest.d.ts +40 -0
- package/dist/tag/EventDigest.js +158 -0
- package/dist/tag/TagDurableObject.d.ts +286 -0
- package/dist/tag/TagDurableObject.js +4071 -0
- package/dist/tag/TagSqlMeasurement.d.ts +26 -0
- package/dist/tag/TagSqlMeasurement.js +44 -0
- package/dist/tag/TagSqlSchema.d.ts +29 -0
- package/dist/tag/TagSqlSchema.js +211 -0
- package/dist/tag/types.d.ts +173 -0
- package/dist/tag/types.js +3 -0
- package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
- package/dist/tagstate/TagStateDurableObject.js +499 -0
- package/dist/trace/AttributionRatio.d.ts +12 -0
- package/dist/trace/AttributionRatio.js +68 -0
- package/dist/trace/CloudflareTracing.d.ts +7 -0
- package/dist/trace/CloudflareTracing.js +9 -0
- package/dist/trace/CommitTrace.d.ts +405 -0
- package/dist/trace/CommitTrace.js +1008 -0
- package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
- package/dist/trace/CommitTraceConsoleSink.js +114 -0
- package/dist/trace/CommitTraceVerifier.d.ts +22 -0
- package/dist/trace/CommitTraceVerifier.js +295 -0
- package/dist/trace/ObservationStream.d.ts +118 -0
- package/dist/trace/ObservationStream.js +148 -0
- 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
|
+
}
|