@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,2357 @@
1
+ // src/allocator/SortableUniqueId.ts
2
+ var SORTABLE_UNIQUE_ID_TICKS_DIGITS = 19;
3
+ var SORTABLE_UNIQUE_ID_RANDOM_DIGITS = 11;
4
+ var SORTABLE_UNIQUE_ID_DIGITS = SORTABLE_UNIQUE_ID_TICKS_DIGITS + SORTABLE_UNIQUE_ID_RANDOM_DIGITS;
5
+ var DOTNET_UNIX_EPOCH_TICKS = 621355968000000000n;
6
+ var DOTNET_MAX_TICKS = 3155378975999999999n;
7
+ var DOTNET_TICKS_PER_MILLISECOND = 10000n;
8
+ var SORTABLE_UNIQUE_ID_RANDOM_LIMIT = 10n ** BigInt(SORTABLE_UNIQUE_ID_RANDOM_DIGITS);
9
+ var MAX_UNIX_MILLISECONDS = (DOTNET_MAX_TICKS - DOTNET_UNIX_EPOCH_TICKS) / DOTNET_TICKS_PER_MILLISECOND;
10
+ var SortableUniqueIdError = class extends Error {
11
+ constructor(code, message, options) {
12
+ super(message, options);
13
+ this.code = code;
14
+ this.name = "SortableUniqueIdError";
15
+ }
16
+ code;
17
+ };
18
+ var LEGACY_PREFIXED_SUID = /^suid-[0-9]+$/;
19
+ var LEGACY_DECISION_OBSERVER = /* @__PURE__ */ Symbol.for("@sekiban/dcb-runtime/legacy-sortable-unique-id-observer");
20
+ function legacyDecisionObserver() {
21
+ return globalThis[LEGACY_DECISION_OBSERVER];
22
+ }
23
+ function canonicalDecimal(value, name) {
24
+ if (!/^(?:0|[1-9][0-9]*)$/.test(value)) {
25
+ throw new SortableUniqueIdError("SUID_UNIX_MILLISECONDS_INVALID", `${name} must be canonical ASCII decimal text`);
26
+ }
27
+ return BigInt(value);
28
+ }
29
+ function asUnixMilliseconds(value) {
30
+ if (typeof value === "number") {
31
+ if (!Number.isSafeInteger(value) || value < 0) {
32
+ throw new SortableUniqueIdError("SUID_UNIX_MILLISECONDS_INVALID", "Unix milliseconds must be a non-negative safe integer");
33
+ }
34
+ return BigInt(value);
35
+ }
36
+ if (typeof value === "string") return canonicalDecimal(value, "Unix milliseconds");
37
+ if (value < 0n) {
38
+ throw new SortableUniqueIdError("SUID_UNIX_MILLISECONDS_INVALID", "Unix milliseconds must be non-negative");
39
+ }
40
+ return value;
41
+ }
42
+ function unixMsToDotNetTicks(value) {
43
+ const milliseconds = asUnixMilliseconds(value);
44
+ const ticks = milliseconds * DOTNET_TICKS_PER_MILLISECOND + DOTNET_UNIX_EPOCH_TICKS;
45
+ if (ticks < 0n || ticks > DOTNET_MAX_TICKS) {
46
+ throw new SortableUniqueIdError("SUID_TICKS_INVALID", "Unix milliseconds are outside the .NET DateTime tick range");
47
+ }
48
+ return ticks;
49
+ }
50
+ function assertDotNetTicks(ticks) {
51
+ if (ticks < 0n || ticks > DOTNET_MAX_TICKS) {
52
+ throw new SortableUniqueIdError("SUID_TICKS_INVALID", "SortableUniqueId ticks are outside the .NET DateTime range");
53
+ }
54
+ return ticks;
55
+ }
56
+ function assertSortableUniqueId(value) {
57
+ if (typeof value === "string" && LEGACY_PREFIXED_SUID.test(value)) {
58
+ legacyDecisionObserver()?.(value);
59
+ throw new SortableUniqueIdError(
60
+ "SUID_LEGACY_FORMAT_RETIRED",
61
+ "Legacy prefixed SortableUniqueId is retired; G32 requires exactly 30 ASCII decimal digits"
62
+ );
63
+ }
64
+ if (typeof value !== "string" || !new RegExp(`^[0-9]{${SORTABLE_UNIQUE_ID_DIGITS}}$`).test(value)) {
65
+ throw new SortableUniqueIdError("SUID_INVALID", "SortableUniqueId must be exactly 30 ASCII decimal digits");
66
+ }
67
+ const ticksText = value.slice(0, SORTABLE_UNIQUE_ID_TICKS_DIGITS);
68
+ const randomText = value.slice(SORTABLE_UNIQUE_ID_TICKS_DIGITS);
69
+ const ticks = assertDotNetTicks(BigInt(ticksText));
70
+ const random = BigInt(randomText);
71
+ if (random < 0n || random >= SORTABLE_UNIQUE_ID_RANDOM_LIMIT) {
72
+ throw new SortableUniqueIdError("SUID_RANDOM_INVALID", "SortableUniqueId random suffix is outside its 11-digit range");
73
+ }
74
+ return Object.freeze({ value, ticks, random });
75
+ }
76
+ function formatSortableUniqueId(ticks, random) {
77
+ assertDotNetTicks(ticks);
78
+ if (random < 0n || random >= SORTABLE_UNIQUE_ID_RANDOM_LIMIT) {
79
+ throw new SortableUniqueIdError("SUID_RANDOM_INVALID", "SortableUniqueId random suffix is outside its 11-digit range");
80
+ }
81
+ return `${ticks.toString().padStart(SORTABLE_UNIQUE_ID_TICKS_DIGITS, "0")}${random.toString().padStart(SORTABLE_UNIQUE_ID_RANDOM_DIGITS, "0")}`;
82
+ }
83
+ function compareSortableUniqueId(left, right) {
84
+ const leftValue = assertSortableUniqueId(left).value;
85
+ const rightValue = assertSortableUniqueId(right).value;
86
+ return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
87
+ }
88
+
89
+ // src/safeWindow.ts
90
+ var PUBLISHED_SAFE_WINDOW_MS = 2e4;
91
+ var MAX_PUBLISHED_SAFE_WINDOW_MS = 12e4;
92
+ function safeWindowMs(dynamicLagBoundMs) {
93
+ return Math.min(MAX_PUBLISHED_SAFE_WINDOW_MS, Math.max(PUBLISHED_SAFE_WINDOW_MS, dynamicLagBoundMs));
94
+ }
95
+ function safeWindowCeilingExceeded(dynamicLagBoundMs) {
96
+ return dynamicLagBoundMs > MAX_PUBLISHED_SAFE_WINDOW_MS;
97
+ }
98
+ function safeWindowCutoffSuid(nowMs, dynamicLagBoundMs) {
99
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0) throw new Error("SafeWindow now must be a non-negative safe integer");
100
+ const cutoffMs = Math.max(0, nowMs - safeWindowMs(dynamicLagBoundMs));
101
+ return formatSortableUniqueId(unixMsToDotNetTicks(cutoffMs), 0n);
102
+ }
103
+ function isSortableUniqueIdSafeAt(suid, nowMs, dynamicLagBoundMs) {
104
+ assertSortableUniqueId(suid);
105
+ return compareSortableUniqueId(suid, safeWindowCutoffSuid(nowMs, dynamicLagBoundMs)) <= 0;
106
+ }
107
+
108
+ // src/mv/MaterializedViewRuntime.ts
109
+ var MAX_CHECKPOINT_CAS_RETRIES = 8;
110
+ var MaterializedViewOperationError = class extends Error {
111
+ constructor(operation, code, message) {
112
+ super(message);
113
+ this.operation = operation;
114
+ this.code = code;
115
+ this.name = "MaterializedViewOperationError";
116
+ }
117
+ operation;
118
+ code;
119
+ };
120
+ function compareSuid(left, right) {
121
+ const leftBytes = new TextEncoder().encode(left);
122
+ const rightBytes = new TextEncoder().encode(right);
123
+ const sharedLength = Math.min(leftBytes.length, rightBytes.length);
124
+ for (let index = 0; index < sharedLength; index += 1) {
125
+ const difference = leftBytes[index] - rightBytes[index];
126
+ if (difference !== 0) {
127
+ return difference;
128
+ }
129
+ }
130
+ return leftBytes.length - rightBytes.length;
131
+ }
132
+ function nonEmpty(value) {
133
+ return value.length > 0;
134
+ }
135
+ function materializedViewId(definitionId) {
136
+ return `mv:${definitionId}`;
137
+ }
138
+ function materializedViewCandidateId(definitionId, rebuildId) {
139
+ return `${materializedViewId(definitionId)}:rebuild:${rebuildId}`;
140
+ }
141
+ var MaterializedViewRuntime = class {
142
+ constructor(store) {
143
+ this.store = store;
144
+ }
145
+ store;
146
+ async read(serviceId, definition) {
147
+ const viewId = this.checkedViewId(definition, "follow");
148
+ const checkpoint = await this.store.readProjectionCheckpoint(serviceId, viewId);
149
+ return checkpoint === void 0 ? void 0 : this.snapshot(serviceId, viewId, checkpoint, definition, "follow");
150
+ }
151
+ /** Creates the initial durable state then follows through the SafeWindow. */
152
+ async build(serviceId, definition, nowMs, hooks = {}) {
153
+ try {
154
+ const viewId = this.checkedViewId(definition, "build");
155
+ await this.ensureInitialCheckpoint(serviceId, viewId, definition, nowMs, "build", true);
156
+ return await this.followCheckpoint(serviceId, viewId, definition, nowMs, "build", hooks);
157
+ } catch (error) {
158
+ throw this.typedFailure("build", error);
159
+ }
160
+ }
161
+ /** Continues the promoted durable view from its own global SUID checkpoint. */
162
+ async follow(serviceId, definition, nowMs, hooks = {}) {
163
+ try {
164
+ const viewId = this.checkedViewId(definition, "follow");
165
+ await this.ensureInitialCheckpoint(serviceId, viewId, definition, nowMs, "follow", false);
166
+ return await this.followCheckpoint(serviceId, viewId, definition, nowMs, "follow", hooks);
167
+ } catch (error) {
168
+ throw this.typedFailure("follow", error);
169
+ }
170
+ }
171
+ /**
172
+ * Builds a separate durable candidate from the beginning of the global
173
+ * source. The caller must explicitly promote it; no rebuild silently swaps
174
+ * production state.
175
+ */
176
+ async rebuild(serviceId, definition, rebuildId, nowMs, hooks = {}) {
177
+ try {
178
+ const definitionId = this.checkedDefinitionId(definition, "rebuild");
179
+ if (!nonEmpty(rebuildId)) {
180
+ throw this.failure("rebuild", "MV_REBUILD_ID_INVALID", "Materialized-view rebuildId is required");
181
+ }
182
+ const candidateId = materializedViewCandidateId(definitionId, rebuildId);
183
+ await this.ensureInitialCheckpoint(serviceId, candidateId, definition, nowMs, "rebuild", true);
184
+ return {
185
+ candidateId,
186
+ result: await this.followCheckpoint(serviceId, candidateId, definition, nowMs, "rebuild", hooks)
187
+ };
188
+ } catch (error) {
189
+ throw this.typedFailure("rebuild", error);
190
+ }
191
+ }
192
+ /** Resets the promoted view to a durable initial state, never a no-op. */
193
+ async reset(serviceId, definition, nowMs) {
194
+ try {
195
+ const viewId = this.checkedViewId(definition, "reset");
196
+ const existing = await this.store.readProjectionCheckpoint(serviceId, viewId);
197
+ if (existing === void 0) {
198
+ throw this.failure("reset", "MV_RESET_MISSING", "Cannot reset a materialized view that was never built");
199
+ }
200
+ const initial = this.checkpointFor(serviceId, viewId, "", this.initialState(definition, "reset"), definition, nowMs, "reset");
201
+ if (existing.lastSuid === initial.lastSuid && existing.stateJson === initial.stateJson && existing.version === initial.version) {
202
+ throw this.failure("reset", "MV_RESET_NOTHING_TO_RESET", "Materialized view is already at its durable initial state");
203
+ }
204
+ const advanced = await this.store.advanceProjectionCheckpoint({
205
+ ...initial,
206
+ expectedLastSuid: existing.lastSuid
207
+ });
208
+ if (!advanced) {
209
+ throw this.failure("reset", "MV_RESET_CONFLICT", "Materialized-view checkpoint changed before reset could commit");
210
+ }
211
+ return this.snapshot(serviceId, viewId, initial, definition, "reset");
212
+ } catch (error) {
213
+ throw this.typedFailure("reset", error);
214
+ }
215
+ }
216
+ /** Promotes a previously rebuilt durable candidate with a compare-and-swap. */
217
+ async promote(serviceId, definition, rebuildId, nowMs) {
218
+ try {
219
+ const definitionId = this.checkedDefinitionId(definition, "promote");
220
+ if (!nonEmpty(rebuildId)) {
221
+ throw this.failure("promote", "MV_REBUILD_ID_INVALID", "Materialized-view rebuildId is required");
222
+ }
223
+ const viewId = materializedViewId(definitionId);
224
+ const candidateId = materializedViewCandidateId(definitionId, rebuildId);
225
+ const candidate = await this.store.readProjectionCheckpoint(serviceId, candidateId);
226
+ if (candidate === void 0) {
227
+ throw this.failure("promote", "MV_PROMOTE_CANDIDATE_MISSING", "Materialized-view rebuild candidate does not exist");
228
+ }
229
+ const candidateSnapshot = this.snapshot(serviceId, candidateId, candidate, definition, "promote");
230
+ const current = await this.store.readProjectionCheckpoint(serviceId, viewId);
231
+ if (current !== void 0 && current.lastSuid === candidate.lastSuid && current.stateJson === candidate.stateJson && current.version === candidate.version) {
232
+ throw this.failure("promote", "MV_PROMOTE_NOTHING_TO_PROMOTE", "Materialized-view candidate is already promoted");
233
+ }
234
+ const promoted = {
235
+ ...candidateSnapshot.checkpoint,
236
+ projectionId: viewId,
237
+ updatedAt: nowMs
238
+ };
239
+ const advanced = await this.store.advanceProjectionCheckpoint({
240
+ ...promoted,
241
+ expectedLastSuid: current?.lastSuid ?? null
242
+ });
243
+ if (!advanced) {
244
+ throw this.failure("promote", "MV_PROMOTE_CONFLICT", "Materialized-view checkpoint changed before candidate promotion could commit");
245
+ }
246
+ return this.snapshot(serviceId, viewId, promoted, definition, "promote");
247
+ } catch (error) {
248
+ throw this.typedFailure("promote", error);
249
+ }
250
+ }
251
+ async ensureInitialCheckpoint(serviceId, viewId, definition, nowMs, operation, failIfPresent) {
252
+ const existing = await this.store.readProjectionCheckpoint(serviceId, viewId);
253
+ if (existing !== void 0) {
254
+ if (failIfPresent) {
255
+ throw this.failure(
256
+ operation,
257
+ operation === "build" ? "MV_BUILD_ALREADY_EXISTS" : "MV_REBUILD_CANDIDATE_EXISTS",
258
+ operation === "build" ? "Materialized view already has durable state; use follow or rebuild" : "Materialized-view rebuild candidate already exists"
259
+ );
260
+ }
261
+ return existing;
262
+ }
263
+ const initial = this.checkpointFor(serviceId, viewId, "", this.initialState(definition, operation), definition, nowMs, operation);
264
+ const advanced = await this.store.advanceProjectionCheckpoint({
265
+ ...initial,
266
+ expectedLastSuid: null
267
+ });
268
+ if (advanced) {
269
+ return initial;
270
+ }
271
+ if (failIfPresent) {
272
+ throw this.failure(
273
+ operation,
274
+ operation === "build" ? "MV_BUILD_CONFLICT" : "MV_REBUILD_CONFLICT",
275
+ "Materialized-view checkpoint appeared before the requested durable operation could commit"
276
+ );
277
+ }
278
+ const raced = await this.store.readProjectionCheckpoint(serviceId, viewId);
279
+ if (raced === void 0) {
280
+ throw this.failure("follow", "MV_FOLLOW_CHECKPOINT_UNAVAILABLE", "Materialized-view checkpoint could not be established");
281
+ }
282
+ return raced;
283
+ }
284
+ async followCheckpoint(serviceId, viewId, definition, nowMs, operation, hooks) {
285
+ const dynamicLagBoundMs = await this.store.currentLagBound(serviceId, nowMs);
286
+ const windowMs = safeWindowMs(dynamicLagBoundMs);
287
+ for (let retry = 0; retry < MAX_CHECKPOINT_CAS_RETRIES; retry += 1) {
288
+ const checkpoint = await this.store.readProjectionCheckpoint(serviceId, viewId);
289
+ if (checkpoint === void 0) {
290
+ throw this.failure("follow", "MV_FOLLOW_CHECKPOINT_UNAVAILABLE", "Materialized-view checkpoint disappeared during follow");
291
+ }
292
+ if (safeWindowCeilingExceeded(dynamicLagBoundMs)) {
293
+ return {
294
+ ...this.snapshot(serviceId, viewId, checkpoint, definition, operation),
295
+ dynamicLagBoundMs,
296
+ safeWindowMs: windowMs,
297
+ advancedSourceEvents: 0,
298
+ appliedEvents: 0
299
+ };
300
+ }
301
+ let currentCheckpoint = checkpoint;
302
+ let state = this.stateFrom(definition, checkpoint, operation);
303
+ const sourceEvents = await this.store.readAllEvents(serviceId, checkpoint.lastSuid);
304
+ let validatedPreviousSuid = checkpoint.lastSuid;
305
+ for (const event of sourceEvents) {
306
+ assertSortableUniqueId(event.suid);
307
+ if (compareSuid(validatedPreviousSuid, event.suid) >= 0) {
308
+ throw this.failure(
309
+ operation,
310
+ "MV_SOURCE_NOT_STRICTLY_SUID_ORDERED",
311
+ "Materialized-view source was not strictly SUID ordered"
312
+ );
313
+ }
314
+ validatedPreviousSuid = event.suid;
315
+ }
316
+ let advancedSourceEvents = 0;
317
+ let appliedEvents = 0;
318
+ let casConflict = false;
319
+ for (const event of sourceEvents) {
320
+ if (!isSortableUniqueIdSafeAt(event.suid, nowMs, dynamicLagBoundMs)) {
321
+ return {
322
+ ...this.snapshot(serviceId, viewId, currentCheckpoint, definition, operation),
323
+ dynamicLagBoundMs,
324
+ safeWindowMs: windowMs,
325
+ advancedSourceEvents,
326
+ appliedEvents
327
+ };
328
+ }
329
+ const nextState = this.apply(definition, state, event, operation);
330
+ const nextCheckpoint = this.checkpointFor(serviceId, viewId, event.suid, nextState, definition, nowMs, operation);
331
+ await hooks.beforeCheckpoint?.(event, nextCheckpoint);
332
+ const advanced = await this.store.advanceProjectionCheckpoint({
333
+ ...nextCheckpoint,
334
+ expectedLastSuid: currentCheckpoint.lastSuid
335
+ });
336
+ if (!advanced) {
337
+ casConflict = true;
338
+ break;
339
+ }
340
+ currentCheckpoint = nextCheckpoint;
341
+ state = nextState;
342
+ advancedSourceEvents += 1;
343
+ appliedEvents += 1;
344
+ await hooks.afterCheckpoint?.(event, nextCheckpoint);
345
+ }
346
+ if (!casConflict) {
347
+ return {
348
+ ...this.snapshot(serviceId, viewId, currentCheckpoint, definition, operation),
349
+ dynamicLagBoundMs,
350
+ safeWindowMs: windowMs,
351
+ advancedSourceEvents,
352
+ appliedEvents
353
+ };
354
+ }
355
+ }
356
+ throw this.failure(operation, "MV_CHECKPOINT_CONFLICT", "Materialized-view checkpoint did not converge after concurrent updates");
357
+ }
358
+ checkedViewId(definition, operation) {
359
+ return materializedViewId(this.checkedDefinitionId(definition, operation));
360
+ }
361
+ checkedDefinitionId(definition, operation) {
362
+ if (!nonEmpty(definition.id)) {
363
+ throw this.failure(operation, "MV_DEFINITION_INVALID", "Materialized-view definition id is required");
364
+ }
365
+ return definition.id;
366
+ }
367
+ stateFrom(definition, checkpoint, operation) {
368
+ try {
369
+ return definition.deserializeState(checkpoint.stateJson);
370
+ } catch (error) {
371
+ throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view durable state was invalid: ${String(error)}`);
372
+ }
373
+ }
374
+ initialState(definition, operation) {
375
+ try {
376
+ return definition.initialState();
377
+ } catch (error) {
378
+ throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view initial state was invalid: ${String(error)}`);
379
+ }
380
+ }
381
+ apply(definition, state, event, operation) {
382
+ try {
383
+ return definition.apply(state, event);
384
+ } catch (error) {
385
+ throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view reducer failed: ${String(error)}`);
386
+ }
387
+ }
388
+ checkpointFor(serviceId, viewId, lastSuid, state, definition, nowMs, operation) {
389
+ try {
390
+ return {
391
+ serviceId,
392
+ projectionId: viewId,
393
+ lastSuid,
394
+ stateJson: definition.serializeState(state),
395
+ version: definition.version(state),
396
+ updatedAt: nowMs
397
+ };
398
+ } catch (error) {
399
+ throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view state could not be persisted: ${String(error)}`);
400
+ }
401
+ }
402
+ snapshot(serviceId, viewId, checkpoint, definition, operation) {
403
+ return {
404
+ serviceId,
405
+ viewId,
406
+ checkpoint,
407
+ state: this.stateFrom(definition, checkpoint, operation)
408
+ };
409
+ }
410
+ failure(operation, code, message) {
411
+ return new MaterializedViewOperationError(operation, code, message);
412
+ }
413
+ typedFailure(operation, error) {
414
+ return error instanceof MaterializedViewOperationError ? error : this.failure(operation, "MV_OPERATION_FAILED", `Materialized-view ${operation} failed: ${String(error)}`);
415
+ }
416
+ };
417
+
418
+ // ../dcb-core/dist/index.js
419
+ var JsonValidationError = class extends Error {
420
+ code = "invalid_json_value";
421
+ boundary;
422
+ path;
423
+ constructor(message, boundary = "value", path = "$", options) {
424
+ super(message, options);
425
+ this.name = "JsonValidationError";
426
+ this.boundary = boundary;
427
+ this.path = path;
428
+ }
429
+ };
430
+ var isPlainRecord = (value) => {
431
+ const prototype = Object.getPrototypeOf(value);
432
+ return prototype === Object.prototype || prototype === null;
433
+ };
434
+ function assertJsonValue(value, boundary = "value") {
435
+ const active = /* @__PURE__ */ new WeakSet();
436
+ const visit = (candidate, path) => {
437
+ if (candidate === null || typeof candidate === "string" || typeof candidate === "boolean") {
438
+ return candidate;
439
+ }
440
+ if (typeof candidate === "number") {
441
+ if (Number.isFinite(candidate)) return candidate;
442
+ throw new JsonValidationError("JSON numbers must be finite", boundary, path);
443
+ }
444
+ if (typeof candidate !== "object") {
445
+ throw new JsonValidationError(`Value at ${path} is not JSON serializable`, boundary, path);
446
+ }
447
+ if (active.has(candidate)) {
448
+ throw new JsonValidationError(`Cyclic JSON value at ${path}`, boundary, path);
449
+ }
450
+ active.add(candidate);
451
+ try {
452
+ if (Array.isArray(candidate)) {
453
+ return candidate.map((item, index) => visit(item, `${path}[${index}]`));
454
+ }
455
+ if (!isPlainRecord(candidate)) {
456
+ throw new JsonValidationError(`Value at ${path} must be a plain JSON object`, boundary, path);
457
+ }
458
+ const result = {};
459
+ for (const [key, item] of Object.entries(candidate)) {
460
+ result[key] = visit(item, `${path}.${key}`);
461
+ }
462
+ return result;
463
+ } finally {
464
+ active.delete(candidate);
465
+ }
466
+ };
467
+ return visit(value, "$");
468
+ }
469
+
470
+ // src/mv/UnsafeWindowMaterializedView.ts
471
+ var UnsafeWindowMaterializedViewError = class extends Error {
472
+ constructor(code, retryable, message, options) {
473
+ super(message, options);
474
+ this.code = code;
475
+ this.retryable = retryable;
476
+ this.name = "UnsafeWindowMaterializedViewError";
477
+ }
478
+ code;
479
+ retryable;
480
+ };
481
+ function compareSuid2(left, right) {
482
+ const leftBytes = new TextEncoder().encode(left);
483
+ const rightBytes = new TextEncoder().encode(right);
484
+ for (let index = 0; index < Math.min(leftBytes.length, rightBytes.length); index += 1) {
485
+ const difference = leftBytes[index] - rightBytes[index];
486
+ if (difference !== 0) return difference;
487
+ }
488
+ return leftBytes.length - rightBytes.length;
489
+ }
490
+ function string(row, key) {
491
+ if (typeof row[key] !== "string") throw new Error(`Unsafe MV ${key} was not a string`);
492
+ return row[key];
493
+ }
494
+ function integer(row, key) {
495
+ const value = typeof row[key] === "number" ? row[key] : Number(row[key]);
496
+ if (!Number.isSafeInteger(value)) throw new Error(`Unsafe MV ${key} was not an integer`);
497
+ return value;
498
+ }
499
+ function operationId() {
500
+ return `mv-unsafe-${crypto.randomUUID()}`;
501
+ }
502
+ function errorFrom(error) {
503
+ const value = String(error);
504
+ if (value.includes("mv_unsafe_receipts") && (value.includes("UNIQUE") || value.includes("constraint"))) {
505
+ return new UnsafeWindowMaterializedViewError("UNSAFE_DUPLICATE_RACE", false, "Concurrent unsafe receipt was committed by another delivery", { cause: error });
506
+ }
507
+ if (value.includes("unsafe_row_cas")) return new UnsafeWindowMaterializedViewError("UNSAFE_ROW_CAS_MISMATCH", true, "Unsafe row version changed before atomic apply", { cause: error });
508
+ if (value.includes("unsafe_suid_contradiction")) return new UnsafeWindowMaterializedViewError("UNSAFE_SUID_CONTRADICTION", false, "Unsafe same-SUID payload contradiction", { cause: error });
509
+ if (value.includes("unsafe_safe_ahead")) return new UnsafeWindowMaterializedViewError("UNSAFE_SAFE_AHEAD", false, "Safe head already covers unsafe event", { cause: error });
510
+ if (value.includes("unsafe_behind_frontier")) return new UnsafeWindowMaterializedViewError("UNSAFE_BEHIND_FRONTIER", false, "Unsafe reads are fail-closed pending rebuild", { cause: error });
511
+ return void 0;
512
+ }
513
+ var UnsafeWindowMaterializedViewStore = class {
514
+ constructor(database, options = {}) {
515
+ this.database = database;
516
+ this.options = options;
517
+ }
518
+ database;
519
+ options;
520
+ /**
521
+ * The common row-materializer shape is one complete upsert. Keep its
522
+ * duplicate/order guards and all observable mutations in one D1 batch. A
523
+ * guard race falls back to apply(), which preserves the detailed older,
524
+ * duplicate, and contradiction classifications; only the uncontended fast
525
+ * path avoids the receipt/row pre-reads and separate arrival write.
526
+ */
527
+ async applyUpsertFast(input) {
528
+ const { rowUpserts, rowPatches, rowDeletes } = input.mutations;
529
+ if (rowUpserts.length !== 1 || rowPatches.length !== 0 || rowDeletes.length !== 0) return void 0;
530
+ const upsert = rowUpserts[0];
531
+ if (upsert.sourceSuid !== input.suid) return void 0;
532
+ const valueJson = JSON.stringify(assertJsonValue(upsert.value, "state-persistence"));
533
+ const operation = operationId();
534
+ const statements = [];
535
+ const guard = (name, predicate, values) => {
536
+ statements.push(this.database.prepare(
537
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
538
+ SELECT ?, CASE WHEN (${predicate}) THEN 1 ELSE NULL END`
539
+ ).bind(`${operation}:${name}`, ...values));
540
+ };
541
+ guard(
542
+ "receipt",
543
+ "NOT EXISTS (SELECT 1 FROM mv_unsafe_receipts WHERE service_id = ? AND view_id = ? AND event_id = ?)",
544
+ [input.serviceId, input.viewId, input.eventId]
545
+ );
546
+ guard("safe-head", "? = '' OR ? COLLATE BINARY < ? COLLATE BINARY", [input.safeHead, input.safeHead, input.suid]);
547
+ guard(
548
+ "row-order",
549
+ "NOT EXISTS (SELECT 1 FROM mv_unsafe_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ? AND source_suid COLLATE BINARY > ? COLLATE BINARY)",
550
+ [input.serviceId, input.viewId, input.generation, upsert.rowKey, input.suid]
551
+ );
552
+ guard(
553
+ "same-suid",
554
+ "NOT EXISTS (SELECT 1 FROM mv_unsafe_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ? AND source_suid COLLATE BINARY = ? COLLATE BINARY AND value_json <> ?)",
555
+ [input.serviceId, input.viewId, input.generation, upsert.rowKey, input.suid, valueJson]
556
+ );
557
+ if (input.expectedRowVersion !== void 0 && input.expectedRowVersion !== null) {
558
+ guard(
559
+ "row-cas",
560
+ "NOT EXISTS (SELECT 1 FROM mv_unsafe_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?) OR EXISTS (SELECT 1 FROM mv_unsafe_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ? AND row_version = ?)",
561
+ [
562
+ input.serviceId,
563
+ input.viewId,
564
+ input.generation,
565
+ upsert.rowKey,
566
+ input.serviceId,
567
+ input.viewId,
568
+ input.generation,
569
+ upsert.rowKey,
570
+ input.expectedRowVersion
571
+ ]
572
+ );
573
+ }
574
+ if (input.recordArrival === true) {
575
+ const behindFrontier = input.safeHead !== "" && compareSuid2(input.suid, input.safeHead) <= 0;
576
+ statements.push(this.database.prepare(
577
+ `INSERT INTO mv_unsafe_arrivals
578
+ (service_id, view_id, generation, safe_head, arrival_watermark, behind_frontier_event_id, behind_frontier_suid, rebuild_required)
579
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
580
+ ON CONFLICT (service_id, view_id, generation) DO UPDATE SET
581
+ arrival_watermark = CASE WHEN excluded.arrival_watermark COLLATE BINARY > mv_unsafe_arrivals.arrival_watermark COLLATE BINARY THEN excluded.arrival_watermark ELSE mv_unsafe_arrivals.arrival_watermark END,
582
+ behind_frontier_event_id = CASE WHEN mv_unsafe_arrivals.rebuild_required = 1 THEN mv_unsafe_arrivals.behind_frontier_event_id ELSE excluded.behind_frontier_event_id END,
583
+ behind_frontier_suid = CASE WHEN mv_unsafe_arrivals.rebuild_required = 1 THEN mv_unsafe_arrivals.behind_frontier_suid ELSE excluded.behind_frontier_suid END,
584
+ rebuild_required = MAX(mv_unsafe_arrivals.rebuild_required, excluded.rebuild_required)`
585
+ ).bind(
586
+ input.serviceId,
587
+ input.viewId,
588
+ input.generation,
589
+ input.safeHead,
590
+ input.suid,
591
+ behindFrontier ? input.eventId : null,
592
+ behindFrontier ? input.suid : null,
593
+ behindFrontier ? 1 : 0
594
+ ));
595
+ }
596
+ statements.push(this.database.prepare(
597
+ `DELETE FROM mv_unsafe_index_entries WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`
598
+ ).bind(input.serviceId, input.viewId, input.generation, upsert.rowKey));
599
+ statements.push(this.database.prepare(
600
+ `INSERT INTO mv_unsafe_rows (service_id, view_id, generation, row_key, value_json, row_version, source_suid, tombstone)
601
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0)
602
+ ON CONFLICT (service_id, view_id, generation, row_key) DO UPDATE SET
603
+ value_json = excluded.value_json, row_version = excluded.row_version, source_suid = excluded.source_suid, tombstone = 0`
604
+ ).bind(input.serviceId, input.viewId, input.generation, upsert.rowKey, valueJson, upsert.rowVersion, upsert.sourceSuid));
605
+ for (const entry of input.mutations.indexEntries) this.addIndex(statements, "mv_unsafe_index_entries", input, entry);
606
+ statements.push(this.database.prepare(
607
+ `INSERT INTO mv_unsafe_receipts (service_id, view_id, event_id, suid, outcome, observed_at)
608
+ VALUES (?, ?, ?, ?, 'applied', ?)`
609
+ ).bind(input.serviceId, input.viewId, input.eventId, input.suid, input.updatedAt));
610
+ statements.push(this.database.prepare(
611
+ `INSERT INTO mv_wait_receipts
612
+ (service_id, view_id, generation, definition_version, event_id, suid, observed_at)
613
+ SELECT ?, ?, ?, instance.definition_version, ?, ?, ?
614
+ FROM mv_instances instance
615
+ JOIN mv_active_generations active
616
+ ON active.service_id = instance.service_id
617
+ AND active.view_id = instance.view_id
618
+ AND active.generation = instance.generation
619
+ WHERE instance.service_id = ? AND instance.view_id = ? AND instance.generation = ?
620
+ ON CONFLICT DO NOTHING`
621
+ ).bind(
622
+ input.serviceId,
623
+ input.viewId,
624
+ input.generation,
625
+ input.eventId,
626
+ input.suid,
627
+ input.updatedAt,
628
+ input.serviceId,
629
+ input.viewId,
630
+ input.generation
631
+ ));
632
+ statements.push(this.database.prepare(
633
+ `INSERT INTO mv_unsafe_kicks (service_id, view_id, target_suid, dirty)
634
+ VALUES (?, ?, ?, 1)
635
+ ON CONFLICT (service_id, view_id) DO UPDATE SET
636
+ target_suid = CASE WHEN excluded.target_suid COLLATE BINARY > mv_unsafe_kicks.target_suid COLLATE BINARY THEN excluded.target_suid ELSE mv_unsafe_kicks.target_suid END,
637
+ dirty = 1`
638
+ ).bind(input.serviceId, input.viewId, input.targetSuid ?? input.suid));
639
+ statements.push(this.database.prepare("DELETE FROM mv_atomic_guards WHERE operation_id LIKE ?").bind(`${operation}:%`));
640
+ await this.options.beforeApplyBatch?.(input, statements);
641
+ try {
642
+ await this.database.batch(statements);
643
+ } catch (error) {
644
+ const text = String(error);
645
+ const guardFailure = text.includes("mv_atomic_guards") || text.includes("checkpoint_match") || text.includes("mv_unsafe_receipts") && (text.includes("UNIQUE") || text.includes("constraint"));
646
+ if (guardFailure) {
647
+ const receipt = await this.database.prepare(
648
+ `SELECT suid FROM mv_unsafe_receipts WHERE service_id = ? AND view_id = ? AND event_id = ? ORDER BY observed_at DESC LIMIT 1`
649
+ ).bind(input.serviceId, input.viewId, input.eventId).first();
650
+ if (receipt !== null && receipt !== void 0) {
651
+ if (string(receipt, "suid") !== input.suid) {
652
+ throw new UnsafeWindowMaterializedViewError("UNSAFE_SUID_CONTRADICTION", false, "Duplicate event identity has a contradictory SUID", { cause: error });
653
+ }
654
+ if (this.options.beforeApplyBatch !== void 0) {
655
+ throw new UnsafeWindowMaterializedViewError("UNSAFE_DUPLICATE_RACE", false, "Concurrent unsafe receipt was committed by another delivery", { cause: error });
656
+ }
657
+ return { outcome: "no-change", duplicate: true };
658
+ }
659
+ return this.apply(input, true);
660
+ }
661
+ throw error;
662
+ }
663
+ const result = { outcome: "applied", duplicate: false };
664
+ await this.options.afterApplyBatch?.(input, result);
665
+ return result;
666
+ }
667
+ async apply(input, skipFastPath = false) {
668
+ assertSortableUniqueId(input.suid);
669
+ if (input.safeHead.length > 0) assertSortableUniqueId(input.safeHead);
670
+ if (input.targetSuid !== void 0) assertSortableUniqueId(input.targetSuid);
671
+ for (const mutation of [...input.mutations.rowUpserts, ...input.mutations.rowPatches]) {
672
+ assertSortableUniqueId(mutation.sourceSuid);
673
+ }
674
+ const fast = skipFastPath ? void 0 : await this.applyUpsertFast(input);
675
+ if (fast !== void 0) return fast;
676
+ if (input.recordArrival === true) await this.observeArrival(input.serviceId, input.viewId, input.generation, input.eventId, input.suid);
677
+ const receipt = await this.database.prepare(
678
+ `SELECT suid FROM mv_unsafe_receipts WHERE service_id = ? AND view_id = ? AND event_id = ? ORDER BY observed_at DESC LIMIT 1`
679
+ ).bind(input.serviceId, input.viewId, input.eventId).first();
680
+ if (receipt !== null && receipt !== void 0) {
681
+ if (string(receipt, "suid") !== input.suid) {
682
+ throw new UnsafeWindowMaterializedViewError("UNSAFE_SUID_CONTRADICTION", false, "Duplicate event identity has a contradictory SUID");
683
+ }
684
+ return { outcome: "no-change", duplicate: true };
685
+ }
686
+ const rowKeys = [.../* @__PURE__ */ new Set([
687
+ ...input.mutations.rowUpserts.map((row) => row.rowKey),
688
+ ...input.mutations.rowPatches.map((row) => row.rowKey),
689
+ ...input.mutations.rowDeletes.map((row) => row.rowKey)
690
+ ])];
691
+ const existing = /* @__PURE__ */ new Map();
692
+ for (const rowKey of rowKeys) {
693
+ const row = await this.database.prepare(
694
+ `SELECT row_key, value_json, row_version, source_suid FROM mv_unsafe_rows
695
+ WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`
696
+ ).bind(input.serviceId, input.viewId, input.generation, rowKey).first();
697
+ if (row !== null && row !== void 0) existing.set(rowKey, row);
698
+ }
699
+ const older = [...existing.values()].some((row) => compareSuid2(string(row, "source_suid"), input.suid) > 0);
700
+ const noRowForPatch = input.mutations.rowPatches.some((patch) => !existing.has(patch.rowKey));
701
+ const deleteWithoutRow = input.mutations.rowDeletes.some((row) => !existing.has(row.rowKey));
702
+ const outcome = older ? "older" : noRowForPatch ? "patch-not-found" : deleteWithoutRow ? "delete-without-row" : input.mutations.rowUpserts.length + input.mutations.rowPatches.length + input.mutations.rowDeletes.length === 0 ? "no-change" : "applied";
703
+ const operation = operationId();
704
+ const statements = [];
705
+ const safeAhead = input.safeHead !== "" && compareSuid2(input.safeHead, input.suid) >= 0;
706
+ let rowCasMismatch = false;
707
+ let sameSuidContradiction = false;
708
+ const guard = (name, predicate, values) => {
709
+ statements.push(this.database.prepare(
710
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
711
+ SELECT ?, CASE WHEN (${predicate}) THEN 1 ELSE NULL END`
712
+ ).bind(`${operation}:${name}`, ...values));
713
+ };
714
+ guard("unsafe_safe_ahead", "? = '' OR ? COLLATE BINARY < ? COLLATE BINARY", [input.safeHead, input.safeHead, input.suid]);
715
+ for (const [rowKey, row] of existing) {
716
+ if (input.expectedRowVersion !== void 0 && input.expectedRowVersion !== null) {
717
+ rowCasMismatch ||= integer(row, "row_version") !== input.expectedRowVersion;
718
+ guard("unsafe_row_cas", "? = ?", [integer(row, "row_version"), input.expectedRowVersion]);
719
+ }
720
+ const upsert = input.mutations.rowUpserts.find((candidate) => candidate.rowKey === rowKey);
721
+ if (upsert !== void 0 && string(row, "source_suid") === input.suid && string(row, "value_json") !== JSON.stringify(assertJsonValue(upsert.value, "state-persistence"))) {
722
+ sameSuidContradiction = true;
723
+ guard("unsafe_suid_contradiction", "0", []);
724
+ }
725
+ }
726
+ if (!older && outcome === "applied") {
727
+ for (const deletion of input.mutations.rowDeletes) {
728
+ statements.push(this.database.prepare(`DELETE FROM mv_unsafe_index_entries WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`).bind(input.serviceId, input.viewId, input.generation, deletion.rowKey));
729
+ statements.push(this.database.prepare(`DELETE FROM mv_unsafe_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`).bind(input.serviceId, input.viewId, input.generation, deletion.rowKey));
730
+ }
731
+ for (const upsert of input.mutations.rowUpserts) {
732
+ statements.push(this.database.prepare(`DELETE FROM mv_unsafe_index_entries WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`).bind(input.serviceId, input.viewId, input.generation, upsert.rowKey));
733
+ statements.push(this.database.prepare(
734
+ `INSERT INTO mv_unsafe_rows (service_id, view_id, generation, row_key, value_json, row_version, source_suid, tombstone)
735
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0)
736
+ ON CONFLICT (service_id, view_id, generation, row_key) DO UPDATE SET
737
+ value_json = excluded.value_json, row_version = excluded.row_version, source_suid = excluded.source_suid, tombstone = 0`
738
+ ).bind(input.serviceId, input.viewId, input.generation, upsert.rowKey, JSON.stringify(assertJsonValue(upsert.value, "state-persistence")), upsert.rowVersion, upsert.sourceSuid));
739
+ }
740
+ for (const patch of input.mutations.rowPatches) {
741
+ statements.push(this.database.prepare(
742
+ `UPDATE mv_unsafe_rows SET value_json = json_patch(value_json, ?), row_version = ?, source_suid = ?, tombstone = 0
743
+ WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`
744
+ ).bind(JSON.stringify(assertJsonValue(patch.patch, "state-persistence")), patch.rowVersion, patch.sourceSuid, input.serviceId, input.viewId, input.generation, patch.rowKey));
745
+ }
746
+ for (const entry of input.mutations.indexEntries) this.addIndex(statements, "mv_unsafe_index_entries", input, entry);
747
+ for (const patch of input.mutations.rowPatches) for (const entry of patch.indexEntries) this.addIndex(statements, "mv_unsafe_index_entries", input, entry);
748
+ }
749
+ const markerReason = outcome === "applied" ? void 0 : outcome;
750
+ if (markerReason !== void 0) {
751
+ for (const rowKey of rowKeys.length === 0 ? [input.eventId] : rowKeys) {
752
+ statements.push(this.database.prepare(
753
+ `INSERT INTO mv_unsafe_markers (service_id, view_id, generation, row_key, event_id, suid, reason)
754
+ VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING`
755
+ ).bind(input.serviceId, input.viewId, input.generation, rowKey, input.eventId, input.suid, markerReason));
756
+ }
757
+ }
758
+ statements.push(this.database.prepare(
759
+ `INSERT INTO mv_unsafe_receipts (service_id, view_id, event_id, suid, outcome, observed_at) VALUES (?, ?, ?, ?, ?, ?)`
760
+ ).bind(input.serviceId, input.viewId, input.eventId, input.suid, outcome, input.updatedAt));
761
+ statements.push(this.database.prepare(
762
+ `INSERT INTO mv_wait_receipts
763
+ (service_id, view_id, generation, definition_version, event_id, suid, observed_at)
764
+ SELECT ?, ?, ?, instance.definition_version, ?, ?, ?
765
+ FROM mv_instances instance
766
+ JOIN mv_active_generations active
767
+ ON active.service_id = instance.service_id
768
+ AND active.view_id = instance.view_id
769
+ AND active.generation = instance.generation
770
+ WHERE instance.service_id = ? AND instance.view_id = ? AND instance.generation = ?
771
+ ON CONFLICT DO NOTHING`
772
+ ).bind(
773
+ input.serviceId,
774
+ input.viewId,
775
+ input.generation,
776
+ input.eventId,
777
+ input.suid,
778
+ input.updatedAt,
779
+ input.serviceId,
780
+ input.viewId,
781
+ input.generation
782
+ ));
783
+ statements.push(this.database.prepare(
784
+ `INSERT INTO mv_unsafe_kicks (service_id, view_id, target_suid, dirty)
785
+ VALUES (?, ?, ?, 1)
786
+ ON CONFLICT (service_id, view_id) DO UPDATE SET
787
+ target_suid = CASE WHEN excluded.target_suid COLLATE BINARY > mv_unsafe_kicks.target_suid COLLATE BINARY THEN excluded.target_suid ELSE mv_unsafe_kicks.target_suid END,
788
+ dirty = 1`
789
+ ).bind(input.serviceId, input.viewId, input.targetSuid ?? input.suid));
790
+ statements.push(this.database.prepare("DELETE FROM mv_atomic_guards WHERE operation_id LIKE ?").bind(`${operation}:%`));
791
+ await this.options.beforeApplyBatch?.(input, statements);
792
+ try {
793
+ await this.database.batch(statements);
794
+ } catch (error) {
795
+ if (sameSuidContradiction) throw new UnsafeWindowMaterializedViewError("UNSAFE_SUID_CONTRADICTION", false, "Unsafe same-SUID payload contradiction", { cause: error });
796
+ if (rowCasMismatch) throw new UnsafeWindowMaterializedViewError("UNSAFE_ROW_CAS_MISMATCH", true, "Unsafe row version changed before atomic apply", { cause: error });
797
+ if (safeAhead) throw new UnsafeWindowMaterializedViewError("UNSAFE_SAFE_AHEAD", false, "Safe head already covers unsafe event", { cause: error });
798
+ const typed = errorFrom(error);
799
+ if (typed !== void 0) throw typed;
800
+ throw error;
801
+ }
802
+ const result = { outcome, duplicate: false };
803
+ await this.options.afterApplyBatch?.(input, result);
804
+ return result;
805
+ }
806
+ /** Advance safe head and remove markers only if this transaction observed its exact receipt. */
807
+ async observeSafeReceipt(serviceId, viewId, generation, eventId, suid) {
808
+ assertSortableUniqueId(suid);
809
+ const operation = operationId();
810
+ await this.database.batch([
811
+ this.database.prepare(
812
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
813
+ SELECT ?, CASE WHEN EXISTS (SELECT 1 FROM mv_unsafe_receipts WHERE service_id = ? AND view_id = ? AND event_id = ? AND suid = ?) THEN 1 ELSE NULL END`
814
+ ).bind(operation, serviceId, viewId, eventId, suid),
815
+ this.database.prepare(
816
+ `INSERT INTO mv_unsafe_arrivals (service_id, view_id, generation, safe_head, arrival_watermark)
817
+ VALUES (?, ?, ?, ?, ?)
818
+ ON CONFLICT (service_id, view_id, generation) DO UPDATE SET safe_head = excluded.safe_head`
819
+ ).bind(serviceId, viewId, generation, suid, suid),
820
+ this.database.prepare(`DELETE FROM mv_unsafe_markers WHERE service_id = ? AND view_id = ? AND generation = ? AND event_id = ? AND suid = ?`).bind(serviceId, viewId, generation, eventId, suid),
821
+ this.database.prepare("DELETE FROM mv_atomic_guards WHERE operation_id = ?").bind(operation)
822
+ ]);
823
+ }
824
+ /** Detects late first-arrivals independently of SafeWindow and fail-closes reads. */
825
+ async observeArrival(serviceId, viewId, generation, eventId, suid) {
826
+ assertSortableUniqueId(suid);
827
+ const existing = await this.database.prepare(
828
+ `SELECT safe_head, arrival_watermark FROM mv_unsafe_arrivals WHERE service_id = ? AND view_id = ? AND generation = ?`
829
+ ).bind(serviceId, viewId, generation).first();
830
+ const safeHead = existing === null || existing === void 0 ? "" : string(existing, "safe_head");
831
+ const behind = safeHead !== "" && compareSuid2(suid, safeHead) <= 0;
832
+ await this.database.prepare(
833
+ `INSERT INTO mv_unsafe_arrivals (service_id, view_id, generation, safe_head, arrival_watermark, behind_frontier_event_id, behind_frontier_suid, rebuild_required)
834
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
835
+ ON CONFLICT (service_id, view_id, generation) DO UPDATE SET
836
+ arrival_watermark = CASE WHEN excluded.arrival_watermark COLLATE BINARY > mv_unsafe_arrivals.arrival_watermark COLLATE BINARY THEN excluded.arrival_watermark ELSE mv_unsafe_arrivals.arrival_watermark END,
837
+ behind_frontier_event_id = CASE WHEN mv_unsafe_arrivals.rebuild_required = 1 THEN mv_unsafe_arrivals.behind_frontier_event_id ELSE excluded.behind_frontier_event_id END,
838
+ behind_frontier_suid = CASE WHEN mv_unsafe_arrivals.rebuild_required = 1 THEN mv_unsafe_arrivals.behind_frontier_suid ELSE excluded.behind_frontier_suid END,
839
+ rebuild_required = MAX(mv_unsafe_arrivals.rebuild_required, excluded.rebuild_required)`
840
+ ).bind(serviceId, viewId, generation, safeHead, suid, behind ? eventId : null, behind ? suid : null, behind ? 1 : 0).run();
841
+ return behind;
842
+ }
843
+ async readMeta(serviceId, viewId, generation) {
844
+ const arrival = await this.database.prepare(`SELECT safe_head, rebuild_required FROM mv_unsafe_arrivals WHERE service_id = ? AND view_id = ? AND generation = ?`).bind(serviceId, viewId, generation).first();
845
+ const row = await this.database.prepare(`SELECT COALESCE(MAX(suid), '') AS max_suid FROM mv_unsafe_receipts WHERE service_id = ? AND view_id = ?`).bind(serviceId, viewId).first();
846
+ return {
847
+ safeContiguousHead: arrival === null || arrival === void 0 ? "" : string(arrival, "safe_head"),
848
+ unsafeObservedMaxSuid: row === null || row === void 0 ? "" : string(row, "max_suid"),
849
+ rebuildRequired: arrival !== null && arrival !== void 0 && integer(arrival, "rebuild_required") === 1
850
+ };
851
+ }
852
+ async hasTargetReceipt(serviceId, viewId, eventId, suid) {
853
+ const receipt = await this.database.prepare(`SELECT 1 FROM mv_unsafe_receipts WHERE service_id = ? AND view_id = ? AND event_id = ? AND suid = ?`).bind(serviceId, viewId, eventId, suid).first();
854
+ return receipt !== null && receipt !== void 0;
855
+ }
856
+ async acquireKick(serviceId, viewId, owner, nowMs, leaseMs) {
857
+ const operation = operationId();
858
+ try {
859
+ await this.database.batch([
860
+ this.database.prepare(
861
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
862
+ SELECT ?, CASE WHEN EXISTS (SELECT 1 FROM mv_unsafe_kicks WHERE service_id = ? AND view_id = ? AND dirty = 1 AND (lease_owner IS NULL OR lease_until <= ? OR lease_owner = ?)) THEN 1 ELSE NULL END`
863
+ ).bind(operation, serviceId, viewId, nowMs, owner),
864
+ this.database.prepare(`UPDATE mv_unsafe_kicks SET lease_owner = ?, lease_until = ?, dirty = 0 WHERE service_id = ? AND view_id = ?`).bind(owner, nowMs + leaseMs, serviceId, viewId),
865
+ this.database.prepare("DELETE FROM mv_atomic_guards WHERE operation_id = ?").bind(operation)
866
+ ]);
867
+ } catch (error) {
868
+ throw new UnsafeWindowMaterializedViewError("UNSAFE_KICK_LEASE_HELD", true, "Unsafe kick is held by another lease owner", { cause: error });
869
+ }
870
+ const row = await this.database.prepare(`SELECT target_suid, dirty FROM mv_unsafe_kicks WHERE service_id = ? AND view_id = ?`).bind(serviceId, viewId).first();
871
+ return row === null || row === void 0 ? void 0 : { targetSuid: string(row, "target_suid"), dirty: integer(row, "dirty") === 1 };
872
+ }
873
+ /**
874
+ * Settle an unsafe kick at the checkpoint actually reached by its follow.
875
+ * A first-unsafe return is a successful bounded pass, not proof that the
876
+ * lease target was reached. When a reached checkpoint is supplied, keep the
877
+ * durable dirty bit set if the target remains ahead so the next scheduled
878
+ * tick can acquire the kick again. Requiring this checkpoint keeps every
879
+ * production settlement honest about whether its target was reached.
880
+ */
881
+ async finishKick(serviceId, viewId, owner, reachedSuid) {
882
+ if (reachedSuid.length > 0) assertSortableUniqueId(reachedSuid);
883
+ const result = await this.database.prepare(
884
+ `UPDATE mv_unsafe_kicks
885
+ SET lease_owner = NULL,
886
+ lease_until = 0,
887
+ dirty = CASE
888
+ WHEN ? IS NOT NULL AND target_suid COLLATE BINARY > ? COLLATE BINARY THEN 1
889
+ ELSE 0
890
+ END
891
+ WHERE service_id = ? AND view_id = ? AND lease_owner = ? AND dirty = 0`
892
+ ).bind(reachedSuid, reachedSuid, serviceId, viewId, owner).run();
893
+ return result.meta.changes === 1;
894
+ }
895
+ /**
896
+ * Collect a repairable unsafe row only when every recovery predicate holds.
897
+ * Every predicate is a constraint-promotion guard ahead of the deletes; a
898
+ * rejected GC therefore cannot leak a partially removed index/receipt.
899
+ */
900
+ async garbageCollect(input) {
901
+ const operation = operationId();
902
+ const statements = [
903
+ // safeHead >= row source, including the mandatory post-GC resurrection fence.
904
+ this.database.prepare(`INSERT INTO mv_atomic_guards (operation_id, checkpoint_match) SELECT ?, CASE WHEN ? COLLATE BINARY >= ? COLLATE BINARY THEN 1 ELSE NULL END`).bind(`${operation}:safe-head`, input.safeHead, input.expectedSourceSuid),
905
+ // Arrival receipt/watermark is a separate condition from the
906
+ // fail-closed detector state; each has its own in-batch guard.
907
+ this.database.prepare(
908
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
909
+ SELECT ?, CASE WHEN EXISTS (SELECT 1 FROM mv_unsafe_arrivals WHERE service_id = ? AND view_id = ? AND generation = ? AND arrival_watermark COLLATE BINARY >= ? COLLATE BINARY) THEN 1 ELSE NULL END`
910
+ ).bind(`${operation}:arrival`, input.serviceId, input.viewId, input.generation, input.expectedSourceSuid),
911
+ this.database.prepare(
912
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
913
+ SELECT ?, CASE WHEN EXISTS (SELECT 1 FROM mv_unsafe_arrivals WHERE service_id = ? AND view_id = ? AND generation = ? AND rebuild_required = 0) THEN 1 ELSE NULL END`
914
+ ).bind(`${operation}:frontier`, input.serviceId, input.viewId, input.generation),
915
+ // Candidate/retired generations and changed materializer definitions are
916
+ // never GC targets. These are intentionally separate guards so a
917
+ // definition mutation cannot piggy-back on an active-generation check.
918
+ this.database.prepare(
919
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
920
+ SELECT ?, CASE WHEN EXISTS (SELECT 1 FROM mv_active_generations WHERE service_id = ? AND view_id = ? AND generation = ?) THEN 1 ELSE NULL END`
921
+ ).bind(`${operation}:active-generation`, input.serviceId, input.viewId, input.generation),
922
+ this.database.prepare(
923
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
924
+ SELECT ?, CASE WHEN EXISTS (SELECT 1 FROM mv_instances WHERE service_id = ? AND view_id = ? AND generation = ? AND definition_version = ?) THEN 1 ELSE NULL END`
925
+ ).bind(`${operation}:definition`, input.serviceId, input.viewId, input.generation, input.definitionVersion),
926
+ // Compare-and-delete is revalidated inside the atomic batch, never on a
927
+ // read-then-delete snapshot.
928
+ this.database.prepare(
929
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
930
+ SELECT ?, CASE WHEN EXISTS (SELECT 1 FROM mv_unsafe_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ? AND row_version = ? AND source_suid COLLATE BINARY = ? COLLATE BINARY) THEN 1 ELSE NULL END`
931
+ ).bind(`${operation}:row`, input.serviceId, input.viewId, input.generation, input.rowKey, input.expectedRowVersion, input.expectedSourceSuid),
932
+ this.database.prepare(`DELETE FROM mv_unsafe_index_entries WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`).bind(input.serviceId, input.viewId, input.generation, input.rowKey),
933
+ this.database.prepare(`DELETE FROM mv_unsafe_markers WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`).bind(input.serviceId, input.viewId, input.generation, input.rowKey),
934
+ this.database.prepare(`DELETE FROM mv_unsafe_receipts WHERE service_id = ? AND view_id = ? AND suid = ?`).bind(input.serviceId, input.viewId, input.expectedSourceSuid),
935
+ this.database.prepare(`DELETE FROM mv_wait_receipts WHERE service_id = ? AND view_id = ? AND generation = ? AND suid = ?`).bind(input.serviceId, input.viewId, input.generation, input.expectedSourceSuid),
936
+ this.database.prepare(`DELETE FROM mv_unsafe_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ? AND row_version = ? AND source_suid COLLATE BINARY = ? COLLATE BINARY`).bind(input.serviceId, input.viewId, input.generation, input.rowKey, input.expectedRowVersion, input.expectedSourceSuid),
937
+ this.database.prepare("DELETE FROM mv_atomic_guards WHERE operation_id LIKE ?").bind(`${operation}:%`)
938
+ ];
939
+ try {
940
+ await this.database.batch(statements);
941
+ return true;
942
+ } catch (error) {
943
+ if (String(error).includes("mv_atomic_guards") || String(error).includes("checkpoint_match")) return false;
944
+ throw error;
945
+ }
946
+ }
947
+ /**
948
+ * Idle catch-up calls this after it has advanced (or observed) the safe
949
+ * checkpoint. Candidates are deliberately rechecked by garbageCollect;
950
+ * this read is only work selection, never permission to delete.
951
+ */
952
+ async collectEligible(serviceId, viewId, generation, definitionVersion, safeHead) {
953
+ if (safeHead === "") return 0;
954
+ const candidates = await this.database.prepare(
955
+ `SELECT row_key, row_version, source_suid FROM mv_unsafe_rows
956
+ WHERE service_id = ? AND view_id = ? AND generation = ?
957
+ AND source_suid COLLATE BINARY <= ? COLLATE BINARY
958
+ ORDER BY source_suid COLLATE BINARY ASC, row_key COLLATE BINARY ASC`
959
+ ).bind(serviceId, viewId, generation, safeHead).all();
960
+ let collected = 0;
961
+ for (const row of candidates.results) {
962
+ const candidate = {
963
+ rowKey: string(row, "row_key"),
964
+ rowVersion: integer(row, "row_version"),
965
+ sourceSuid: string(row, "source_suid")
966
+ };
967
+ if (await this.garbageCollect({
968
+ serviceId,
969
+ viewId,
970
+ generation,
971
+ definitionVersion,
972
+ rowKey: candidate.rowKey,
973
+ expectedRowVersion: candidate.rowVersion,
974
+ expectedSourceSuid: candidate.sourceSuid,
975
+ safeHead
976
+ })) collected += 1;
977
+ }
978
+ return collected;
979
+ }
980
+ async queryComposedPage(serviceId, viewId, generation, limit, offset, descending = false) {
981
+ const meta = await this.readMeta(serviceId, viewId, generation);
982
+ if (meta.rebuildRequired) throw new UnsafeWindowMaterializedViewError("UNSAFE_BEHIND_FRONTIER", false, "Unsafe window is rebuilding after a behind-frontier arrival");
983
+ const direction = descending ? "DESC" : "ASC";
984
+ const result = await this.database.prepare(
985
+ `WITH candidates AS (
986
+ SELECT row_key, value_json, row_version, source_suid, 0 AS tombstone, 0 AS unsafe_layer FROM mv_rows
987
+ WHERE service_id = ? AND view_id = ? AND generation = ?
988
+ UNION ALL
989
+ SELECT row_key, value_json, row_version, source_suid, tombstone, 1 AS unsafe_layer FROM mv_unsafe_rows
990
+ WHERE service_id = ? AND view_id = ? AND generation = ?
991
+ ), ranked AS (
992
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY row_key ORDER BY source_suid COLLATE BINARY DESC, unsafe_layer ASC) AS winner_rank FROM candidates
993
+ ), live AS (
994
+ SELECT *, COUNT(*) OVER() AS total_count FROM ranked WHERE winner_rank = 1 AND tombstone = 0
995
+ )
996
+ SELECT row_key, value_json, row_version, source_suid, total_count FROM live
997
+ ORDER BY source_suid COLLATE BINARY ${direction}, row_key COLLATE BINARY ${direction} LIMIT ? OFFSET ?`
998
+ ).bind(serviceId, viewId, generation, serviceId, viewId, generation, limit, offset).all();
999
+ const rows = result.results.map((row) => ({
1000
+ serviceId,
1001
+ viewId,
1002
+ generation,
1003
+ rowKey: string(row, "row_key"),
1004
+ value: assertJsonValue(JSON.parse(string(row, "value_json")), "state-persistence"),
1005
+ rowVersion: integer(row, "row_version"),
1006
+ sourceSuid: string(row, "source_suid")
1007
+ }));
1008
+ return { rows, totalCount: result.results.length === 0 ? 0 : integer(result.results[0], "total_count") };
1009
+ }
1010
+ addIndex(statements, table, input, entry) {
1011
+ const typed = entry.valueType === "text" ? [entry.value, null, null] : entry.valueType === "integer" ? [null, entry.value, null] : [null, null, entry.value];
1012
+ statements.push(this.database.prepare(
1013
+ `INSERT INTO ${table} (service_id, view_id, generation, index_id, value_type, text_value, integer_value, real_value, row_key)
1014
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING`
1015
+ ).bind(input.serviceId, input.viewId, input.generation, entry.indexId, entry.valueType, ...typed, entry.rowKey));
1016
+ }
1017
+ };
1018
+
1019
+ // src/mv/MaterializedViewStore.ts
1020
+ var MaterializedViewStoreError = class extends Error {
1021
+ constructor(operation, code, message, options) {
1022
+ super(message, options);
1023
+ this.operation = operation;
1024
+ this.code = code;
1025
+ this.name = "MaterializedViewStoreError";
1026
+ }
1027
+ operation;
1028
+ code;
1029
+ };
1030
+ var MaterializedViewCasError = class extends MaterializedViewStoreError {
1031
+ constructor(message = "Materialized-view checkpoint CAS did not match") {
1032
+ super("apply", "MV_CAS_MISMATCH", message);
1033
+ this.name = "MaterializedViewCasError";
1034
+ }
1035
+ };
1036
+ var MaterializedViewPromotionCasError = class extends MaterializedViewStoreError {
1037
+ constructor(message = "Materialized-view active-generation CAS did not match") {
1038
+ super("promote", "MV_PROMOTION_CAS_MISMATCH", message);
1039
+ this.name = "MaterializedViewPromotionCasError";
1040
+ }
1041
+ };
1042
+ var MaterializedViewPatchError = class extends MaterializedViewStoreError {
1043
+ constructor(message = "Materialized-view JSON patch target row does not exist") {
1044
+ super("apply", "MV_PATCH_ROW_MISSING", message);
1045
+ this.name = "MaterializedViewPatchError";
1046
+ }
1047
+ };
1048
+ function asString(value, name) {
1049
+ if (typeof value !== "string") throw new Error(`MV ${name} was not a string`);
1050
+ return value;
1051
+ }
1052
+ function asInteger(value, name) {
1053
+ const number = typeof value === "number" ? value : Number(value);
1054
+ if (!Number.isSafeInteger(number)) throw new Error(`MV ${name} was not a safe integer`);
1055
+ return number;
1056
+ }
1057
+ function asStatus(value) {
1058
+ if (value === "active" || value === "candidate" || value === "retired") return value;
1059
+ throw new Error("MV instance status was invalid");
1060
+ }
1061
+ function compareSuid3(left, right) {
1062
+ const leftBytes = new TextEncoder().encode(left);
1063
+ const rightBytes = new TextEncoder().encode(right);
1064
+ for (let index = 0; index < Math.min(leftBytes.length, rightBytes.length); index += 1) {
1065
+ const difference = leftBytes[index] - rightBytes[index];
1066
+ if (difference !== 0) return difference;
1067
+ }
1068
+ return leftBytes.length - rightBytes.length;
1069
+ }
1070
+ function maxReflectedSuid(rows) {
1071
+ let maximum = "";
1072
+ for (const row of rows) {
1073
+ if (maximum === "" || compareSuid3(row.sourceSuid, maximum) > 0) maximum = row.sourceSuid;
1074
+ }
1075
+ return maximum;
1076
+ }
1077
+ function instanceFrom(row) {
1078
+ return {
1079
+ serviceId: asString(row.service_id, "service_id"),
1080
+ viewId: asString(row.view_id, "view_id"),
1081
+ generation: asInteger(row.generation, "generation"),
1082
+ status: asStatus(row.status),
1083
+ lastSuid: asString(row.last_suid, "last_suid"),
1084
+ definitionVersion: asInteger(row.definition_version, "definition_version"),
1085
+ updatedAt: asInteger(row.updated_at, "updated_at"),
1086
+ rebuildVerifiedAt: row.rebuild_verified_at === null || row.rebuild_verified_at === void 0 ? null : asInteger(row.rebuild_verified_at, "rebuild_verified_at"),
1087
+ rebuildRecoveryId: row.rebuild_recovery_id === null || row.rebuild_recovery_id === void 0 ? null : asString(row.rebuild_recovery_id, "rebuild_recovery_id"),
1088
+ rebuildIncidentIdentity: row.rebuild_incident_identity === null || row.rebuild_incident_identity === void 0 ? null : asString(row.rebuild_incident_identity, "rebuild_incident_identity"),
1089
+ rebuildIncidentGeneration: row.rebuild_incident_generation === null || row.rebuild_incident_generation === void 0 ? null : asInteger(row.rebuild_incident_generation, "rebuild_incident_generation"),
1090
+ rebuildSourceEventCount: row.rebuild_source_event_count === null || row.rebuild_source_event_count === void 0 ? null : asInteger(row.rebuild_source_event_count, "rebuild_source_event_count"),
1091
+ rebuildSourceMaxSuid: row.rebuild_source_max_suid === null || row.rebuild_source_max_suid === void 0 ? null : asString(row.rebuild_source_max_suid, "rebuild_source_max_suid"),
1092
+ rebuildSourceHistoryDigest: row.rebuild_source_history_digest === null || row.rebuild_source_history_digest === void 0 ? null : asString(row.rebuild_source_history_digest, "rebuild_source_history_digest")
1093
+ };
1094
+ }
1095
+ function valueType(value) {
1096
+ if (value === "text" || value === "integer" || value === "real") return value;
1097
+ throw new Error("MV index value type was invalid");
1098
+ }
1099
+ function indexValue(row, type) {
1100
+ if (type === "text") return asString(row.text_value, "text_value");
1101
+ const number = typeof row[`${type}_value`] === "number" ? row[`${type}_value`] : Number(row[`${type}_value`]);
1102
+ if (!Number.isFinite(number) || type === "integer" && !Number.isSafeInteger(number)) {
1103
+ throw new Error(`MV ${type}_value was invalid`);
1104
+ }
1105
+ return number;
1106
+ }
1107
+ function operationId2() {
1108
+ return `mv-${crypto.randomUUID()}`;
1109
+ }
1110
+ function isCasFailure(error) {
1111
+ const message = String(error);
1112
+ return message.includes("mv_atomic_guards.checkpoint_match") || message.includes("MV_CAS_MISMATCH");
1113
+ }
1114
+ function isPatchFailure(error) {
1115
+ return String(error).includes("CHECK constraint failed: checkpoint_match");
1116
+ }
1117
+ var D1MaterializedViewStore = class {
1118
+ constructor(database) {
1119
+ this.database = database;
1120
+ this.unsafe = new UnsafeWindowMaterializedViewStore(database);
1121
+ }
1122
+ database;
1123
+ initialized = false;
1124
+ unsafe;
1125
+ async initialize() {
1126
+ if (this.initialized) return;
1127
+ await this.database.prepare("SELECT 1 FROM mv_instances LIMIT 1").all();
1128
+ await this.database.prepare("SELECT 1 FROM mv_atomic_guards LIMIT 1").all();
1129
+ await this.database.prepare("SELECT 1 FROM mv_checkpoint_ahead_findings LIMIT 1").all();
1130
+ await this.database.prepare("SELECT 1 FROM mv_ordering_quarantines LIMIT 1").all();
1131
+ this.initialized = true;
1132
+ }
1133
+ async createActive(input) {
1134
+ this.ready("create-active");
1135
+ const generation = input.generation ?? 0;
1136
+ this.validateGeneration(generation, "create-active");
1137
+ const lastSuid = input.lastSuid ?? "";
1138
+ if (lastSuid.length > 0) assertSortableUniqueId(lastSuid);
1139
+ try {
1140
+ await this.database.batch([
1141
+ this.database.prepare(
1142
+ `INSERT INTO mv_instances
1143
+ (service_id, view_id, generation, status, last_suid, definition_version, updated_at)
1144
+ VALUES (?, ?, ?, 'active', ?, ?, ?)`
1145
+ ).bind(input.serviceId, input.viewId, generation, lastSuid, input.definitionVersion, input.updatedAt),
1146
+ this.database.prepare(
1147
+ `INSERT INTO mv_active_generations (service_id, view_id, generation, updated_at)
1148
+ VALUES (?, ?, ?, ?)`
1149
+ ).bind(input.serviceId, input.viewId, generation, input.updatedAt)
1150
+ ]);
1151
+ } catch (error) {
1152
+ throw new MaterializedViewStoreError("create-active", "MV_INSTANCE_EXISTS", `Active materialized view already exists: ${String(error)}`, { cause: error });
1153
+ }
1154
+ return this.readInstance(input.serviceId, input.viewId, generation);
1155
+ }
1156
+ async createCandidate(input) {
1157
+ this.ready("create-candidate");
1158
+ this.validateGeneration(input.generation, "create-candidate");
1159
+ if ((input.lastSuid ?? "").length > 0) assertSortableUniqueId(input.lastSuid);
1160
+ try {
1161
+ await this.database.prepare(
1162
+ `INSERT INTO mv_instances
1163
+ (service_id, view_id, generation, status, last_suid, definition_version, updated_at)
1164
+ VALUES (?, ?, ?, 'candidate', ?, ?, ?)`
1165
+ ).bind(
1166
+ input.serviceId,
1167
+ input.viewId,
1168
+ input.generation,
1169
+ input.lastSuid ?? "",
1170
+ input.definitionVersion,
1171
+ input.updatedAt
1172
+ ).run();
1173
+ } catch (error) {
1174
+ throw new MaterializedViewStoreError("create-candidate", "MV_INSTANCE_EXISTS", `Candidate generation already exists: ${String(error)}`, { cause: error });
1175
+ }
1176
+ return this.readInstance(input.serviceId, input.viewId, input.generation);
1177
+ }
1178
+ async beginRebuild(input) {
1179
+ this.ready("create-candidate");
1180
+ const generation = input.generation ?? await this.nextGeneration(input.serviceId, input.viewId);
1181
+ return this.createCandidate({ ...input, generation });
1182
+ }
1183
+ async nextGeneration(serviceId, viewId) {
1184
+ this.ready("create-candidate");
1185
+ const row = await this.database.prepare(
1186
+ `SELECT COALESCE(MAX(generation), -1) AS generation FROM mv_instances WHERE service_id = ? AND view_id = ?`
1187
+ ).bind(serviceId, viewId).first();
1188
+ return asInteger(row?.generation ?? -1, "generation") + 1;
1189
+ }
1190
+ async readInstance(serviceId, viewId, generation) {
1191
+ this.ready("initialize");
1192
+ const row = await this.database.prepare(
1193
+ `SELECT service_id, view_id, generation, status, last_suid, definition_version, updated_at,
1194
+ rebuild_verified_at, rebuild_recovery_id, rebuild_incident_identity,
1195
+ rebuild_incident_generation, rebuild_source_event_count,
1196
+ rebuild_source_max_suid, rebuild_source_history_digest
1197
+ FROM mv_instances WHERE service_id = ? AND view_id = ? AND generation = ?`
1198
+ ).bind(serviceId, viewId, generation).first();
1199
+ return row === null || row === void 0 ? void 0 : instanceFrom(row);
1200
+ }
1201
+ async readActive(serviceId, viewId) {
1202
+ this.ready("initialize");
1203
+ const row = await this.database.prepare(
1204
+ `SELECT instance.service_id, instance.view_id, instance.generation, instance.status,
1205
+ instance.last_suid, instance.definition_version, instance.updated_at,
1206
+ instance.rebuild_verified_at, instance.rebuild_recovery_id,
1207
+ instance.rebuild_incident_identity, instance.rebuild_incident_generation,
1208
+ instance.rebuild_source_event_count, instance.rebuild_source_max_suid,
1209
+ instance.rebuild_source_history_digest
1210
+ FROM mv_active_generations pointer
1211
+ JOIN mv_instances instance
1212
+ ON instance.service_id = pointer.service_id
1213
+ AND instance.view_id = pointer.view_id
1214
+ AND instance.generation = pointer.generation
1215
+ WHERE pointer.service_id = ? AND pointer.view_id = ?`
1216
+ ).bind(serviceId, viewId).first();
1217
+ return row === null || row === void 0 ? void 0 : instanceFrom(row);
1218
+ }
1219
+ async readRows(serviceId, viewId, generation) {
1220
+ this.ready("initialize");
1221
+ const selected = generation ?? (await this.readActive(serviceId, viewId))?.generation;
1222
+ if (selected === void 0) return [];
1223
+ const result = await this.database.prepare(
1224
+ `SELECT service_id, view_id, generation, row_key, value_json, row_version, source_suid
1225
+ FROM mv_rows
1226
+ WHERE service_id = ? AND view_id = ? AND generation = ?
1227
+ ORDER BY row_key COLLATE BINARY ASC`
1228
+ ).bind(serviceId, viewId, selected).all();
1229
+ return result.results.map((row) => this.rowFrom(row));
1230
+ }
1231
+ async readIndexEntries(serviceId, viewId, generation) {
1232
+ this.ready("initialize");
1233
+ const selected = generation ?? (await this.readActive(serviceId, viewId))?.generation;
1234
+ if (selected === void 0) return [];
1235
+ const result = await this.database.prepare(
1236
+ `SELECT service_id, view_id, generation, index_id, value_type,
1237
+ text_value, integer_value, real_value, row_key
1238
+ FROM mv_index_entries
1239
+ WHERE service_id = ? AND view_id = ? AND generation = ?
1240
+ ORDER BY index_id COLLATE BINARY ASC, value_type COLLATE BINARY ASC,
1241
+ COALESCE(text_value, CAST(integer_value AS TEXT), CAST(real_value AS TEXT)) COLLATE BINARY ASC,
1242
+ row_key COLLATE BINARY ASC`
1243
+ ).bind(serviceId, viewId, selected).all();
1244
+ return result.results.map((row) => this.indexFrom(row));
1245
+ }
1246
+ /** Read rows through a declared index without exposing SQL identifiers to callers. */
1247
+ async queryRows(serviceId, viewId, options = {}) {
1248
+ this.ready("initialize");
1249
+ const selected = options.generation ?? (await this.readActive(serviceId, viewId))?.generation;
1250
+ if (selected === void 0) return [];
1251
+ const limit = options.limit === void 0 ? 100 : options.limit;
1252
+ const offset = options.offset ?? 0;
1253
+ if (limit !== null && (!Number.isSafeInteger(limit) || limit < 0) || !Number.isSafeInteger(offset) || offset < 0) {
1254
+ throw new MaterializedViewStoreError("initialize", "MV_VALUE_INVALID", "MV query limit/offset must be non-negative integers");
1255
+ }
1256
+ if (options.indexId === void 0) {
1257
+ const limitClause2 = limit === null ? "" : " LIMIT ? OFFSET ?";
1258
+ const direction2 = options.descending === true ? "DESC" : "ASC";
1259
+ const values2 = [serviceId, viewId, selected];
1260
+ if (limit !== null) values2.push(limit, offset);
1261
+ const result2 = await this.database.prepare(
1262
+ `SELECT service_id, view_id, generation, row_key, value_json, row_version, source_suid
1263
+ FROM mv_rows
1264
+ WHERE service_id = ? AND view_id = ? AND generation = ?
1265
+ ORDER BY row_key COLLATE BINARY ${direction2}${limitClause2}`
1266
+ ).bind(...values2).all();
1267
+ return result2.results.map((row) => this.rowFrom(row));
1268
+ }
1269
+ const valueType2 = options.valueType;
1270
+ const orderColumn = valueType2 === "text" ? "index_entry.text_value" : valueType2 === "integer" ? "index_entry.integer_value" : valueType2 === "real" ? "index_entry.real_value" : "index_entry.row_key";
1271
+ const direction = options.descending === true ? "DESC" : "ASC";
1272
+ const indexPredicate = options.indexId === void 0 ? "" : " AND index_entry.index_id = ?";
1273
+ const typePredicate = valueType2 === void 0 ? "" : " AND index_entry.value_type = ?";
1274
+ const values = [serviceId, viewId, selected];
1275
+ if (options.indexId !== void 0) values.push(options.indexId);
1276
+ if (valueType2 !== void 0) values.push(valueType2);
1277
+ if (limit !== null) values.push(limit, offset);
1278
+ const limitClause = limit === null ? "" : " LIMIT ? OFFSET ?";
1279
+ const result = await this.database.prepare(
1280
+ `SELECT row.service_id, row.view_id, row.generation, row.row_key,
1281
+ row.value_json, row.row_version, row.source_suid
1282
+ FROM mv_index_entries index_entry
1283
+ JOIN mv_rows row
1284
+ ON row.service_id = index_entry.service_id
1285
+ AND row.view_id = index_entry.view_id
1286
+ AND row.generation = index_entry.generation
1287
+ AND row.row_key = index_entry.row_key
1288
+ WHERE row.service_id = ? AND row.view_id = ? AND row.generation = ?${indexPredicate}${typePredicate}
1289
+ ORDER BY ${orderColumn} COLLATE BINARY ${direction}, row.row_key COLLATE BINARY ${direction}${limitClause}`
1290
+ ).bind(...values).all();
1291
+ return result.results.map((row) => this.rowFrom(row));
1292
+ }
1293
+ /** SDT-G23 composed server page; count and page rows share one statement. */
1294
+ async queryRowsWithTotal(serviceId, viewId, options = {}) {
1295
+ this.ready("initialize");
1296
+ const selected = options.generation ?? (await this.readActive(serviceId, viewId))?.generation;
1297
+ if (selected === void 0) return { rows: [], totalCount: 0 };
1298
+ const limit = options.limit === null ? -1 : options.limit === void 0 ? 100 : options.limit;
1299
+ const offset = options.offset ?? 0;
1300
+ if (!Number.isSafeInteger(limit) || limit < -1 || !Number.isSafeInteger(offset) || offset < 0) {
1301
+ throw new MaterializedViewStoreError("initialize", "MV_VALUE_INVALID", "MV composed query needs limit -1 or a non-negative limit/offset");
1302
+ }
1303
+ return this.unsafe.queryComposedPage(serviceId, viewId, selected, limit, offset, options.descending === true);
1304
+ }
1305
+ /**
1306
+ * SDT-G55's explicit list-read port. Existing composed reads intentionally
1307
+ * keep their G23 semantics; only this port can include unsafe rows, and it
1308
+ * can include only rows newer than the active safe checkpoint.
1309
+ */
1310
+ async readListPage(serviceId, viewId, options = {}) {
1311
+ this.ready("initialize");
1312
+ const active = await this.readActive(serviceId, viewId);
1313
+ if (active === void 0) return { rows: [], totalCount: 0, readHead: "" };
1314
+ const selected = options.generation ?? active.generation;
1315
+ const selectedInstance = selected === active.generation ? active : await this.readInstance(serviceId, viewId, selected);
1316
+ if (selectedInstance === void 0) {
1317
+ throw new MaterializedViewStoreError("initialize", "MV_INSTANCE_MISSING", "Requested materialized-view generation is unavailable");
1318
+ }
1319
+ const limit = options.limit === null ? -1 : options.limit === void 0 ? 100 : options.limit;
1320
+ const offset = options.offset ?? 0;
1321
+ if (!Number.isSafeInteger(limit) || limit < -1 || !Number.isSafeInteger(offset) || offset < 0) {
1322
+ throw new MaterializedViewStoreError("initialize", "MV_VALUE_INVALID", "MV list query needs limit -1 or a non-negative limit/offset");
1323
+ }
1324
+ if (options.indexId !== void 0 || options.valueType !== void 0) {
1325
+ throw new MaterializedViewStoreError("initialize", "MV_VALUE_INVALID", "MV list query does not support an index selector");
1326
+ }
1327
+ const direction = options.descending === true ? "DESC" : "ASC";
1328
+ const safeRows = `SELECT row_key, value_json, row_version, source_suid, 0 AS tombstone, 0 AS unsafe_layer FROM mv_rows
1329
+ WHERE service_id = ? AND view_id = ? AND generation = ?`;
1330
+ const result = options.consistency === "unsafe" ? await this.database.prepare(
1331
+ `WITH candidates AS (
1332
+ ${safeRows}
1333
+ UNION ALL
1334
+ SELECT row_key, value_json, row_version, source_suid, tombstone, 1 AS unsafe_layer FROM mv_unsafe_rows
1335
+ WHERE service_id = ? AND view_id = ? AND generation = ?
1336
+ AND source_suid COLLATE BINARY > ? COLLATE BINARY
1337
+ ), ranked AS (
1338
+ SELECT *, ROW_NUMBER() OVER (PARTITION BY row_key ORDER BY source_suid COLLATE BINARY DESC, unsafe_layer ASC) AS winner_rank FROM candidates
1339
+ ), live AS (
1340
+ SELECT *, COUNT(*) OVER() AS total_count FROM ranked WHERE winner_rank = 1 AND tombstone = 0
1341
+ )
1342
+ SELECT row_key, value_json, row_version, source_suid, total_count FROM live
1343
+ ORDER BY source_suid COLLATE BINARY ${direction}, row_key COLLATE BINARY ${direction} LIMIT ? OFFSET ?`
1344
+ ).bind(serviceId, viewId, selected, serviceId, viewId, selected, selectedInstance.lastSuid, limit, offset).all() : await this.database.prepare(
1345
+ `WITH live AS (
1346
+ SELECT row_key, value_json, row_version, source_suid, COUNT(*) OVER() AS total_count FROM mv_rows
1347
+ WHERE service_id = ? AND view_id = ? AND generation = ?
1348
+ )
1349
+ SELECT row_key, value_json, row_version, source_suid, total_count FROM live
1350
+ ORDER BY source_suid COLLATE BINARY ${direction}, row_key COLLATE BINARY ${direction} LIMIT ? OFFSET ?`
1351
+ ).bind(serviceId, viewId, selected, limit, offset).all();
1352
+ const rows = result.results.map((row) => ({
1353
+ serviceId,
1354
+ viewId,
1355
+ generation: selected,
1356
+ rowKey: asString(row.row_key, "row_key"),
1357
+ value: assertJsonValue(JSON.parse(asString(row.value_json, "value_json")), "state-persistence"),
1358
+ rowVersion: asInteger(row.row_version, "row_version"),
1359
+ sourceSuid: asString(row.source_suid, "source_suid")
1360
+ }));
1361
+ return {
1362
+ rows,
1363
+ totalCount: result.results.length === 0 ? 0 : asInteger(result.results[0].total_count, "total_count"),
1364
+ // Safe reads report the active checkpoint even for an empty page. Unsafe
1365
+ // reads instead report only what this page actually reflected.
1366
+ readHead: options.consistency === "unsafe" ? maxReflectedSuid(rows) : selectedInstance.lastSuid
1367
+ };
1368
+ }
1369
+ /** Explicit unsafe port: callers must choose this exceptional immediate lane. */
1370
+ unsafeWindow() {
1371
+ this.ready("apply");
1372
+ return this.unsafe;
1373
+ }
1374
+ /** Runs repairable unsafe GC after an idle or advancing safe catch-up pass. */
1375
+ async collectUnsafeGarbage(serviceId, viewId, generation, definitionVersion, safeHead) {
1376
+ this.ready("apply");
1377
+ return this.unsafe.collectEligible(serviceId, viewId, generation, definitionVersion, safeHead);
1378
+ }
1379
+ async hasTargetReceipt(serviceId, viewId, eventId, suid) {
1380
+ this.ready("initialize");
1381
+ return this.unsafe.hasTargetReceipt(serviceId, viewId, eventId, suid);
1382
+ }
1383
+ /**
1384
+ * Read all G31 wait facts in one static D1 statement. Every table access is
1385
+ * keyed by service/view/active generation (and target identity where
1386
+ * applicable); no row or source-history scan is permitted on this path.
1387
+ */
1388
+ async readWaitForState(serviceId, viewId, target) {
1389
+ this.ready("initialize");
1390
+ const row = await this.database.prepare(
1391
+ `WITH active AS (
1392
+ SELECT instance.generation, instance.definition_version, instance.last_suid
1393
+ FROM mv_active_generations pointer
1394
+ JOIN mv_instances instance
1395
+ ON instance.service_id = pointer.service_id
1396
+ AND instance.view_id = pointer.view_id
1397
+ AND instance.generation = pointer.generation
1398
+ WHERE pointer.service_id = ? AND pointer.view_id = ?
1399
+ )
1400
+ SELECT
1401
+ (SELECT generation FROM active) AS active_generation,
1402
+ (SELECT definition_version FROM active) AS active_definition_version,
1403
+ COALESCE((SELECT last_suid FROM active), '') AS safe_contiguous_head,
1404
+ CASE WHEN EXISTS (
1405
+ SELECT 1
1406
+ FROM mv_wait_receipts receipt
1407
+ JOIN active
1408
+ ON active.generation = receipt.generation
1409
+ AND active.definition_version = receipt.definition_version
1410
+ WHERE receipt.service_id = ? AND receipt.view_id = ?
1411
+ AND receipt.event_id = ? AND receipt.suid = ?
1412
+ ) THEN 1 ELSE 0 END AS target_receipt,
1413
+ CASE WHEN EXISTS (
1414
+ SELECT 1
1415
+ FROM mv_checkpoint_ahead_findings finding
1416
+ JOIN active
1417
+ ON active.generation = finding.generation
1418
+ WHERE finding.service_id = ? AND finding.view_id = ?
1419
+ ) THEN 1 ELSE 0 END AS checkpoint_ahead,
1420
+ COALESCE((
1421
+ SELECT arrival.rebuild_required
1422
+ FROM mv_unsafe_arrivals arrival
1423
+ JOIN active
1424
+ ON active.generation = arrival.generation
1425
+ WHERE arrival.service_id = ? AND arrival.view_id = ?
1426
+ ), 0) AS rebuild_required,
1427
+ CASE WHEN EXISTS (
1428
+ SELECT 1
1429
+ FROM mv_wait_target_poison finding
1430
+ JOIN active
1431
+ ON active.generation = finding.generation
1432
+ AND active.definition_version = finding.definition_version
1433
+ WHERE finding.service_id = ? AND finding.view_id = ?
1434
+ AND finding.suid = ?
1435
+ AND (? IS NULL OR finding.event_id = ?)
1436
+ ) THEN 1 ELSE 0 END AS poison`
1437
+ ).bind(
1438
+ serviceId,
1439
+ viewId,
1440
+ serviceId,
1441
+ viewId,
1442
+ target.eventId ?? "",
1443
+ target.suid,
1444
+ serviceId,
1445
+ viewId,
1446
+ serviceId,
1447
+ viewId,
1448
+ serviceId,
1449
+ viewId,
1450
+ target.suid,
1451
+ target.eventId ?? null,
1452
+ target.eventId ?? null
1453
+ ).first();
1454
+ if (row === null || row === void 0) {
1455
+ throw new MaterializedViewStoreError("initialize", "MV_STORE_OPERATION_FAILED", "D1 wait-state query returned no row");
1456
+ }
1457
+ const generation = row.active_generation === null || row.active_generation === void 0 ? void 0 : asInteger(row.active_generation, "active_generation");
1458
+ const definitionVersion = row.active_definition_version === null || row.active_definition_version === void 0 ? void 0 : asInteger(row.active_definition_version, "active_definition_version");
1459
+ return {
1460
+ activeGeneration: generation,
1461
+ activeDefinitionVersion: definitionVersion,
1462
+ safeContiguousHead: asString(row.safe_contiguous_head, "safe_contiguous_head"),
1463
+ targetReceipt: asInteger(row.target_receipt, "target_receipt") === 1,
1464
+ checkpointAhead: asInteger(row.checkpoint_ahead, "checkpoint_ahead") === 1,
1465
+ rebuildRequired: asInteger(row.rebuild_required, "rebuild_required") === 1,
1466
+ poison: asInteger(row.poison, "poison") === 1
1467
+ };
1468
+ }
1469
+ /** Idempotent operational fact: a stored Queue event needs unsafe retry/DLQ attention. */
1470
+ async recordUnsafeFailureFinding(input) {
1471
+ this.ready("apply");
1472
+ const legacyFinding = this.database.prepare(
1473
+ `INSERT INTO mv_unsafe_failure_findings
1474
+ (service_id, view_id, event_id, suid, classification, observed_at)
1475
+ VALUES (?, ?, ?, ?, 'UNSAFE_APPLY_RETRY', ?)
1476
+ ON CONFLICT (service_id, view_id, event_id, suid, classification) DO NOTHING`
1477
+ ).bind(input.serviceId, input.viewId, input.eventId, input.suid, input.observedAt);
1478
+ const targetPoison = input.generation === void 0 ? this.database.prepare(
1479
+ `INSERT INTO mv_wait_target_poison
1480
+ (service_id, view_id, generation, definition_version, event_id, suid, classification, observed_at)
1481
+ SELECT instance.service_id, instance.view_id, instance.generation, instance.definition_version, ?, ?, 'UNSAFE_APPLY_RETRY', ?
1482
+ FROM mv_active_generations active
1483
+ JOIN mv_instances instance
1484
+ ON instance.service_id = active.service_id
1485
+ AND instance.view_id = active.view_id
1486
+ AND instance.generation = active.generation
1487
+ WHERE active.service_id = ? AND active.view_id = ?
1488
+ ON CONFLICT DO NOTHING`
1489
+ ).bind(input.eventId, input.suid, input.observedAt, input.serviceId, input.viewId) : this.database.prepare(
1490
+ `INSERT INTO mv_wait_target_poison
1491
+ (service_id, view_id, generation, definition_version, event_id, suid, classification, observed_at)
1492
+ SELECT instance.service_id, instance.view_id, instance.generation, instance.definition_version, ?, ?, 'UNSAFE_APPLY_RETRY', ?
1493
+ FROM mv_instances instance
1494
+ WHERE instance.service_id = ? AND instance.view_id = ? AND instance.generation = ?
1495
+ ON CONFLICT DO NOTHING`
1496
+ ).bind(input.eventId, input.suid, input.observedAt, input.serviceId, input.viewId, input.generation);
1497
+ await this.database.batch([legacyFinding, targetPoison]);
1498
+ }
1499
+ /**
1500
+ * Persist the source-reset observation exactly once for the affected
1501
+ * generation. This deliberately never rewinds `mv_instances.last_suid`.
1502
+ */
1503
+ async recordCheckpointAhead(input) {
1504
+ this.ready("apply");
1505
+ await this.database.prepare(
1506
+ `INSERT INTO mv_checkpoint_ahead_findings
1507
+ (service_id, view_id, generation, checkpoint_suid, store_max_suid, observed_at)
1508
+ VALUES (?, ?, ?, ?, ?, ?)
1509
+ ON CONFLICT (service_id, view_id, generation) DO NOTHING`
1510
+ ).bind(
1511
+ input.serviceId,
1512
+ input.viewId,
1513
+ input.generation,
1514
+ input.checkpointSuid,
1515
+ input.storeMaxSuid,
1516
+ input.observedAt
1517
+ ).run();
1518
+ }
1519
+ /** An open finding on the active generation fail-closes composed MV reads. */
1520
+ async hasCheckpointAheadFinding(serviceId, viewId) {
1521
+ this.ready("initialize");
1522
+ const result = await this.database.prepare(
1523
+ `SELECT 1 AS present
1524
+ FROM mv_checkpoint_ahead_findings finding
1525
+ JOIN mv_active_generations active
1526
+ ON active.service_id = finding.service_id
1527
+ AND active.view_id = finding.view_id
1528
+ AND active.generation = finding.generation
1529
+ WHERE finding.service_id = ? AND finding.view_id = ?
1530
+ LIMIT 1`
1531
+ ).bind(serviceId, viewId).first();
1532
+ return result?.present === 1;
1533
+ }
1534
+ /** Open quarantine is keyed to the active generation; old generations are not a public read gate. */
1535
+ async readOrderingQuarantine(serviceId, viewId) {
1536
+ this.ready("initialize");
1537
+ const row = await this.database.prepare(
1538
+ `SELECT quarantine.service_id, quarantine.view_id, quarantine.generation,
1539
+ quarantine.checkpoint_suid, quarantine.late_suid, quarantine.event_id,
1540
+ quarantine.classification, quarantine.status, quarantine.observed_at,
1541
+ quarantine.resolved_at, quarantine.incident_identity
1542
+ FROM mv_ordering_quarantines quarantine
1543
+ JOIN mv_active_generations active
1544
+ ON active.service_id = quarantine.service_id
1545
+ AND active.view_id = quarantine.view_id
1546
+ AND active.generation = quarantine.generation
1547
+ WHERE quarantine.service_id = ? AND quarantine.view_id = ? AND quarantine.status = 'open'
1548
+ LIMIT 1`
1549
+ ).bind(serviceId, viewId).first();
1550
+ if (row === null || row === void 0) return void 0;
1551
+ const classification = asString(row.classification, "ordering_quarantine.classification");
1552
+ if (classification !== "LATE_LOWER_SUID" && classification !== "ORDER_VIOLATION") {
1553
+ throw new MaterializedViewStoreError("initialize", "MV_STORE_OPERATION_FAILED", "Materialized-view ordering quarantine classification was invalid");
1554
+ }
1555
+ const status = asString(row.status, "ordering_quarantine.status");
1556
+ if (status !== "open" && status !== "resolved") {
1557
+ throw new MaterializedViewStoreError("initialize", "MV_STORE_OPERATION_FAILED", "Materialized-view ordering quarantine status was invalid");
1558
+ }
1559
+ return {
1560
+ serviceId: asString(row.service_id, "ordering_quarantine.service_id"),
1561
+ viewId: asString(row.view_id, "ordering_quarantine.view_id"),
1562
+ generation: asInteger(row.generation, "ordering_quarantine.generation"),
1563
+ checkpointSuid: asString(row.checkpoint_suid, "ordering_quarantine.checkpoint_suid"),
1564
+ lateSuid: asString(row.late_suid, "ordering_quarantine.late_suid"),
1565
+ eventId: asString(row.event_id, "ordering_quarantine.event_id"),
1566
+ classification,
1567
+ status,
1568
+ observedAt: asInteger(row.observed_at, "ordering_quarantine.observed_at"),
1569
+ resolvedAt: row.resolved_at === null || row.resolved_at === void 0 ? null : asInteger(row.resolved_at, "ordering_quarantine.resolved_at"),
1570
+ incidentIdentity: row.incident_identity === null || row.incident_identity === void 0 ? null : asString(row.incident_identity, "ordering_quarantine.incident_identity")
1571
+ };
1572
+ }
1573
+ async readActiveGeneration(serviceId, viewId) {
1574
+ this.ready("initialize");
1575
+ return (await this.readActive(serviceId, viewId))?.generation;
1576
+ }
1577
+ /** Persist the detector result before stopping the safe pass. Repeated observations are idempotent. */
1578
+ async recordOrderingQuarantine(input) {
1579
+ this.ready("apply");
1580
+ this.validateGeneration(input.generation, "apply");
1581
+ const incidentIdentity = `${input.classification}|${input.serviceId}|${input.viewId}|${input.checkpointSuid}|${input.lateSuid}|${input.eventId}`;
1582
+ await this.database.prepare(
1583
+ `INSERT INTO mv_ordering_quarantines
1584
+ (service_id, view_id, generation, checkpoint_suid, late_suid, event_id, classification, status, observed_at, resolved_at, incident_identity)
1585
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, NULL, ?)
1586
+ ON CONFLICT (service_id, view_id, generation) DO UPDATE SET
1587
+ checkpoint_suid = excluded.checkpoint_suid,
1588
+ late_suid = excluded.late_suid,
1589
+ event_id = excluded.event_id,
1590
+ classification = excluded.classification,
1591
+ status = 'open',
1592
+ observed_at = MIN(mv_ordering_quarantines.observed_at, excluded.observed_at),
1593
+ resolved_at = NULL,
1594
+ incident_identity = excluded.incident_identity`
1595
+ ).bind(
1596
+ input.serviceId,
1597
+ input.viewId,
1598
+ input.generation,
1599
+ input.checkpointSuid,
1600
+ input.lateSuid,
1601
+ input.eventId,
1602
+ input.classification,
1603
+ input.observedAt,
1604
+ incidentIdentity
1605
+ ).run();
1606
+ }
1607
+ /**
1608
+ * Mark a candidate only after the real rebuild path observed the complete
1609
+ * source history. The proof is incident-bound when an ordering quarantine is
1610
+ * open, and an empty row set is valid when the source history itself is
1611
+ * complete (for example, a rebuild whose materializer deletes every row).
1612
+ */
1613
+ async markGenerationRebuilt(input) {
1614
+ this.ready("promote");
1615
+ this.validateGeneration(input.generation, "promote");
1616
+ if (input.rebuildId.length === 0 || !Number.isSafeInteger(input.sourceEventCount) || input.sourceEventCount < 0 || input.sourceEventIds.length !== input.sourceEventCount || input.sourceSuids.length !== input.sourceEventCount || (input.sourceEventCount === 0 ? input.sourceMaxSuid !== "" : input.sourceMaxSuid.length === 0) || input.sourceHistoryDigest.length === 0) {
1617
+ throw new MaterializedViewStoreError("promote", "MV_REBUILD_NOT_VERIFIED", "Rebuild proof does not describe a complete source history");
1618
+ }
1619
+ const quarantine = await this.database.prepare(
1620
+ `SELECT generation, incident_identity, event_id, late_suid
1621
+ FROM mv_ordering_quarantines
1622
+ WHERE service_id = ? AND view_id = ? AND status = 'open'
1623
+ ORDER BY generation DESC LIMIT 1`
1624
+ ).bind(input.serviceId, input.viewId).first();
1625
+ const incidentGeneration = quarantine === null || quarantine === void 0 ? null : asInteger(quarantine.generation, "rebuild.incident_generation");
1626
+ const incidentIdentity = quarantine === null || quarantine === void 0 ? null : typeof quarantine.incident_identity === "string" && quarantine.incident_identity.length > 0 ? quarantine.incident_identity : null;
1627
+ if (quarantine !== null && quarantine !== void 0) {
1628
+ if (incidentIdentity === null) {
1629
+ throw new MaterializedViewStoreError("promote", "MV_REBUILD_NOT_VERIFIED", "Ordering quarantine has no durable incident identity");
1630
+ }
1631
+ const incidentEventId = asString(quarantine.event_id, "rebuild.incident_event_id");
1632
+ const incidentLateSuid = asString(quarantine.late_suid, "rebuild.incident_late_suid");
1633
+ if (!input.sourceEventIds.includes(incidentEventId) || !input.sourceSuids.includes(incidentLateSuid)) {
1634
+ throw new MaterializedViewStoreError("promote", "MV_REBUILD_NOT_VERIFIED", "Rebuild source history does not include the quarantined event");
1635
+ }
1636
+ }
1637
+ const result = await this.database.prepare(
1638
+ `UPDATE mv_instances
1639
+ SET rebuild_verified_at = ?,
1640
+ rebuild_recovery_id = ?,
1641
+ rebuild_incident_identity = ?,
1642
+ rebuild_incident_generation = ?,
1643
+ rebuild_source_event_count = ?,
1644
+ rebuild_source_max_suid = ?,
1645
+ rebuild_source_history_digest = ?
1646
+ WHERE service_id = ? AND view_id = ? AND generation = ?
1647
+ AND status = 'candidate'
1648
+ AND last_suid COLLATE BINARY = ? COLLATE BINARY`
1649
+ ).bind(
1650
+ input.verifiedAt,
1651
+ input.rebuildId,
1652
+ incidentIdentity,
1653
+ incidentGeneration,
1654
+ input.sourceEventCount,
1655
+ input.sourceMaxSuid,
1656
+ input.sourceHistoryDigest,
1657
+ input.serviceId,
1658
+ input.viewId,
1659
+ input.generation,
1660
+ input.sourceMaxSuid
1661
+ ).run();
1662
+ if ((result.meta?.changes ?? 0) !== 1) {
1663
+ throw new MaterializedViewStoreError("promote", "MV_REBUILD_NOT_VERIFIED", "Candidate generation does not match the complete rebuild source history");
1664
+ }
1665
+ }
1666
+ /**
1667
+ * Apply rows/index entries and advance one generation checkpoint atomically.
1668
+ * The guard row intentionally uses a NOT NULL failure to abort a stale CAS.
1669
+ */
1670
+ async applyMutationsAndAdvanceCheckpoint(input) {
1671
+ assertSortableUniqueId(input.lastSuid);
1672
+ if (input.expectedLastSuid !== null) assertSortableUniqueId(input.expectedLastSuid);
1673
+ for (const mutation of [...input.mutations.rowUpserts, ...input.mutations.rowPatches]) {
1674
+ assertSortableUniqueId(mutation.sourceSuid);
1675
+ }
1676
+ this.ready("apply");
1677
+ this.validateGeneration(input.generation, "apply");
1678
+ const operation = operationId2();
1679
+ const statements = [
1680
+ this.database.prepare(
1681
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
1682
+ SELECT ?, CASE WHEN EXISTS (
1683
+ SELECT 1 FROM mv_instances
1684
+ WHERE service_id = ? AND view_id = ? AND generation = ?
1685
+ AND ((? IS NULL AND last_suid = '') OR last_suid COLLATE BINARY = ? COLLATE BINARY)
1686
+ ) THEN 1 ELSE NULL END`
1687
+ ).bind(
1688
+ operation,
1689
+ input.serviceId,
1690
+ input.viewId,
1691
+ input.generation,
1692
+ input.expectedLastSuid,
1693
+ input.expectedLastSuid
1694
+ )
1695
+ ];
1696
+ for (const patch of input.mutations.rowPatches) {
1697
+ statements.push(this.database.prepare(
1698
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
1699
+ SELECT ?, CASE WHEN EXISTS (
1700
+ SELECT 1 FROM mv_rows
1701
+ WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key COLLATE BINARY = ? COLLATE BINARY
1702
+ ) THEN 1 ELSE 0 END`
1703
+ ).bind(
1704
+ `${operation}:patch:${patch.rowKey}`,
1705
+ input.serviceId,
1706
+ input.viewId,
1707
+ input.generation,
1708
+ patch.rowKey
1709
+ ));
1710
+ }
1711
+ for (const deletion of input.mutations.rowDeletes) {
1712
+ statements.push(this.database.prepare(
1713
+ `DELETE FROM mv_index_entries WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`
1714
+ ).bind(input.serviceId, input.viewId, input.generation, deletion.rowKey));
1715
+ statements.push(this.database.prepare(
1716
+ `DELETE FROM mv_rows WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`
1717
+ ).bind(input.serviceId, input.viewId, input.generation, deletion.rowKey));
1718
+ }
1719
+ for (const upsert of input.mutations.rowUpserts) {
1720
+ statements.push(this.database.prepare(
1721
+ `DELETE FROM mv_index_entries WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`
1722
+ ).bind(input.serviceId, input.viewId, input.generation, upsert.rowKey));
1723
+ statements.push(this.database.prepare(
1724
+ `INSERT INTO mv_rows
1725
+ (service_id, view_id, generation, row_key, value_json, row_version, source_suid)
1726
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1727
+ ON CONFLICT (service_id, view_id, generation, row_key) DO UPDATE SET
1728
+ value_json = excluded.value_json,
1729
+ row_version = excluded.row_version,
1730
+ source_suid = excluded.source_suid`
1731
+ ).bind(
1732
+ input.serviceId,
1733
+ input.viewId,
1734
+ input.generation,
1735
+ upsert.rowKey,
1736
+ JSON.stringify(assertJsonValue(upsert.value, "state-persistence")),
1737
+ upsert.rowVersion,
1738
+ upsert.sourceSuid
1739
+ ));
1740
+ }
1741
+ for (const patch of input.mutations.rowPatches) {
1742
+ if (patch.indexEntries.length > 0) {
1743
+ statements.push(this.database.prepare(
1744
+ `DELETE FROM mv_index_entries WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?`
1745
+ ).bind(input.serviceId, input.viewId, input.generation, patch.rowKey));
1746
+ }
1747
+ statements.push(this.database.prepare(
1748
+ `UPDATE mv_rows
1749
+ SET value_json = json_patch(value_json, ?), row_version = ?, source_suid = ?
1750
+ WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key COLLATE BINARY = ? COLLATE BINARY`
1751
+ ).bind(
1752
+ JSON.stringify(assertJsonValue(patch.patch, "state-persistence")),
1753
+ patch.rowVersion,
1754
+ patch.sourceSuid,
1755
+ input.serviceId,
1756
+ input.viewId,
1757
+ input.generation,
1758
+ patch.rowKey
1759
+ ));
1760
+ }
1761
+ for (const deletion of input.mutations.indexDeletes) {
1762
+ const indexPredicate = deletion.indexId === void 0 ? "" : " AND index_id = ?";
1763
+ const values = [input.serviceId, input.viewId, input.generation, deletion.rowKey];
1764
+ if (deletion.indexId !== void 0) values.push(deletion.indexId);
1765
+ statements.push(this.database.prepare(
1766
+ `DELETE FROM mv_index_entries
1767
+ WHERE service_id = ? AND view_id = ? AND generation = ? AND row_key = ?${indexPredicate}`
1768
+ ).bind(...values));
1769
+ }
1770
+ for (const entry of input.mutations.indexEntries) {
1771
+ const typed = entry.valueType === "text" ? [entry.value, null, null] : entry.valueType === "integer" ? [null, entry.value, null] : [null, null, entry.value];
1772
+ statements.push(this.database.prepare(
1773
+ `INSERT INTO mv_index_entries
1774
+ (service_id, view_id, generation, index_id, value_type, text_value, integer_value, real_value, row_key)
1775
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1776
+ ON CONFLICT (service_id, view_id, generation, index_id, value_type, text_value, integer_value, real_value, row_key)
1777
+ DO NOTHING`
1778
+ ).bind(
1779
+ input.serviceId,
1780
+ input.viewId,
1781
+ input.generation,
1782
+ entry.indexId,
1783
+ entry.valueType,
1784
+ ...typed,
1785
+ entry.rowKey
1786
+ ));
1787
+ }
1788
+ for (const patch of input.mutations.rowPatches) {
1789
+ for (const entry of patch.indexEntries) {
1790
+ const typed = entry.valueType === "text" ? [entry.value, null, null] : entry.valueType === "integer" ? [null, entry.value, null] : [null, null, entry.value];
1791
+ statements.push(this.database.prepare(
1792
+ `INSERT INTO mv_index_entries
1793
+ (service_id, view_id, generation, index_id, value_type, text_value, integer_value, real_value, row_key)
1794
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1795
+ ON CONFLICT (service_id, view_id, generation, index_id, value_type, text_value, integer_value, real_value, row_key)
1796
+ DO NOTHING`
1797
+ ).bind(
1798
+ input.serviceId,
1799
+ input.viewId,
1800
+ input.generation,
1801
+ entry.indexId,
1802
+ entry.valueType,
1803
+ ...typed,
1804
+ entry.rowKey
1805
+ ));
1806
+ }
1807
+ }
1808
+ statements.push(this.database.prepare(
1809
+ `UPDATE mv_instances
1810
+ SET last_suid = ?, definition_version = ?, updated_at = MAX(updated_at, ?),
1811
+ rebuild_verified_at = NULL,
1812
+ rebuild_recovery_id = NULL,
1813
+ rebuild_incident_identity = NULL,
1814
+ rebuild_incident_generation = NULL,
1815
+ rebuild_source_event_count = NULL,
1816
+ rebuild_source_max_suid = NULL,
1817
+ rebuild_source_history_digest = NULL
1818
+ WHERE service_id = ? AND view_id = ? AND generation = ?`
1819
+ ).bind(
1820
+ input.lastSuid,
1821
+ input.definitionVersion,
1822
+ input.updatedAt,
1823
+ input.serviceId,
1824
+ input.viewId,
1825
+ input.generation
1826
+ ));
1827
+ statements.push(this.database.prepare(
1828
+ "DELETE FROM mv_atomic_guards WHERE operation_id = ? OR operation_id LIKE ?"
1829
+ ).bind(operation, `${operation}:%`));
1830
+ try {
1831
+ await this.database.batch(statements);
1832
+ } catch (error) {
1833
+ if (isPatchFailure(error)) throw new MaterializedViewPatchError();
1834
+ if (isCasFailure(error)) throw new MaterializedViewCasError();
1835
+ throw new MaterializedViewStoreError("apply", "MV_STORE_OPERATION_FAILED", `Materialized-view atomic apply failed: ${String(error)}`, { cause: error });
1836
+ }
1837
+ const instance = await this.readInstance(input.serviceId, input.viewId, input.generation);
1838
+ if (instance === void 0) throw new MaterializedViewStoreError("apply", "MV_INSTANCE_MISSING", "Materialized-view instance disappeared after apply");
1839
+ return {
1840
+ instance,
1841
+ rowUpserts: input.mutations.rowUpserts.length,
1842
+ rowDeletes: input.mutations.rowDeletes.length,
1843
+ rowPatches: input.mutations.rowPatches.length,
1844
+ indexEntries: input.mutations.indexEntries.length
1845
+ };
1846
+ }
1847
+ async promoteGeneration(input) {
1848
+ this.ready("promote");
1849
+ this.validateGeneration(input.candidateGeneration, "promote");
1850
+ const operation = operationId2();
1851
+ const statements = [
1852
+ this.database.prepare(
1853
+ `INSERT INTO mv_atomic_guards (operation_id, checkpoint_match)
1854
+ SELECT ?, CASE WHEN EXISTS (
1855
+ SELECT 1 FROM mv_instances candidate
1856
+ WHERE candidate.service_id = ? AND candidate.view_id = ?
1857
+ AND candidate.generation = ? AND candidate.status = 'candidate'
1858
+ AND (
1859
+ NOT EXISTS (
1860
+ SELECT 1 FROM mv_ordering_quarantines open_quarantine
1861
+ WHERE open_quarantine.service_id = candidate.service_id
1862
+ AND open_quarantine.view_id = candidate.view_id
1863
+ AND open_quarantine.status = 'open'
1864
+ )
1865
+ OR EXISTS (
1866
+ SELECT 1 FROM mv_ordering_quarantines open_quarantine
1867
+ WHERE open_quarantine.service_id = candidate.service_id
1868
+ AND open_quarantine.view_id = candidate.view_id
1869
+ AND open_quarantine.status = 'open'
1870
+ AND open_quarantine.incident_identity IS NOT NULL
1871
+ AND candidate.rebuild_verified_at IS NOT NULL
1872
+ AND candidate.rebuild_incident_generation = open_quarantine.generation
1873
+ AND candidate.rebuild_incident_identity = open_quarantine.incident_identity
1874
+ )
1875
+ )
1876
+ ) AND (
1877
+ (? IS NULL AND NOT EXISTS (
1878
+ SELECT 1 FROM mv_active_generations existing
1879
+ WHERE existing.service_id = ? AND existing.view_id = ?
1880
+ ))
1881
+ OR (? IS NOT NULL AND EXISTS (
1882
+ SELECT 1 FROM mv_active_generations pointer
1883
+ WHERE pointer.service_id = ? AND pointer.view_id = ?
1884
+ AND pointer.generation = ?
1885
+ ))
1886
+ ) THEN 1 ELSE NULL END`
1887
+ ).bind(
1888
+ operation,
1889
+ input.serviceId,
1890
+ input.viewId,
1891
+ input.candidateGeneration,
1892
+ input.expectedActiveGeneration,
1893
+ input.serviceId,
1894
+ input.viewId,
1895
+ input.expectedActiveGeneration,
1896
+ input.serviceId,
1897
+ input.viewId,
1898
+ input.expectedActiveGeneration
1899
+ ),
1900
+ this.database.prepare(
1901
+ `UPDATE mv_instances SET status = 'retired', updated_at = ?
1902
+ WHERE service_id = ? AND view_id = ? AND status = 'active'`
1903
+ ).bind(input.updatedAt, input.serviceId, input.viewId),
1904
+ this.database.prepare(
1905
+ `UPDATE mv_instances SET status = 'active', updated_at = ?
1906
+ WHERE service_id = ? AND view_id = ? AND generation = ?`
1907
+ ).bind(input.updatedAt, input.serviceId, input.viewId, input.candidateGeneration),
1908
+ this.database.prepare(
1909
+ `INSERT INTO mv_active_generations (service_id, view_id, generation, updated_at)
1910
+ VALUES (?, ?, ?, ?)
1911
+ ON CONFLICT (service_id, view_id) DO UPDATE SET generation = excluded.generation, updated_at = excluded.updated_at`
1912
+ ).bind(input.serviceId, input.viewId, input.candidateGeneration, input.updatedAt),
1913
+ // A successful rebuild/promotion is the explicit recovery action for an
1914
+ // open ordering quarantine. The active-generation pointer changes in the
1915
+ // same batch, so a safe read never observes a cleared gate on the old
1916
+ // generation with the new generation still unproven.
1917
+ this.database.prepare(
1918
+ `UPDATE mv_ordering_quarantines
1919
+ SET status = 'resolved', resolved_at = ?
1920
+ WHERE service_id = ? AND view_id = ? AND status = 'open' AND generation <> ?
1921
+ AND incident_identity = (
1922
+ SELECT candidate.rebuild_incident_identity
1923
+ FROM mv_instances candidate
1924
+ WHERE candidate.service_id = ? AND candidate.view_id = ?
1925
+ AND candidate.generation = ? AND candidate.status = 'active'
1926
+ )`
1927
+ ).bind(
1928
+ input.updatedAt,
1929
+ input.serviceId,
1930
+ input.viewId,
1931
+ input.candidateGeneration,
1932
+ input.serviceId,
1933
+ input.viewId,
1934
+ input.candidateGeneration
1935
+ ),
1936
+ this.database.prepare("DELETE FROM mv_atomic_guards WHERE operation_id = ?").bind(operation)
1937
+ ];
1938
+ try {
1939
+ await this.database.batch(statements);
1940
+ } catch (error) {
1941
+ if (isCasFailure(error)) throw new MaterializedViewPromotionCasError();
1942
+ throw new MaterializedViewStoreError("promote", "MV_STORE_OPERATION_FAILED", `Materialized-view promotion failed: ${String(error)}`, { cause: error });
1943
+ }
1944
+ const active = await this.readActive(input.serviceId, input.viewId);
1945
+ if (active === void 0) throw new MaterializedViewStoreError("promote", "MV_INSTANCE_MISSING", "Active generation disappeared after promotion");
1946
+ return active;
1947
+ }
1948
+ /** Short aliases make the port convenient for runtime composition and tests. */
1949
+ apply = this.applyMutationsAndAdvanceCheckpoint.bind(this);
1950
+ promote = this.promoteGeneration.bind(this);
1951
+ rowFrom(row) {
1952
+ let value;
1953
+ try {
1954
+ value = assertJsonValue(JSON.parse(asString(row.value_json, "value_json")), "state-persistence");
1955
+ } catch (error) {
1956
+ throw new MaterializedViewStoreError("initialize", "MV_VALUE_INVALID", `Materialized-view row JSON was invalid: ${String(error)}`, { cause: error });
1957
+ }
1958
+ return {
1959
+ serviceId: asString(row.service_id, "service_id"),
1960
+ viewId: asString(row.view_id, "view_id"),
1961
+ generation: asInteger(row.generation, "generation"),
1962
+ rowKey: asString(row.row_key, "row_key"),
1963
+ value,
1964
+ rowVersion: asInteger(row.row_version, "row_version"),
1965
+ sourceSuid: asString(row.source_suid, "source_suid")
1966
+ };
1967
+ }
1968
+ indexFrom(row) {
1969
+ const type = valueType(row.value_type);
1970
+ return {
1971
+ serviceId: asString(row.service_id, "service_id"),
1972
+ viewId: asString(row.view_id, "view_id"),
1973
+ generation: asInteger(row.generation, "generation"),
1974
+ indexId: asString(row.index_id, "index_id"),
1975
+ valueType: type,
1976
+ value: indexValue(row, type),
1977
+ rowKey: asString(row.row_key, "row_key")
1978
+ };
1979
+ }
1980
+ validateGeneration(generation, operation) {
1981
+ if (!Number.isSafeInteger(generation) || generation < 0) {
1982
+ throw new MaterializedViewStoreError(operation, "MV_GENERATION_INVALID", "Materialized-view generation must be a non-negative integer");
1983
+ }
1984
+ }
1985
+ ready(operation) {
1986
+ if (!this.initialized) throw new MaterializedViewStoreError(operation, "MV_STORE_NOT_INITIALIZED", "D1MaterializedViewStore.initialize() must complete before use");
1987
+ }
1988
+ };
1989
+
1990
+ // src/mv/MaterializedViewCatchUp.ts
1991
+ var MAX_CAS_RETRIES = 8;
1992
+ var MaterializedViewCatchUpRuntime = class {
1993
+ constructor(source, materializedViews) {
1994
+ this.source = source;
1995
+ this.materializedViews = materializedViews;
1996
+ }
1997
+ source;
1998
+ materializedViews;
1999
+ async build(serviceId, materializer, nowMs, hooks = {}, options = {}) {
2000
+ const existing = await this.materializedViews.readActive(serviceId, materializer.id);
2001
+ if (existing !== void 0) {
2002
+ throw new MaterializedViewStoreError("create-active", "MV_INSTANCE_EXISTS", "Active materialized view already exists");
2003
+ }
2004
+ await this.materializedViews.createActive({
2005
+ serviceId,
2006
+ viewId: materializer.id,
2007
+ generation: 0,
2008
+ definitionVersion: materializer.version,
2009
+ updatedAt: nowMs
2010
+ });
2011
+ return this.follow(serviceId, materializer, nowMs, hooks, options);
2012
+ }
2013
+ async follow(serviceId, materializer, nowMs, hooks = {}, options = {}) {
2014
+ const active = await this.materializedViews.readActive(serviceId, materializer.id);
2015
+ if (active === void 0) {
2016
+ throw new MaterializedViewStoreError("apply", "MV_INSTANCE_MISSING", "Materialized-view must be built before follow");
2017
+ }
2018
+ return this.followGeneration(serviceId, materializer, active.generation, nowMs, "apply", hooks, options);
2019
+ }
2020
+ async rebuild(serviceId, materializer, nowMs, rebuildId, hooks = {}, options = {}) {
2021
+ const effectiveRebuildId = rebuildId ?? `g69-rebuild-${crypto.randomUUID()}`;
2022
+ const sourceBefore = await this.source.readAllEvents(serviceId, "");
2023
+ const sourceBeforeProof = await sourceHistoryProof(sourceBefore);
2024
+ const candidate = await this.materializedViews.beginRebuild({
2025
+ serviceId,
2026
+ viewId: materializer.id,
2027
+ definitionVersion: materializer.version,
2028
+ updatedAt: nowMs
2029
+ });
2030
+ const result = await this.followGeneration(serviceId, materializer, candidate.generation, nowMs, "apply", hooks, options);
2031
+ const sourceAfter = await this.source.readAllEvents(serviceId, "");
2032
+ const sourceAfterProof = await sourceHistoryProof(sourceAfter);
2033
+ if (result.stopReason === null && !result.indeterminate && result.advancedSourceEvents === sourceBefore.length && sourceBeforeProof.digest === sourceAfterProof.digest) {
2034
+ await this.materializedViews.markGenerationRebuilt({
2035
+ serviceId,
2036
+ viewId: materializer.id,
2037
+ generation: candidate.generation,
2038
+ verifiedAt: nowMs,
2039
+ rebuildId: effectiveRebuildId,
2040
+ sourceEventCount: sourceBeforeProof.eventIds.length,
2041
+ sourceEventIds: sourceBeforeProof.eventIds,
2042
+ sourceSuids: sourceBeforeProof.suids,
2043
+ sourceMaxSuid: sourceBeforeProof.maxSuid,
2044
+ sourceHistoryDigest: sourceBeforeProof.digest
2045
+ });
2046
+ }
2047
+ return { ...result, candidateGeneration: candidate.generation, rebuildId: effectiveRebuildId };
2048
+ }
2049
+ async promote(serviceId, materializer, candidateGeneration, nowMs) {
2050
+ const active = await this.materializedViews.readActive(serviceId, materializer.id);
2051
+ return this.materializedViews.promoteGeneration({
2052
+ serviceId,
2053
+ viewId: materializer.id,
2054
+ candidateGeneration,
2055
+ expectedActiveGeneration: active?.generation ?? null,
2056
+ updatedAt: nowMs
2057
+ });
2058
+ }
2059
+ async read(serviceId, materializer) {
2060
+ const instance = await this.materializedViews.readActive(serviceId, materializer.id);
2061
+ if (instance === void 0) return void 0;
2062
+ return { instance, rows: await this.materializedViews.readRows(serviceId, materializer.id, instance.generation) };
2063
+ }
2064
+ async followGeneration(serviceId, materializer, generation, nowMs, _operation, hooks, options) {
2065
+ for (let attempt = 0; attempt < MAX_CAS_RETRIES; attempt += 1) {
2066
+ const instance = await this.materializedViews.readInstance(serviceId, materializer.id, generation);
2067
+ if (instance === void 0) {
2068
+ throw new MaterializedViewStoreError("apply", "MV_INSTANCE_MISSING", "Materialized-view generation is missing");
2069
+ }
2070
+ const sourceForHead = await this.source.readAllEvents(serviceId, "");
2071
+ for (const event of sourceForHead) assertSortableUniqueId(event.suid);
2072
+ const storeMaxSuid = sourceForHead.reduce(
2073
+ (maximum, event) => compareSuid4(maximum, event.suid) >= 0 ? maximum : event.suid,
2074
+ ""
2075
+ );
2076
+ if (instance.lastSuid.length > 0 && compareSuid4(storeMaxSuid, instance.lastSuid) < 0) {
2077
+ await this.materializedViews.recordCheckpointAhead({
2078
+ serviceId,
2079
+ viewId: materializer.id,
2080
+ generation,
2081
+ checkpointSuid: instance.lastSuid,
2082
+ storeMaxSuid,
2083
+ observedAt: nowMs
2084
+ });
2085
+ throw new MaterializedViewStoreError(
2086
+ "apply",
2087
+ "MV_STORE_OPERATION_FAILED",
2088
+ "CHECKPOINT_AHEAD: materialized-view checkpoint is ahead of the source store; rebuild and promote a generation"
2089
+ );
2090
+ }
2091
+ const dynamicLagBoundMs = await this.source.currentLagBound(serviceId, nowMs);
2092
+ const windowMs = safeWindowMs(dynamicLagBoundMs);
2093
+ if (safeWindowCeilingExceeded(dynamicLagBoundMs)) {
2094
+ return {
2095
+ instance,
2096
+ dynamicLagBoundMs,
2097
+ safeWindowMs: windowMs,
2098
+ advancedSourceEvents: 0,
2099
+ appliedEvents: 0,
2100
+ lateLowerQueryDurationMs: 0,
2101
+ indeterminate: true,
2102
+ deferredEventSuid: null,
2103
+ deferredEventLastArrivedAt: null,
2104
+ deferredDeadlineAt: null,
2105
+ stopReason: "safe_window_ceiling"
2106
+ };
2107
+ }
2108
+ const sourceEvents = await this.source.readAllEvents(serviceId, instance.lastSuid);
2109
+ await this.assertSourceBatchOrder(
2110
+ serviceId,
2111
+ materializer.id,
2112
+ generation,
2113
+ instance.lastSuid,
2114
+ sourceEvents
2115
+ );
2116
+ let lateLowerQueryDurationMs = 0;
2117
+ if (options.runOrderingDetector === true) {
2118
+ lateLowerQueryDurationMs = await this.detectLateLowerSuid(
2119
+ serviceId,
2120
+ materializer.id,
2121
+ generation,
2122
+ instance.lastSuid,
2123
+ instance.updatedAt
2124
+ );
2125
+ }
2126
+ let current = instance;
2127
+ let advancedSourceEvents = 0;
2128
+ let appliedEvents = 0;
2129
+ let conflicted = false;
2130
+ for (const event of sourceEvents) {
2131
+ if (options.maximumSuid === null || options.maximumSuid !== void 0 && compareSuid4(event.suid, options.maximumSuid) > 0) {
2132
+ return {
2133
+ instance: current,
2134
+ dynamicLagBoundMs,
2135
+ safeWindowMs: windowMs,
2136
+ advancedSourceEvents,
2137
+ appliedEvents,
2138
+ lateLowerQueryDurationMs,
2139
+ indeterminate: false,
2140
+ deferredEventSuid: event.suid,
2141
+ deferredEventLastArrivedAt: event.lastArrivedAt,
2142
+ deferredDeadlineAt: null,
2143
+ stopReason: "frontier_fence"
2144
+ };
2145
+ }
2146
+ if (event.lastArrivedAt > nowMs - windowMs) {
2147
+ return {
2148
+ instance: current,
2149
+ dynamicLagBoundMs,
2150
+ safeWindowMs: windowMs,
2151
+ advancedSourceEvents,
2152
+ appliedEvents,
2153
+ lateLowerQueryDurationMs,
2154
+ indeterminate: false,
2155
+ deferredEventSuid: event.suid,
2156
+ deferredEventLastArrivedAt: event.lastArrivedAt,
2157
+ deferredDeadlineAt: event.lastArrivedAt + windowMs,
2158
+ stopReason: "safe_window_fence"
2159
+ };
2160
+ }
2161
+ await hooks.beforeApply?.(event);
2162
+ const mutations = materializer.plan(event);
2163
+ let result;
2164
+ try {
2165
+ result = await this.materializedViews.applyMutationsAndAdvanceCheckpoint({
2166
+ serviceId,
2167
+ viewId: materializer.id,
2168
+ generation,
2169
+ // The durable schema represents an unadvanced checkpoint as an
2170
+ // empty string, while the atomic apply port reserves `null` for
2171
+ // that state so all non-null ingress values are full G32 SUIDs.
2172
+ expectedLastSuid: current.lastSuid.length === 0 ? null : current.lastSuid,
2173
+ lastSuid: event.suid,
2174
+ definitionVersion: materializer.version,
2175
+ updatedAt: nowMs,
2176
+ mutations
2177
+ });
2178
+ } catch (error) {
2179
+ if (error instanceof MaterializedViewCasError) {
2180
+ conflicted = true;
2181
+ break;
2182
+ }
2183
+ throw error;
2184
+ }
2185
+ current = result.instance;
2186
+ advancedSourceEvents += 1;
2187
+ appliedEvents += 1;
2188
+ const unsafe = this.materializedViews.unsafeWindow();
2189
+ if (await unsafe.hasTargetReceipt(serviceId, materializer.id, event.eventId, event.suid)) {
2190
+ await unsafe.observeSafeReceipt(serviceId, materializer.id, generation, event.eventId, event.suid);
2191
+ }
2192
+ await hooks.afterApply?.(event, result);
2193
+ }
2194
+ if (!conflicted) {
2195
+ await this.materializedViews.collectUnsafeGarbage(
2196
+ serviceId,
2197
+ materializer.id,
2198
+ generation,
2199
+ materializer.version,
2200
+ current.lastSuid
2201
+ );
2202
+ return {
2203
+ instance: current,
2204
+ dynamicLagBoundMs,
2205
+ safeWindowMs: windowMs,
2206
+ advancedSourceEvents,
2207
+ appliedEvents,
2208
+ lateLowerQueryDurationMs,
2209
+ indeterminate: false,
2210
+ deferredEventSuid: null,
2211
+ deferredEventLastArrivedAt: null,
2212
+ deferredDeadlineAt: null,
2213
+ stopReason: null
2214
+ };
2215
+ }
2216
+ }
2217
+ throw new MaterializedViewStoreError("apply", "MV_CAS_MISMATCH", "Materialized-view catch-up did not converge after concurrent updates");
2218
+ }
2219
+ async detectLateLowerSuid(serviceId, viewId, generation, priorSuid, checkpointUpdatedAt) {
2220
+ let lateLowerQueryDurationMs = 0;
2221
+ if (priorSuid.length > 0) {
2222
+ const detectorStartedAt = Date.now();
2223
+ const evidence = this.source.findLateLowerSuidEvidence === void 0 ? void 0 : await this.source.findLateLowerSuidEvidence(serviceId, priorSuid, checkpointUpdatedAt, generation);
2224
+ const lateLower = evidence === void 0 ? await this.source.findLateLowerSuid?.(serviceId, priorSuid, checkpointUpdatedAt) : evidence.kind === "late-lower-suid" ? evidence.event : void 0;
2225
+ lateLowerQueryDurationMs = evidence?.durationMs ?? Math.max(0, Date.now() - detectorStartedAt);
2226
+ if (evidence?.kind === "unknown") {
2227
+ const incident = {
2228
+ serviceId,
2229
+ identityKey: `ORDERING_DETECTOR_UNKNOWN|${serviceId}|${viewId}|${generation}|${priorSuid}|${checkpointUpdatedAt}`,
2230
+ classification: "ORDERING_DETECTOR_UNKNOWN",
2231
+ suid: priorSuid,
2232
+ observedAt: checkpointUpdatedAt
2233
+ };
2234
+ await this.source.appendDeliveryIncident(incident);
2235
+ console.error("SDT-G69_ORDERING_DETECTOR_UNKNOWN", JSON.stringify({
2236
+ ...incident,
2237
+ reason: evidence.reason ?? "unknown"
2238
+ }));
2239
+ }
2240
+ if (lateLower !== void 0) {
2241
+ const incident = {
2242
+ serviceId,
2243
+ identityKey: `LATE_LOWER_SUID|${serviceId}|${viewId}|${priorSuid}|${lateLower.suid}|${lateLower.eventId}`,
2244
+ classification: "ORDER_VIOLATION",
2245
+ suid: lateLower.suid,
2246
+ eventId: lateLower.eventId,
2247
+ incomingEventId: lateLower.eventId,
2248
+ observedAt: lateLower.lastArrivedAt
2249
+ };
2250
+ await this.source.appendDeliveryIncident(incident);
2251
+ await this.materializedViews.recordOrderingQuarantine({
2252
+ serviceId,
2253
+ viewId,
2254
+ generation,
2255
+ checkpointSuid: priorSuid,
2256
+ lateSuid: lateLower.suid,
2257
+ eventId: lateLower.eventId,
2258
+ classification: "LATE_LOWER_SUID",
2259
+ observedAt: lateLower.lastArrivedAt
2260
+ });
2261
+ console.error("SDT-G69_ORDERING_QUARANTINE", JSON.stringify({
2262
+ serviceId,
2263
+ viewId,
2264
+ generation,
2265
+ checkpointSuid: priorSuid,
2266
+ lateSuid: lateLower.suid,
2267
+ eventId: lateLower.eventId,
2268
+ classification: "LATE_LOWER_SUID"
2269
+ }));
2270
+ throw new MaterializedViewStoreError(
2271
+ "apply",
2272
+ "MV_ORDERING_QUARANTINED",
2273
+ "Materialized-view safe lane is quarantined for an ordering violation; rebuild and promote a generation"
2274
+ );
2275
+ }
2276
+ }
2277
+ return lateLowerQueryDurationMs;
2278
+ }
2279
+ async assertSourceBatchOrder(serviceId, viewId, generation, priorSuid, events) {
2280
+ let previous = priorSuid;
2281
+ for (const event of events) {
2282
+ if (compareSuid4(previous, event.suid) >= 0) {
2283
+ const incident = {
2284
+ serviceId,
2285
+ identityKey: `ORDER_VIOLATION|${serviceId}|${viewId}|${previous}|${event.suid}|${event.eventId}`,
2286
+ classification: "ORDER_VIOLATION",
2287
+ suid: event.suid,
2288
+ eventId: event.eventId,
2289
+ incomingEventId: event.eventId,
2290
+ observedAt: event.lastArrivedAt
2291
+ };
2292
+ await this.source.appendDeliveryIncident(incident);
2293
+ await this.materializedViews.recordOrderingQuarantine({
2294
+ serviceId,
2295
+ viewId,
2296
+ generation,
2297
+ checkpointSuid: previous,
2298
+ lateSuid: event.suid,
2299
+ eventId: event.eventId,
2300
+ classification: "ORDER_VIOLATION",
2301
+ observedAt: event.lastArrivedAt
2302
+ });
2303
+ console.error("SDT-G69_ORDERING_QUARANTINE", JSON.stringify({
2304
+ serviceId,
2305
+ viewId,
2306
+ generation,
2307
+ checkpointSuid: previous,
2308
+ lateSuid: event.suid,
2309
+ eventId: event.eventId,
2310
+ classification: "ORDER_VIOLATION"
2311
+ }));
2312
+ throw new MaterializedViewStoreError(
2313
+ "apply",
2314
+ "MV_ORDERING_QUARANTINED",
2315
+ "Materialized-view safe lane is quarantined for an ordering violation; rebuild and promote a generation"
2316
+ );
2317
+ }
2318
+ previous = event.suid;
2319
+ }
2320
+ }
2321
+ };
2322
+ async function sourceHistoryProof(events) {
2323
+ const eventIds = events.map((event) => event.eventId);
2324
+ const suids = events.map((event) => event.suid);
2325
+ const maxSuid = events.reduce(
2326
+ (maximum, event) => maximum === "" || compareSuid4(event.suid, maximum) > 0 ? event.suid : maximum,
2327
+ ""
2328
+ );
2329
+ const canonical = events.map((event) => `${event.eventId}\0${event.suid}`).join("\n");
2330
+ const digestBytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical));
2331
+ const digest = [...new Uint8Array(digestBytes)].map((value) => value.toString(16).padStart(2, "0")).join("");
2332
+ return { eventIds, suids, maxSuid, digest };
2333
+ }
2334
+ function compareSuid4(left, right) {
2335
+ const leftBytes = new TextEncoder().encode(left);
2336
+ const rightBytes = new TextEncoder().encode(right);
2337
+ const shared = Math.min(leftBytes.length, rightBytes.length);
2338
+ for (let index = 0; index < shared; index += 1) {
2339
+ const difference = leftBytes[index] - rightBytes[index];
2340
+ if (difference !== 0) return difference;
2341
+ }
2342
+ return leftBytes.length - rightBytes.length;
2343
+ }
2344
+ export {
2345
+ D1MaterializedViewStore,
2346
+ MaterializedViewCasError,
2347
+ MaterializedViewCatchUpRuntime,
2348
+ MaterializedViewOperationError,
2349
+ MaterializedViewPatchError,
2350
+ MaterializedViewPromotionCasError,
2351
+ MaterializedViewRuntime,
2352
+ MaterializedViewStoreError,
2353
+ UnsafeWindowMaterializedViewError,
2354
+ UnsafeWindowMaterializedViewStore,
2355
+ materializedViewCandidateId,
2356
+ materializedViewId
2357
+ };