@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,86 @@
1
+ import type { PipelineClock } from "../downstream/types";
2
+ import type { StoreProvider } from "../store/provider";
3
+ import type { PipelineStore } from "../store/types";
4
+ import type { ClosedPrefixCertificate } from "../allocator/types";
5
+ import { type ProjectorRegistry } from "./ProjectorRegistry";
6
+ import { type CatchUpResult, type SafeViewCoverageContext } from "./ProjectionRuntime";
7
+ import { type ServiceIdentityProvider } from "../service/ServiceIdentityProvider";
8
+ export interface LiveProjectionEnv {
9
+ POSTGRES_URL?: string;
10
+ HYPERDRIVE?: Hyperdrive;
11
+ /** Optional explicit D1 provider binding; Postgres remains the default. */
12
+ D1?: D1Database;
13
+ /** Non-secret service identity configured per deployment. */
14
+ SDT_SERVICE_ID?: string;
15
+ BOOTSTRAP?: DurableObjectNamespace;
16
+ /** Internal trusted certificate producer; never used on ordinary poll paths. */
17
+ ALLOCATOR?: DurableObjectNamespace;
18
+ }
19
+ export declare const LIVE_PROJECTION_POLL_OUTCOMES: readonly ["never-invoked", "invoked-and-threw", "invoked-but-no-work", "explicitly-gated", "advanced"];
20
+ export type LiveProjectionPollOutcome = (typeof LIVE_PROJECTION_POLL_OUTCOMES)[number];
21
+ export interface LiveProjectionPollObservation {
22
+ readonly serviceId: string;
23
+ readonly projectorId: string;
24
+ readonly attemptedAt: number;
25
+ readonly outcome: LiveProjectionPollOutcome;
26
+ readonly reason: string | null;
27
+ readonly advancedSourceEvents: number;
28
+ }
29
+ export interface LiveProjectionPollObserver {
30
+ /** Called before bootstrap admission/store initialization is attempted. */
31
+ readonly onAttempt?: (input: {
32
+ readonly env: LiveProjectionEnv;
33
+ readonly serviceId: string;
34
+ readonly projectorIds: readonly string[];
35
+ readonly attemptedAt: number;
36
+ }) => Promise<void> | void;
37
+ /** Called once per registered projector after a poll returns or throws. */
38
+ readonly onOutcome?: (input: LiveProjectionPollObservation & {
39
+ readonly env: LiveProjectionEnv;
40
+ }) => Promise<void> | void;
41
+ }
42
+ export interface ProjectionPollOptions {
43
+ clock?: PipelineClock;
44
+ store?: PipelineStore;
45
+ storeProvider?: StoreProvider;
46
+ registry?: ProjectorRegistry;
47
+ /** Test-only service isolation; scheduled production polls require SDT_SERVICE_ID. */
48
+ serviceId?: string;
49
+ /** Optional single tag scope for queue/HTTP operator catch-up. */
50
+ tag?: string;
51
+ /**
52
+ * Optional G44-proven high-water mark for a scheduled poll. `null` keeps
53
+ * the poll observable but permits no source advancement on BLOCK/UNSETTLED;
54
+ * `undefined` retains the unbounded FULL/on-demand behavior.
55
+ */
56
+ maximumSuid?: string | null;
57
+ /**
58
+ * Explicitly selects the safe-view advance decision. Only this boundary
59
+ * validates the cached closed-prefix certificate; ordinary callers leave it
60
+ * false and retain their existing projection behavior.
61
+ */
62
+ safeViewAdvance?: boolean;
63
+ /** Internal spelling retained for callers that already model the gate. */
64
+ requireClosedPrefixCertificate?: boolean;
65
+ /** Optional certificate prefix supplied alongside the cached certificate. */
66
+ closedPrefixSuid?: string | null;
67
+ /** Consumer-bound allocator certificate for a safe-view decision. */
68
+ closedPrefixCertificate?: ClosedPrefixCertificate;
69
+ /** Cached allocator generation used to bind the certificate. */
70
+ allocatorLineageId?: string;
71
+ /** Existing G44/G62 proven coverage and start-of-pass handoff. */
72
+ safeViewCoverage?: SafeViewCoverageContext;
73
+ serviceIdentityProvider?: ServiceIdentityProvider;
74
+ /** Observation-only lifecycle sink; it cannot alter projection decisions. */
75
+ observer?: LiveProjectionPollObserver;
76
+ }
77
+ /**
78
+ * Scheduled polling entry point. It discovers durable tag memberships and
79
+ * advances every deploy-time registered projector only through SafeWindow.
80
+ */
81
+ export declare function pollLiveProjections(env: LiveProjectionEnv, options?: ProjectionPollOptions): Promise<CatchUpResult[]>;
82
+ /**
83
+ * Internal operational surface; it is deliberately outside the five V1
84
+ * endpoints and does not advance a projection or write event data.
85
+ */
86
+ export declare function handleProjectionLag(request: Request, env: LiveProjectionEnv, registry?: ProjectorRegistry, storeProvider?: StoreProvider, serviceIdentityProvider?: ServiceIdentityProvider): Promise<Response>;
@@ -0,0 +1,240 @@
1
+ import { systemPipelineClock } from "../downstream/types";
2
+ import { DEPLOYED_PROJECTOR_REGISTRY, tagStateIdentityFrom, } from "./ProjectorRegistry";
3
+ import { projectionIdFor, ProjectionRuntime, safeWindowMs, validatedClosedPrefixSuid, validatedSafeViewCoverageMaximumSuid, } from "./ProjectionRuntime";
4
+ import { scopeIdFor } from "../scope/ScopeName";
5
+ import { envServiceIdentity, requireServiceIdentity } from "../service/ServiceIdentityProvider";
6
+ export const LIVE_PROJECTION_POLL_OUTCOMES = [
7
+ "never-invoked",
8
+ "invoked-and-threw",
9
+ "invoked-but-no-work",
10
+ "explicitly-gated",
11
+ "advanced",
12
+ ];
13
+ function sharedStore(env, provider) {
14
+ // Provider clients are request-scoped in workerd; a retained client can
15
+ // otherwise be used by a later cron/Queue request.
16
+ return provider.create(env);
17
+ }
18
+ function json(body, status = 200) {
19
+ return new Response(JSON.stringify(body), {
20
+ status,
21
+ headers: { "content-type": "application/json; charset=utf-8" },
22
+ });
23
+ }
24
+ function error(status, code, message) {
25
+ return json({ error: message, code }, status);
26
+ }
27
+ async function acquireClosedPrefixCertificate(env, serviceId) {
28
+ if (env.ALLOCATOR === undefined) {
29
+ throw new Error("ordering_certificate_unavailable");
30
+ }
31
+ const url = new URL("https://projection.internal/__internal/g77/certificate");
32
+ url.searchParams.set("serviceId", serviceId);
33
+ const response = await env.ALLOCATOR.get(scopeIdFor(env.ALLOCATOR, {
34
+ serviceId,
35
+ doClass: "allocator",
36
+ identity: "allocator",
37
+ })).fetch(new Request(url));
38
+ if (!response.ok) {
39
+ throw new Error("ordering_certificate_unavailable");
40
+ }
41
+ return response.json();
42
+ }
43
+ async function admitBootstrapRoute(env, serviceId) {
44
+ if (env.BOOTSTRAP === undefined)
45
+ return;
46
+ const url = new URL("https://projection.internal/route/check");
47
+ url.searchParams.set("__serviceId", serviceId);
48
+ const admitted = await env.BOOTSTRAP.get(scopeIdFor(env.BOOTSTRAP, {
49
+ serviceId,
50
+ doClass: "bootstrap",
51
+ identity: "coordinator",
52
+ })).fetch(new Request(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ route: "projection-rebuild" }) }));
53
+ if (!admitted.ok)
54
+ throw new Error("bootstrap_route_rejected:projection-rebuild");
55
+ }
56
+ function boundedErrorReason(error) {
57
+ const value = error instanceof Error ? error.message : String(error);
58
+ return value.length > 256 ? `${value.slice(0, 253)}...` : value;
59
+ }
60
+ async function notifyObserver(observer, callback, input) {
61
+ // Health persistence is deliberately ancillary: a D1 diagnostic outage must
62
+ // never change commit/projection semantics or turn a successful poll into a
63
+ // failed scheduled invocation.
64
+ try {
65
+ if (callback === "onAttempt") {
66
+ await observer?.onAttempt?.(input);
67
+ }
68
+ else {
69
+ await observer?.onOutcome?.(input);
70
+ }
71
+ }
72
+ catch {
73
+ // The next health read will retain the last durable observation and its
74
+ // reason; projection advancement remains governed by the existing fences.
75
+ }
76
+ }
77
+ async function notifyOutcomes(observer, serviceId, projectorIds, attemptedAt, results, maximumSuid, env, error) {
78
+ if (observer?.onOutcome === undefined)
79
+ return;
80
+ const byProjector = new Map();
81
+ for (const result of results) {
82
+ const values = byProjector.get(result.projectorId) ?? [];
83
+ values.push(result);
84
+ byProjector.set(result.projectorId, values);
85
+ }
86
+ for (const projectorId of projectorIds) {
87
+ const projectorResults = byProjector.get(projectorId) ?? [];
88
+ let outcome = "invoked-but-no-work";
89
+ let reason = null;
90
+ let advancedSourceEvents = 0;
91
+ if (error !== undefined) {
92
+ outcome = "invoked-and-threw";
93
+ reason = boundedErrorReason(error);
94
+ }
95
+ else {
96
+ advancedSourceEvents = projectorResults.reduce((total, result) => total + result.advancedSourceEvents, 0);
97
+ if (advancedSourceEvents > 0) {
98
+ outcome = "advanced";
99
+ }
100
+ else if (projectorResults.some((result) => result.indeterminate)) {
101
+ outcome = "explicitly-gated";
102
+ reason = "safe_window_ceiling_exceeded";
103
+ }
104
+ else if (maximumSuid !== undefined) {
105
+ outcome = "explicitly-gated";
106
+ reason = maximumSuid === null ? "retained_frontier_unproven" : "retained_frontier_fence";
107
+ }
108
+ }
109
+ await notifyObserver(observer, "onOutcome", {
110
+ serviceId,
111
+ projectorId,
112
+ attemptedAt,
113
+ outcome,
114
+ reason,
115
+ advancedSourceEvents,
116
+ env,
117
+ });
118
+ }
119
+ }
120
+ /**
121
+ * Scheduled polling entry point. It discovers durable tag memberships and
122
+ * advances every deploy-time registered projector only through SafeWindow.
123
+ */
124
+ export async function pollLiveProjections(env, options = {}) {
125
+ if (options.store === undefined && options.storeProvider === undefined) {
126
+ throw new Error("A projection store provider is not configured");
127
+ }
128
+ const serviceId = options.serviceId ?? requireServiceIdentity(options.serviceIdentityProvider ?? envServiceIdentity(env));
129
+ const registry = options.registry ?? DEPLOYED_PROJECTOR_REGISTRY;
130
+ const projectorIds = registry.registered().map((projector) => projector.id);
131
+ const safeViewAdvance = options.safeViewAdvance === true || options.requireClosedPrefixCertificate === true;
132
+ const closedPrefixCertificate = safeViewAdvance
133
+ ? options.closedPrefixCertificate ?? await acquireClosedPrefixCertificate(env, serviceId)
134
+ : undefined;
135
+ const closedPrefixSuid = safeViewAdvance
136
+ ? validatedClosedPrefixSuid({
137
+ closedPrefixSuid: options.closedPrefixSuid,
138
+ closedPrefixCertificate,
139
+ expectedServiceId: serviceId,
140
+ expectedAllocatorLineageId: options.allocatorLineageId,
141
+ }, serviceId)
142
+ : undefined;
143
+ if (safeViewAdvance) {
144
+ validatedSafeViewCoverageMaximumSuid(options.safeViewCoverage, serviceId);
145
+ }
146
+ const attemptedAt = (options.clock ?? systemPipelineClock).now();
147
+ await notifyObserver(options.observer, "onAttempt", { env, serviceId, projectorIds, attemptedAt });
148
+ try {
149
+ await admitBootstrapRoute(env, serviceId);
150
+ const store = options.store ?? sharedStore(env, options.storeProvider);
151
+ await store.initialize();
152
+ const runtime = new ProjectionRuntime(store, registry);
153
+ if (options.tag !== undefined) {
154
+ const results = [];
155
+ for (const projector of registry.registered()) {
156
+ const identity = tagStateIdentityFrom(`${options.tag}:${projector.id}`, registry);
157
+ if (identity.value !== undefined) {
158
+ if (safeViewAdvance) {
159
+ results.push(await runtime.catchUp(serviceId, identity.value, attemptedAt, {}, {
160
+ maximumSuid: options.maximumSuid,
161
+ closedPrefixSuid,
162
+ closedPrefixCertificate,
163
+ requireClosedPrefixCertificate: true,
164
+ expectedServiceId: serviceId,
165
+ expectedAllocatorLineageId: options.allocatorLineageId,
166
+ safeViewCoverage: options.safeViewCoverage,
167
+ }));
168
+ }
169
+ else {
170
+ results.push(await runtime.catchUp(serviceId, identity.value, attemptedAt, {}, { maximumSuid: options.maximumSuid }));
171
+ }
172
+ }
173
+ }
174
+ await notifyOutcomes(options.observer, serviceId, projectorIds, attemptedAt, results, options.maximumSuid, env);
175
+ return results;
176
+ }
177
+ if (safeViewAdvance) {
178
+ const results = await runtime.pollRegistered(serviceId, attemptedAt, options.maximumSuid, closedPrefixSuid, closedPrefixCertificate, true, serviceId, options.allocatorLineageId, options.safeViewCoverage);
179
+ await notifyOutcomes(options.observer, serviceId, projectorIds, attemptedAt, results, options.maximumSuid, env);
180
+ return results;
181
+ }
182
+ const results = await runtime.pollRegistered(serviceId, attemptedAt, options.maximumSuid);
183
+ await notifyOutcomes(options.observer, serviceId, projectorIds, attemptedAt, results, options.maximumSuid, env);
184
+ return results;
185
+ }
186
+ catch (error) {
187
+ await notifyOutcomes(options.observer, serviceId, projectorIds, attemptedAt, [], options.maximumSuid, env, error);
188
+ throw error;
189
+ }
190
+ }
191
+ /**
192
+ * Internal operational surface; it is deliberately outside the five V1
193
+ * endpoints and does not advance a projection or write event data.
194
+ */
195
+ export async function handleProjectionLag(request, env, registry = DEPLOYED_PROJECTOR_REGISTRY, storeProvider, serviceIdentityProvider) {
196
+ if (request.method !== "GET") {
197
+ return error(405, "validation_error", "Projection lag requires GET");
198
+ }
199
+ const query = new URL(request.url).searchParams;
200
+ const tagStateId = query.get("tagStateId");
201
+ const serviceId = query.get("serviceId") || requireServiceIdentity(serviceIdentityProvider ?? envServiceIdentity(env));
202
+ const pollRequested = query.get("poll") === "1";
203
+ if (tagStateId === null || tagStateId.length === 0) {
204
+ return error(400, "validation_error", "tagStateId is required");
205
+ }
206
+ const parsed = tagStateIdentityFrom(tagStateId, registry);
207
+ if (parsed.value === undefined) {
208
+ return error(400, "validation_error", parsed.error ?? "Invalid tagStateId");
209
+ }
210
+ try {
211
+ if (storeProvider === undefined) {
212
+ return error(503, "projection_unavailable", "A projection store provider is not configured");
213
+ }
214
+ const store = sharedStore(env, storeProvider);
215
+ await store.initialize();
216
+ if (pollRequested) {
217
+ // The operator probe names one tag-state. Catch up only that identity;
218
+ // polling every registered tag here can exceed an HTTP request lifetime
219
+ // on a large service-scoped conformance run.
220
+ const runtime = new ProjectionRuntime(store, registry);
221
+ await runtime.catchUp(serviceId, parsed.value, Date.now());
222
+ }
223
+ const projectionId = projectionIdFor(parsed.value);
224
+ const [lag, dynamicLagBoundMs] = await Promise.all([
225
+ store.projectionLag(serviceId, projectionId, parsed.value.tag),
226
+ store.currentLagBound(serviceId, Date.now()),
227
+ ]);
228
+ return json({
229
+ tagStateId,
230
+ checkpointSuid: lag.checkpointSuid,
231
+ headSuid: lag.headSuid,
232
+ behindEvents: lag.behindEvents,
233
+ dynamicLagBoundMs,
234
+ safeWindowMs: safeWindowMs(dynamicLagBoundMs),
235
+ });
236
+ }
237
+ catch {
238
+ return error(503, "projection_unavailable", "Projection lag storage is unavailable");
239
+ }
240
+ }
@@ -0,0 +1,113 @@
1
+ import type { TagEvent } from "../tag/types";
2
+ import type { ClosedPrefixCertificate } from "../allocator/types";
3
+ import type { ProjectionCheckpoint, ProjectionStore, StoredEvent } from "../store/types";
4
+ import { type ProjectorRegistry, type TagStateIdentity, type TagStateProjector } from "./ProjectorRegistry";
5
+ import { decayedLagEstimateMs, MAX_PUBLISHED_SAFE_WINDOW_MS, PUBLISHED_SAFE_WINDOW_MS, safeWindowCeilingExceeded, safeWindowCutoffSuid, safeWindowMs } from "../safeWindow";
6
+ /** Published V1 read-side SafeWindow. It never authorizes a commit. */
7
+ export { MAX_PUBLISHED_SAFE_WINDOW_MS, PUBLISHED_SAFE_WINDOW_MS, safeWindowCeilingExceeded, safeWindowCutoffSuid, safeWindowMs, };
8
+ export declare class ProjectionCheckpointCorruption extends Error {
9
+ constructor(cause?: unknown);
10
+ }
11
+ /**
12
+ * Live projection identities have independent checkpoints. A bounded worker
13
+ * pool prevents a large retained tag set from monopolizing one cron
14
+ * invocation while keeping D1/CAS pressure finite and deterministic.
15
+ */
16
+ export declare const MAX_LIVE_PROJECTION_CONCURRENCY = 8;
17
+ export interface ProjectedTagState {
18
+ payload: string;
19
+ version: number;
20
+ stateJson: string;
21
+ }
22
+ export interface CatchUpHooks {
23
+ /** Test-only crash boundary before the state/checkpoint transaction. */
24
+ beforeCheckpoint?(event: StoredEvent, checkpoint: ProjectionCheckpoint): Promise<void> | void;
25
+ /** Test-only crash boundary after the state/checkpoint transaction. */
26
+ afterCheckpoint?(event: StoredEvent, checkpoint: ProjectionCheckpoint): Promise<void> | void;
27
+ }
28
+ /**
29
+ * Optional scanner-proven high-water mark for a scheduled live-projection
30
+ * pass. `null` means that no settled frontier exists, so the pass may run
31
+ * and observe projection state but cannot advance a source checkpoint.
32
+ * `undefined` retains the ordinary unbounded caller behaviour used by a
33
+ * proven FULL scan and by on-demand operator reads.
34
+ */
35
+ export interface ProjectionCatchUpOptions {
36
+ readonly maximumSuid?: string | null;
37
+ /** A prefix already validated by the safe-view decision boundary. */
38
+ readonly closedPrefixSuid?: string | null;
39
+ /** Cached allocator authority, validated only for an explicitly safe pass. */
40
+ readonly closedPrefixCertificate?: ClosedPrefixCertificate;
41
+ /** Marks this invocation as the certificate-gated safe-view decision. */
42
+ readonly requireClosedPrefixCertificate?: boolean;
43
+ /** Consumer identity used to bind the cached certificate. */
44
+ readonly expectedServiceId?: string;
45
+ /** Allocator generation expected by the consumer. */
46
+ readonly expectedAllocatorLineageId?: string;
47
+ /** Existing G44/G62 proof context for this safe-view pass. */
48
+ readonly safeViewCoverage?: SafeViewCoverageContext;
49
+ }
50
+ /**
51
+ * Handoff from the existing G44/G62 coverage path to the one safe-view
52
+ * decision. `startOfPass: "PROVEN"` makes the proof domain explicit; FULL is
53
+ * the only context that may leave the existing maximumSuid bound unbounded.
54
+ * This runtime validates the handoff and does not mint scanner authority.
55
+ */
56
+ export type SafeViewCoverageContext = Readonly<{
57
+ readonly authority: "g44-g62";
58
+ readonly serviceId: string;
59
+ readonly startOfPass: "PROVEN";
60
+ readonly kind: "FULL";
61
+ readonly frontierSuid: null;
62
+ }> | Readonly<{
63
+ readonly authority: "g44-g62";
64
+ readonly serviceId: string;
65
+ readonly startOfPass: "PROVEN";
66
+ readonly kind: "SETTLED" | "BLOCK/UNSETTLED";
67
+ readonly frontierSuid: string | null;
68
+ }>;
69
+ export type ClosedPrefixCertificateValidationOptions = Pick<ProjectionCatchUpOptions, "closedPrefixSuid" | "closedPrefixCertificate" | "expectedServiceId" | "expectedAllocatorLineageId">;
70
+ /**
71
+ * Validate the allocator fact at the one boundary that decides whether a
72
+ * safe view may advance. Callers that only inspect, rebuild, or perform an
73
+ * ordinary SafeWindow catch-up must not call this function.
74
+ */
75
+ export declare function validatedClosedPrefixSuid(options: ClosedPrefixCertificateValidationOptions, consumerServiceId: string): string | null;
76
+ /**
77
+ * Validate the existing completeness handoff independently of the allocator
78
+ * certificate. The returned bound is deliberately separate from
79
+ * `maximumSuid`, whose G44 stop condition remains an independent gate.
80
+ */
81
+ export declare function validatedSafeViewCoverageMaximumSuid(context: SafeViewCoverageContext | undefined, consumerServiceId: string): string | null | undefined;
82
+ export interface CatchUpResult {
83
+ /** Registered projector and tag identity for scheduled-poll diagnostics. */
84
+ readonly projectorId: string;
85
+ readonly tag: string;
86
+ checkpoint: ProjectionCheckpoint | undefined;
87
+ dynamicLagBoundMs: number;
88
+ safeWindowMs: number;
89
+ /** True means the published 120s ceiling was exceeded; no source event was advanced. */
90
+ indeterminate: boolean;
91
+ advancedSourceEvents: number;
92
+ appliedEvents: number;
93
+ }
94
+ export declare function projectionIdFor(identity: TagStateIdentity): string;
95
+ export { decayedLagEstimateMs };
96
+ /**
97
+ * Deterministically rebuilds one tag-state from the Tag DO's authoritative
98
+ * durable event list. This is the on-demand §5.3 catch-up path; it does not
99
+ * mutate the read-side SafeWindow checkpoint.
100
+ */
101
+ export declare function catchUpDurableTagState(projector: TagStateProjector, events: readonly TagEvent[]): ProjectedTagState;
102
+ /**
103
+ * Polls a global SUID source for one tag/projector. Every source event advances
104
+ * the durable checkpoint, while the registered reducer changes state only for
105
+ * events whose complete durable tag membership includes the target tag.
106
+ */
107
+ export declare class ProjectionRuntime {
108
+ private readonly store;
109
+ private readonly registry;
110
+ constructor(store: ProjectionStore, registry?: ProjectorRegistry);
111
+ catchUp(serviceId: string, identity: TagStateIdentity, nowMs: number, hooks?: CatchUpHooks, options?: ProjectionCatchUpOptions): Promise<CatchUpResult>;
112
+ pollRegistered(serviceId: string, nowMs: number, maximumSuid?: string | null, closedPrefixSuid?: string | null, closedPrefixCertificate?: ClosedPrefixCertificate, requireClosedPrefixCertificate?: boolean, expectedServiceId?: string, expectedAllocatorLineageId?: string, safeViewCoverage?: SafeViewCoverageContext): Promise<CatchUpResult[]>;
113
+ }