@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,499 @@
|
|
|
1
|
+
import { assertSortableUniqueId, compareSortableUniqueId } from "../allocator/SortableUniqueId";
|
|
2
|
+
import { projectionEventFromTagEvent, } from "../projection/ProjectorRegistry";
|
|
3
|
+
import { scopeIdFor } from "../scope/ScopeName";
|
|
4
|
+
/** The fixed bounded source page used by both normal delta and replay. */
|
|
5
|
+
export const TAG_STATE_SOURCE_PAGE_LIMIT = 64;
|
|
6
|
+
// A deployed Worker has one composed domain registry. The public Worker
|
|
7
|
+
// factories install that exact registry before any request can reach a
|
|
8
|
+
// TagStateDO. A global Symbol makes the slot common to the entrypoint bundle
|
|
9
|
+
// and this package module, rather than silently falling back when tooling
|
|
10
|
+
// loads them through distinct module URLs.
|
|
11
|
+
const TAG_STATE_REGISTRY_SLOT = Symbol.for("sekiban.dcb.tagstate.projector-registry.v1");
|
|
12
|
+
function configuredProjectorRegistry() {
|
|
13
|
+
return globalThis[TAG_STATE_REGISTRY_SLOT];
|
|
14
|
+
}
|
|
15
|
+
export function configureTagStateProjectorRegistry(registry) {
|
|
16
|
+
const target = globalThis;
|
|
17
|
+
if (registry === undefined) {
|
|
18
|
+
delete target[TAG_STATE_REGISTRY_SLOT];
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
target[TAG_STATE_REGISTRY_SLOT] = registry;
|
|
22
|
+
}
|
|
23
|
+
class TagStateIdentityConflict extends Error {
|
|
24
|
+
constructor() {
|
|
25
|
+
super("TagState Durable Object identity changed");
|
|
26
|
+
this.name = "TagStateIdentityConflict";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
class TagStateUnknownProjector extends Error {
|
|
30
|
+
constructor() {
|
|
31
|
+
super("TagState projector is no longer registered");
|
|
32
|
+
this.name = "TagStateUnknownProjector";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
class TagStateRegistryFailure extends Error {
|
|
36
|
+
constructor(cause) {
|
|
37
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
38
|
+
this.name = "TagStateRegistryFailure";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class TagStateSourceFailure extends Error {
|
|
42
|
+
status;
|
|
43
|
+
constructor(status, cause) {
|
|
44
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
45
|
+
this.status = status;
|
|
46
|
+
this.name = "TagStateSourceFailure";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
class TagStateCacheCorruption extends Error {
|
|
50
|
+
constructor(cause) {
|
|
51
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
52
|
+
this.name = "TagStateCacheCorruption";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class TagStateCheckpointFault extends Error {
|
|
56
|
+
constructor() {
|
|
57
|
+
super("G46 checkpoint fault");
|
|
58
|
+
this.name = "TagStateCheckpointFault";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function json(body, status = 200) {
|
|
62
|
+
return new Response(JSON.stringify(body), {
|
|
63
|
+
status,
|
|
64
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function error(status, code, message) {
|
|
68
|
+
return json({ code, error: message }, status);
|
|
69
|
+
}
|
|
70
|
+
function isObject(value) {
|
|
71
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
72
|
+
}
|
|
73
|
+
function isNonEmptyString(value) {
|
|
74
|
+
return typeof value === "string" && value.length > 0;
|
|
75
|
+
}
|
|
76
|
+
function sqlString(value, column) {
|
|
77
|
+
if (typeof value !== "string")
|
|
78
|
+
throw new TagStateCacheCorruption(new Error(`TagState SQL ${column} is not a string`));
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function sqlNullableString(value, column) {
|
|
82
|
+
if (value === null || value === undefined)
|
|
83
|
+
return null;
|
|
84
|
+
return sqlString(value, column);
|
|
85
|
+
}
|
|
86
|
+
function parseIdentity(value) {
|
|
87
|
+
if (!isObject(value) || !isNonEmptyString(value.serviceId) || !isNonEmptyString(value.tag) || !isNonEmptyString(value.projectorId)) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return { serviceId: value.serviceId, tag: value.tag, projectorId: value.projectorId };
|
|
91
|
+
}
|
|
92
|
+
function initializeTagStateSchema(sql) {
|
|
93
|
+
sql.exec(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS tag_state_identity (
|
|
95
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
96
|
+
service_id TEXT NOT NULL,
|
|
97
|
+
tag TEXT NOT NULL,
|
|
98
|
+
projector_id TEXT NOT NULL,
|
|
99
|
+
UNIQUE (service_id, tag, projector_id)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS tag_state_cache (
|
|
103
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
104
|
+
phase TEXT NOT NULL CHECK (phase IN ('READY', 'REBUILDING')),
|
|
105
|
+
projector_version TEXT,
|
|
106
|
+
state_json TEXT,
|
|
107
|
+
last_suid TEXT NOT NULL,
|
|
108
|
+
target_projector_version TEXT,
|
|
109
|
+
frozen_through TEXT,
|
|
110
|
+
replay_cursor TEXT,
|
|
111
|
+
accumulator_json TEXT,
|
|
112
|
+
rebuild_id TEXT,
|
|
113
|
+
FOREIGN KEY (singleton) REFERENCES tag_state_identity(singleton) ON DELETE RESTRICT
|
|
114
|
+
);
|
|
115
|
+
`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* TagState cache and resumable replay owner. The only event source is the
|
|
119
|
+
* existing G43 Tag RPC; this object deliberately has neither a full-record
|
|
120
|
+
* fetch route nor a second source-query implementation.
|
|
121
|
+
*/
|
|
122
|
+
export class TagStateDurableObject {
|
|
123
|
+
ctx;
|
|
124
|
+
env;
|
|
125
|
+
g46CheckpointFault;
|
|
126
|
+
g46SourceNamespace;
|
|
127
|
+
/**
|
|
128
|
+
* Installed by the entrypoint when it composes the deployed domain. There
|
|
129
|
+
* is deliberately no default/test-registry fallback here: a DO isolate
|
|
130
|
+
* which was not built from that composition must return the typed registry
|
|
131
|
+
* failure rather than fold with a different projector authority.
|
|
132
|
+
*/
|
|
133
|
+
registry;
|
|
134
|
+
constructor(ctx, env, registry) {
|
|
135
|
+
this.ctx = ctx;
|
|
136
|
+
this.env = env;
|
|
137
|
+
this.registry = registry ?? configuredProjectorRegistry();
|
|
138
|
+
if (typeof ctx.storage.sql?.exec !== "function") {
|
|
139
|
+
throw new Error("TagState Durable Object requires SQLite storage");
|
|
140
|
+
}
|
|
141
|
+
initializeTagStateSchema(ctx.storage.sql);
|
|
142
|
+
}
|
|
143
|
+
/** Test-only fault seam for the two durable checkpoint boundaries. */
|
|
144
|
+
setG46CheckpointFaultForTest(fault) {
|
|
145
|
+
this.g46CheckpointFault = fault;
|
|
146
|
+
}
|
|
147
|
+
/** Test-only source seam; production always resolves `env.TAG`. */
|
|
148
|
+
setG46SourceNamespaceForTest(namespace) {
|
|
149
|
+
this.g46SourceNamespace = namespace;
|
|
150
|
+
}
|
|
151
|
+
async fetch(request) {
|
|
152
|
+
const url = new URL(request.url);
|
|
153
|
+
if (request.method !== "POST" || url.pathname !== "/read") {
|
|
154
|
+
return error(404, "tag_state_route_not_found", "TagState route was not found");
|
|
155
|
+
}
|
|
156
|
+
let body;
|
|
157
|
+
try {
|
|
158
|
+
body = await request.json();
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return error(400, "tag_state_validation_error", "TagState request must be JSON");
|
|
162
|
+
}
|
|
163
|
+
const identity = parseIdentity(body);
|
|
164
|
+
if (identity === undefined) {
|
|
165
|
+
return error(400, "tag_state_validation_error", "TagState identity requires serviceId, tag, and projectorId");
|
|
166
|
+
}
|
|
167
|
+
return await this.read(identity);
|
|
168
|
+
}
|
|
169
|
+
async read(identity) {
|
|
170
|
+
try {
|
|
171
|
+
this.ensureIdentity(identity);
|
|
172
|
+
const projector = this.resolveProjector(identity.projectorId);
|
|
173
|
+
let cache = this.readCache();
|
|
174
|
+
if (cache === undefined) {
|
|
175
|
+
cache = this.beginRebuild(projector, "cache-missing");
|
|
176
|
+
}
|
|
177
|
+
else if (cache.phase === "READY" && cache.projectorVersion !== projector.projectorVersion) {
|
|
178
|
+
cache = this.beginRebuild(projector, "projector-version-mismatch");
|
|
179
|
+
}
|
|
180
|
+
else if (cache.phase === "REBUILDING" && cache.targetProjectorVersion !== projector.projectorVersion) {
|
|
181
|
+
// The author changed its declared version while a previous rebuild was
|
|
182
|
+
// unfinished. Discard only the derived accumulator and restart from
|
|
183
|
+
// the source; the Tag DO remains the sole event authority.
|
|
184
|
+
cache = this.beginRebuild(projector, "target-projector-version-changed");
|
|
185
|
+
}
|
|
186
|
+
if (cache.phase === "READY") {
|
|
187
|
+
return await this.beginDeltaOrRespondReady(identity, projector, cache);
|
|
188
|
+
}
|
|
189
|
+
return await this.continueRebuild(identity, projector, cache);
|
|
190
|
+
}
|
|
191
|
+
catch (caught) {
|
|
192
|
+
if (caught instanceof TagStateIdentityConflict) {
|
|
193
|
+
return error(409, "tag_state_identity_conflict", "TagState Durable Object identity changed");
|
|
194
|
+
}
|
|
195
|
+
if (caught instanceof TagStateUnknownProjector) {
|
|
196
|
+
return error(404, "tag_state_unknown_projector", "TagState projector is not registered");
|
|
197
|
+
}
|
|
198
|
+
if (caught instanceof TagStateRegistryFailure) {
|
|
199
|
+
return error(503, "tag_state_projector_registry_failure", "TagState projector registry is unavailable");
|
|
200
|
+
}
|
|
201
|
+
if (caught instanceof TagStateSourceFailure) {
|
|
202
|
+
return error(caught.status, "tag_state_source_frontier_failure", "TagState source frontier could not be read");
|
|
203
|
+
}
|
|
204
|
+
if (caught instanceof TagStateCacheCorruption) {
|
|
205
|
+
// A corrupt cache is deliberately not an empty state. Start the
|
|
206
|
+
// source-backed rebuild now, then make this read distinguishable from
|
|
207
|
+
// both a normal empty tag and a later rebuild-in-progress response.
|
|
208
|
+
try {
|
|
209
|
+
const projector = this.resolveProjector(identity.projectorId);
|
|
210
|
+
this.beginRebuild(projector, "cache-corrupt");
|
|
211
|
+
}
|
|
212
|
+
catch (rebuildFailure) {
|
|
213
|
+
if (rebuildFailure instanceof TagStateRegistryFailure) {
|
|
214
|
+
return error(503, "tag_state_projector_registry_failure", "TagState projector registry is unavailable");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return error(409, "tag_state_cache_corrupt", "TagState cache is corrupt and is rebuilding");
|
|
218
|
+
}
|
|
219
|
+
if (caught instanceof TagStateCheckpointFault) {
|
|
220
|
+
return error(503, "tag_state_rebuild_interrupted", "TagState replay checkpoint was interrupted");
|
|
221
|
+
}
|
|
222
|
+
return error(503, "tag_state_unavailable", "TagState cache is unavailable");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async beginDeltaOrRespondReady(identity, projector, cache) {
|
|
226
|
+
// Freeze the next delta before changing durable phase. A zero-event page
|
|
227
|
+
// completes immediately and preserves the legitimate empty initial state.
|
|
228
|
+
const page = await this.readSource(identity, cache.lastSuid, undefined);
|
|
229
|
+
if (page.events.length === 0 && page.completeThrough !== null) {
|
|
230
|
+
return json(this.success(projector, cache.stateJson, cache.lastSuid));
|
|
231
|
+
}
|
|
232
|
+
const rebuild = this.beginRebuild(projector, "normal-delta", {
|
|
233
|
+
cursor: cache.lastSuid,
|
|
234
|
+
accumulatorJson: cache.stateJson,
|
|
235
|
+
frozenThrough: page.through,
|
|
236
|
+
});
|
|
237
|
+
return this.foldAndCheckpoint(projector, rebuild, page);
|
|
238
|
+
}
|
|
239
|
+
async continueRebuild(identity, projector, cache) {
|
|
240
|
+
const page = await this.readSource(identity, cache.replayCursor, cache.frozenThrough);
|
|
241
|
+
return this.foldAndCheckpoint(projector, cache, page);
|
|
242
|
+
}
|
|
243
|
+
async foldAndCheckpoint(projector, cache, page) {
|
|
244
|
+
let state;
|
|
245
|
+
try {
|
|
246
|
+
state = projector.deserializeState(cache.accumulatorJson);
|
|
247
|
+
for (const event of page.events)
|
|
248
|
+
state = projector.apply(state, projectionEventFromTagEvent(event));
|
|
249
|
+
}
|
|
250
|
+
catch (caught) {
|
|
251
|
+
throw new TagStateCacheCorruption(caught);
|
|
252
|
+
}
|
|
253
|
+
let accumulatorJson;
|
|
254
|
+
try {
|
|
255
|
+
accumulatorJson = projector.serializeState(state);
|
|
256
|
+
}
|
|
257
|
+
catch (caught) {
|
|
258
|
+
throw new TagStateCacheCorruption(caught);
|
|
259
|
+
}
|
|
260
|
+
if (this.g46CheckpointFault === "before-checkpoint")
|
|
261
|
+
throw new TagStateCheckpointFault();
|
|
262
|
+
const complete = page.completeThrough !== null;
|
|
263
|
+
this.transaction(() => {
|
|
264
|
+
const sql = this.sql();
|
|
265
|
+
if (complete) {
|
|
266
|
+
sql.exec(`
|
|
267
|
+
UPDATE tag_state_cache
|
|
268
|
+
SET phase = 'READY', projector_version = ?, state_json = ?, last_suid = ?,
|
|
269
|
+
target_projector_version = NULL, frozen_through = NULL, replay_cursor = NULL,
|
|
270
|
+
accumulator_json = NULL, rebuild_id = NULL
|
|
271
|
+
WHERE singleton = 1
|
|
272
|
+
`, projector.projectorVersion, accumulatorJson, page.completeThrough);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
sql.exec(`
|
|
276
|
+
UPDATE tag_state_cache
|
|
277
|
+
SET frozen_through = ?, replay_cursor = ?, accumulator_json = ?
|
|
278
|
+
WHERE singleton = 1 AND phase = 'REBUILDING' AND rebuild_id = ?
|
|
279
|
+
`, page.through, page.lastSortableUniqueId, accumulatorJson, cache.rebuildId);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
if (this.g46CheckpointFault === "after-checkpoint")
|
|
283
|
+
throw new TagStateCheckpointFault();
|
|
284
|
+
if (!complete) {
|
|
285
|
+
return error(503, "tag_state_rebuild_in_progress", "TagState replay is rebuilding to its frozen frontier");
|
|
286
|
+
}
|
|
287
|
+
return json(this.success(projector, accumulatorJson, page.completeThrough));
|
|
288
|
+
}
|
|
289
|
+
success(projector, stateJson, lastSortedUniqueId) {
|
|
290
|
+
try {
|
|
291
|
+
const state = projector.deserializeState(stateJson);
|
|
292
|
+
return {
|
|
293
|
+
kind: "ready",
|
|
294
|
+
payload: projector.payload(state),
|
|
295
|
+
version: projector.version(state),
|
|
296
|
+
lastSortedUniqueId,
|
|
297
|
+
projectorVersion: projector.projectorVersion,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
catch (caught) {
|
|
301
|
+
throw new TagStateCacheCorruption(caught);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async readSource(identity, cursor, through) {
|
|
305
|
+
if (cursor !== "")
|
|
306
|
+
assertSortableUniqueId(cursor);
|
|
307
|
+
if (through !== undefined && through !== null && through !== "")
|
|
308
|
+
assertSortableUniqueId(through);
|
|
309
|
+
try {
|
|
310
|
+
const namespace = this.g46SourceNamespace ?? this.env.TAG;
|
|
311
|
+
const stub = namespace.get(scopeIdFor(namespace, {
|
|
312
|
+
serviceId: identity.serviceId,
|
|
313
|
+
doClass: "tag",
|
|
314
|
+
identity: identity.tag,
|
|
315
|
+
}));
|
|
316
|
+
const sourceUrl = new URL("https://tag-source.internal/__internal/g46/tag-state-incremental");
|
|
317
|
+
sourceUrl.searchParams.set("__tag", identity.tag);
|
|
318
|
+
sourceUrl.searchParams.set("__serviceId", identity.serviceId);
|
|
319
|
+
const response = await stub.fetch(new Request(sourceUrl, {
|
|
320
|
+
method: "POST",
|
|
321
|
+
headers: {
|
|
322
|
+
"content-type": "application/json",
|
|
323
|
+
"x-sdt-g46-source-read": "1",
|
|
324
|
+
},
|
|
325
|
+
body: JSON.stringify({
|
|
326
|
+
tag: identity.tag,
|
|
327
|
+
cursor,
|
|
328
|
+
limit: TAG_STATE_SOURCE_PAGE_LIMIT,
|
|
329
|
+
...(through === undefined || through === null ? {} : { through }),
|
|
330
|
+
}),
|
|
331
|
+
}));
|
|
332
|
+
if (response.status !== 200) {
|
|
333
|
+
const detail = await response.text();
|
|
334
|
+
const identityConflict = response.status === 409;
|
|
335
|
+
throw new TagStateSourceFailure(identityConflict ? 409 : 503, new Error(detail));
|
|
336
|
+
}
|
|
337
|
+
const page = await response.json();
|
|
338
|
+
this.assertSourcePage(cursor, through, page);
|
|
339
|
+
return page;
|
|
340
|
+
}
|
|
341
|
+
catch (caught) {
|
|
342
|
+
if (caught instanceof TagStateSourceFailure)
|
|
343
|
+
throw caught;
|
|
344
|
+
const identityConflict = caught instanceof Error && caught.name === "TagIdentityConflict";
|
|
345
|
+
throw new TagStateSourceFailure(identityConflict ? 409 : 503, caught);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
assertSourcePage(cursor, expectedThrough, page) {
|
|
349
|
+
if (!isObject(page) || !Array.isArray(page.events) || typeof page.lastSortableUniqueId !== "string" ||
|
|
350
|
+
typeof page.through !== "string" || (page.completeThrough !== null && typeof page.completeThrough !== "string")) {
|
|
351
|
+
throw new TagStateSourceFailure(503, new Error("TagState source response was malformed"));
|
|
352
|
+
}
|
|
353
|
+
if (expectedThrough !== undefined && expectedThrough !== null && page.through !== expectedThrough) {
|
|
354
|
+
throw new TagStateSourceFailure(503, new Error("TagState source changed its frozen frontier"));
|
|
355
|
+
}
|
|
356
|
+
if (page.through !== "")
|
|
357
|
+
assertSortableUniqueId(page.through);
|
|
358
|
+
let prior = cursor;
|
|
359
|
+
for (const event of page.events) {
|
|
360
|
+
assertSortableUniqueId(event.suid);
|
|
361
|
+
if (prior !== "" && compareSortableUniqueId(event.suid, prior) <= 0) {
|
|
362
|
+
throw new TagStateSourceFailure(503, new Error("TagState source cursor did not advance"));
|
|
363
|
+
}
|
|
364
|
+
if (page.through !== "" && compareSortableUniqueId(event.suid, page.through) > 0) {
|
|
365
|
+
throw new TagStateSourceFailure(503, new Error("TagState source read beyond its frozen frontier"));
|
|
366
|
+
}
|
|
367
|
+
prior = event.suid;
|
|
368
|
+
}
|
|
369
|
+
const expectedLast = page.events.length === 0 ? cursor : prior;
|
|
370
|
+
if (page.lastSortableUniqueId !== expectedLast) {
|
|
371
|
+
throw new TagStateSourceFailure(503, new Error("TagState source cursor/result mismatch"));
|
|
372
|
+
}
|
|
373
|
+
if (page.completeThrough !== null && page.completeThrough !== page.through) {
|
|
374
|
+
throw new TagStateSourceFailure(503, new Error("TagState source completion frontier mismatch"));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
resolveProjector(projectorId) {
|
|
378
|
+
try {
|
|
379
|
+
const registry = this.registry;
|
|
380
|
+
if (registry === undefined) {
|
|
381
|
+
throw new TagStateRegistryFailure(new Error("TagState projector registry was not installed by the runtime composition"));
|
|
382
|
+
}
|
|
383
|
+
const projector = registry.resolve(projectorId);
|
|
384
|
+
if (projector === undefined)
|
|
385
|
+
throw new TagStateUnknownProjector();
|
|
386
|
+
return projector;
|
|
387
|
+
}
|
|
388
|
+
catch (caught) {
|
|
389
|
+
if (caught instanceof TagStateUnknownProjector || caught instanceof TagStateRegistryFailure)
|
|
390
|
+
throw caught;
|
|
391
|
+
throw new TagStateRegistryFailure(caught);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
ensureIdentity(identity) {
|
|
395
|
+
const row = this.sql().exec(`
|
|
396
|
+
SELECT service_id, tag, projector_id
|
|
397
|
+
FROM tag_state_identity
|
|
398
|
+
WHERE singleton = 1
|
|
399
|
+
`).toArray()[0];
|
|
400
|
+
if (row === undefined) {
|
|
401
|
+
this.transaction(() => {
|
|
402
|
+
this.sql().exec(`
|
|
403
|
+
INSERT INTO tag_state_identity (singleton, service_id, tag, projector_id)
|
|
404
|
+
VALUES (1, ?, ?, ?)
|
|
405
|
+
`, identity.serviceId, identity.tag, identity.projectorId);
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (sqlString(row.service_id, "tag_state_identity.service_id") !== identity.serviceId ||
|
|
410
|
+
sqlString(row.tag, "tag_state_identity.tag") !== identity.tag ||
|
|
411
|
+
sqlString(row.projector_id, "tag_state_identity.projector_id") !== identity.projectorId)
|
|
412
|
+
throw new TagStateIdentityConflict();
|
|
413
|
+
}
|
|
414
|
+
readCache() {
|
|
415
|
+
const row = this.sql().exec(`
|
|
416
|
+
SELECT phase, projector_version, state_json, last_suid,
|
|
417
|
+
target_projector_version, frozen_through, replay_cursor,
|
|
418
|
+
accumulator_json, rebuild_id
|
|
419
|
+
FROM tag_state_cache
|
|
420
|
+
WHERE singleton = 1
|
|
421
|
+
`).toArray()[0];
|
|
422
|
+
if (row === undefined)
|
|
423
|
+
return undefined;
|
|
424
|
+
const phase = sqlString(row.phase, "tag_state_cache.phase");
|
|
425
|
+
if (phase === "READY") {
|
|
426
|
+
return {
|
|
427
|
+
phase,
|
|
428
|
+
projectorVersion: sqlString(row.projector_version, "tag_state_cache.projector_version"),
|
|
429
|
+
stateJson: sqlString(row.state_json, "tag_state_cache.state_json"),
|
|
430
|
+
lastSuid: sqlString(row.last_suid, "tag_state_cache.last_suid"),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
if (phase === "REBUILDING") {
|
|
434
|
+
return {
|
|
435
|
+
phase,
|
|
436
|
+
targetProjectorVersion: sqlString(row.target_projector_version, "tag_state_cache.target_projector_version"),
|
|
437
|
+
frozenThrough: sqlNullableString(row.frozen_through, "tag_state_cache.frozen_through"),
|
|
438
|
+
replayCursor: sqlString(row.replay_cursor, "tag_state_cache.replay_cursor"),
|
|
439
|
+
accumulatorJson: sqlString(row.accumulator_json, "tag_state_cache.accumulator_json"),
|
|
440
|
+
rebuildId: sqlString(row.rebuild_id, "tag_state_cache.rebuild_id"),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
throw new TagStateCacheCorruption(new Error("TagState cache phase was invalid"));
|
|
444
|
+
}
|
|
445
|
+
beginRebuild(projector, _reason, existing = undefined) {
|
|
446
|
+
let accumulatorJson;
|
|
447
|
+
try {
|
|
448
|
+
accumulatorJson = existing?.accumulatorJson ?? projector.serializeState(projector.initialState());
|
|
449
|
+
// Validate the serialized state before it becomes a durable rebuild
|
|
450
|
+
// accumulator. A bad registry reducer is a registry failure, not an
|
|
451
|
+
// empty successful projection.
|
|
452
|
+
projector.deserializeState(accumulatorJson);
|
|
453
|
+
}
|
|
454
|
+
catch (caught) {
|
|
455
|
+
throw new TagStateRegistryFailure(caught);
|
|
456
|
+
}
|
|
457
|
+
const rebuild = {
|
|
458
|
+
phase: "REBUILDING",
|
|
459
|
+
targetProjectorVersion: projector.projectorVersion,
|
|
460
|
+
frozenThrough: existing?.frozenThrough ?? null,
|
|
461
|
+
replayCursor: existing?.cursor ?? "",
|
|
462
|
+
accumulatorJson,
|
|
463
|
+
rebuildId: crypto.randomUUID(),
|
|
464
|
+
};
|
|
465
|
+
this.transaction(() => {
|
|
466
|
+
this.sql().exec(`
|
|
467
|
+
INSERT INTO tag_state_cache (
|
|
468
|
+
singleton, phase, projector_version, state_json, last_suid,
|
|
469
|
+
target_projector_version, frozen_through, replay_cursor,
|
|
470
|
+
accumulator_json, rebuild_id
|
|
471
|
+
) VALUES (1, 'REBUILDING', NULL, NULL, '', ?, ?, ?, ?, ?)
|
|
472
|
+
ON CONFLICT(singleton) DO UPDATE SET
|
|
473
|
+
phase = excluded.phase,
|
|
474
|
+
projector_version = NULL,
|
|
475
|
+
state_json = NULL,
|
|
476
|
+
last_suid = '',
|
|
477
|
+
target_projector_version = excluded.target_projector_version,
|
|
478
|
+
frozen_through = excluded.frozen_through,
|
|
479
|
+
replay_cursor = excluded.replay_cursor,
|
|
480
|
+
accumulator_json = excluded.accumulator_json,
|
|
481
|
+
rebuild_id = excluded.rebuild_id
|
|
482
|
+
`, rebuild.targetProjectorVersion, rebuild.frozenThrough, rebuild.replayCursor, rebuild.accumulatorJson, rebuild.rebuildId);
|
|
483
|
+
});
|
|
484
|
+
return rebuild;
|
|
485
|
+
}
|
|
486
|
+
sql() {
|
|
487
|
+
const sql = this.ctx.storage.sql;
|
|
488
|
+
if (typeof sql?.exec !== "function")
|
|
489
|
+
throw new Error("TagState Durable Object requires SQLite storage");
|
|
490
|
+
return sql;
|
|
491
|
+
}
|
|
492
|
+
transaction(callback) {
|
|
493
|
+
const storage = this.ctx.storage;
|
|
494
|
+
if (typeof storage.transactionSync !== "function") {
|
|
495
|
+
throw new Error("TagState Durable Object requires SQLite transactionSync");
|
|
496
|
+
}
|
|
497
|
+
storage.transactionSync(callback);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommitTraceSnapshot } from "./CommitTrace";
|
|
2
|
+
export interface UnattributedRatio {
|
|
3
|
+
readonly rootDurationMs: number;
|
|
4
|
+
readonly coveredDurationMs: number;
|
|
5
|
+
readonly unattributedRatio: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The attribution numerator is the union of caller-domain coverage only.
|
|
9
|
+
* Callee, nested, fan-out-member, and provider-derived intervals are never
|
|
10
|
+
* eligible, even if a provider happens to attach a duration to them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function calculateUnattributedRatio(snapshot: CommitTraceSnapshot): UnattributedRatio;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import commitTraceManifestJson from "../../../../contracts/commit-trace-manifest.json";
|
|
2
|
+
function failure(code, message) {
|
|
3
|
+
const error = new Error(`${code}:${message}`);
|
|
4
|
+
error.name = "CommitTraceAttributionError";
|
|
5
|
+
throw error;
|
|
6
|
+
}
|
|
7
|
+
function clip(interval, root) {
|
|
8
|
+
const start = Math.max(interval[0], root[0]);
|
|
9
|
+
const end = Math.min(interval[1], root[1]);
|
|
10
|
+
return end < start ? undefined : [start, end];
|
|
11
|
+
}
|
|
12
|
+
function unionDuration(intervals) {
|
|
13
|
+
const sorted = [...intervals].sort((left, right) => left[0] - right[0] || left[1] - right[1]);
|
|
14
|
+
let duration = 0;
|
|
15
|
+
let current;
|
|
16
|
+
for (const interval of sorted) {
|
|
17
|
+
if (current === undefined) {
|
|
18
|
+
current = [interval[0], interval[1]];
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (interval[0] <= current[1]) {
|
|
22
|
+
current[1] = Math.max(current[1], interval[1]);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
duration += current[1] - current[0];
|
|
26
|
+
current = [interval[0], interval[1]];
|
|
27
|
+
}
|
|
28
|
+
return duration + (current === undefined ? 0 : current[1] - current[0]);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The attribution numerator is the union of caller-domain coverage only.
|
|
32
|
+
* Callee, nested, fan-out-member, and provider-derived intervals are never
|
|
33
|
+
* eligible, even if a provider happens to attach a duration to them.
|
|
34
|
+
*/
|
|
35
|
+
export function calculateUnattributedRatio(snapshot) {
|
|
36
|
+
if (snapshot.schema !== "sdt.commit/v1")
|
|
37
|
+
failure("ratio-schema", "unattributed ratio is defined for sdt.commit/v1 only");
|
|
38
|
+
const root = snapshot.spans.find((span) => span.rowId === "S00");
|
|
39
|
+
if (root === undefined)
|
|
40
|
+
failure("ratio-root-missing", "sdt.commit/v1 needs S00");
|
|
41
|
+
const manifest = commitTraceManifestJson;
|
|
42
|
+
const coverage = new Set(manifest.schemas["sdt.commit/v1"].callerCoverageIntervals);
|
|
43
|
+
const rows = new Map(manifest.schemas["sdt.commit/v1"].rows.map((row) => [row.rowId, row]));
|
|
44
|
+
const rootInterval = [root.startMs, root.endMs];
|
|
45
|
+
const intervals = snapshot.spans
|
|
46
|
+
.filter((span) => coverage.has(span.rowId))
|
|
47
|
+
.map((span) => {
|
|
48
|
+
const row = rows.get(span.rowId);
|
|
49
|
+
if (row === undefined)
|
|
50
|
+
failure("coverage-row", `coverage row ${span.rowId} is not manifest-declared`);
|
|
51
|
+
if (span.clockDomain !== "caller" ||
|
|
52
|
+
row.emitter === "callee-do" ||
|
|
53
|
+
row.kind === "nested" ||
|
|
54
|
+
row.kind === "fanout-member" ||
|
|
55
|
+
row.kind === "sequential-member") {
|
|
56
|
+
failure("coverage-noncaller", `${span.rowId} is forbidden from caller coverage`);
|
|
57
|
+
}
|
|
58
|
+
return clip([span.startMs, span.endMs], rootInterval);
|
|
59
|
+
})
|
|
60
|
+
.filter((interval) => interval !== undefined);
|
|
61
|
+
const rootDurationMs = Math.max(0, root.endMs - root.startMs);
|
|
62
|
+
const coveredDurationMs = Math.min(rootDurationMs, unionDuration(intervals));
|
|
63
|
+
return {
|
|
64
|
+
rootDurationMs,
|
|
65
|
+
coveredDurationMs,
|
|
66
|
+
unattributedRatio: rootDurationMs === 0 ? 0 : Math.max(0, rootDurationMs - coveredDurationMs) / rootDurationMs,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { NativeTracing } from "./CommitTrace";
|
|
2
|
+
/**
|
|
3
|
+
* The Worker runtime, not application code, establishes parentage. Both
|
|
4
|
+
* entrypoints expose the identical API; using the module form lets Durable
|
|
5
|
+
* Object handlers participate without adding a header or body field.
|
|
6
|
+
*/
|
|
7
|
+
export declare function cloudflareTracing(context?: Pick<ExecutionContext, "tracing">): NativeTracing;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { tracing } from "cloudflare:workers";
|
|
2
|
+
/**
|
|
3
|
+
* The Worker runtime, not application code, establishes parentage. Both
|
|
4
|
+
* entrypoints expose the identical API; using the module form lets Durable
|
|
5
|
+
* Object handlers participate without adding a header or body field.
|
|
6
|
+
*/
|
|
7
|
+
export function cloudflareTracing(context) {
|
|
8
|
+
return (context?.tracing ?? tracing);
|
|
9
|
+
}
|