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