@sekiban/dcb-runtime 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +86 -0
- package/README.md +13 -0
- package/dist/allocator/AllocatorDurableObject.d.ts +36 -0
- package/dist/allocator/AllocatorDurableObject.js +658 -0
- package/dist/allocator/IssuanceLedger.d.ts +188 -0
- package/dist/allocator/IssuanceLedger.js +608 -0
- package/dist/allocator/IssuanceReconciler.d.ts +41 -0
- package/dist/allocator/IssuanceReconciler.js +191 -0
- package/dist/allocator/OrderClock.d.ts +41 -0
- package/dist/allocator/OrderClock.js +92 -0
- package/dist/allocator/SortableUniqueId.d.ts +46 -0
- package/dist/allocator/SortableUniqueId.js +156 -0
- package/dist/allocator/types.d.ts +52 -0
- package/dist/allocator/types.js +1 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +28 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +357 -0
- package/dist/bootstrap/BootstrapStoreAdapter.d.ts +51 -0
- package/dist/bootstrap/BootstrapStoreAdapter.js +162 -0
- package/dist/bootstrap/OperatorBootstrap.d.ts +20 -0
- package/dist/bootstrap/OperatorBootstrap.js +104 -0
- package/dist/bootstrap/manifest.d.ts +9 -0
- package/dist/bootstrap/manifest.js +115 -0
- package/dist/bootstrap/types.d.ts +75 -0
- package/dist/bootstrap/types.js +1 -0
- package/dist/cli/OperatorRepairCli.d.ts +11 -0
- package/dist/cli/OperatorRepairCli.js +108 -0
- package/dist/cloudflare.d.ts +164 -0
- package/dist/cloudflare.js +19487 -0
- package/dist/commit/CommitWorker.d.ts +101 -0
- package/dist/commit/CommitWorker.js +921 -0
- package/dist/commit/types.d.ts +25 -0
- package/dist/commit/types.js +13 -0
- package/dist/completeness/GlobalCompletenessReconciler.d.ts +77 -0
- package/dist/completeness/GlobalCompletenessReconciler.js +572 -0
- package/dist/completeness/types.d.ts +85 -0
- package/dist/completeness/types.js +6 -0
- package/dist/composition.d.ts +123 -0
- package/dist/composition.js +237 -0
- package/dist/cosmos.d.ts +11 -0
- package/dist/cosmos.js +1411 -0
- package/dist/d1-mv.d.ts +8 -0
- package/dist/d1-mv.js +2001 -0
- package/dist/d1.d.ts +16 -0
- package/dist/d1.js +3418 -0
- package/dist/diagnostics/G60DurableHop.d.ts +99 -0
- package/dist/diagnostics/G60DurableHop.js +248 -0
- package/dist/diagnostics/G65Admission.d.ts +23 -0
- package/dist/diagnostics/G65Admission.js +43 -0
- package/dist/diagnostics/G65DirectRing.d.ts +33 -0
- package/dist/diagnostics/G65DirectRing.js +85 -0
- package/dist/diagnostics/G69AdmissionAttempt.d.ts +43 -0
- package/dist/diagnostics/G69AdmissionAttempt.js +22 -0
- package/dist/downstream/DeliveryCore.d.ts +127 -0
- package/dist/downstream/DeliveryCore.js +304 -0
- package/dist/downstream/Doorbell.d.ts +43 -0
- package/dist/downstream/Doorbell.js +137 -0
- package/dist/downstream/DownstreamAdapter.d.ts +52 -0
- package/dist/downstream/DownstreamAdapter.js +299 -0
- package/dist/downstream/ExclusionLookup.d.ts +36 -0
- package/dist/downstream/ExclusionLookup.js +50 -0
- package/dist/downstream/InconsistencyDetector.d.ts +27 -0
- package/dist/downstream/InconsistencyDetector.js +82 -0
- package/dist/downstream/OutboxDrain.d.ts +35 -0
- package/dist/downstream/OutboxDrain.js +126 -0
- package/dist/downstream/types.d.ts +77 -0
- package/dist/downstream/types.js +64 -0
- package/dist/eventIdentity.d.ts +30 -0
- package/dist/eventIdentity.js +49 -0
- package/dist/eventRecord.d.ts +26 -0
- package/dist/eventRecord.js +53 -0
- package/dist/g42-probe.d.ts +9 -0
- package/dist/g42-probe.js +9 -0
- package/dist/http/SerializedQueryWorker.d.ts +58 -0
- package/dist/http/SerializedQueryWorker.js +497 -0
- package/dist/http/commitResponse.d.ts +46 -0
- package/dist/http/commitResponse.js +80 -0
- package/dist/http/testServiceId.d.ts +17 -0
- package/dist/http/testServiceId.js +14 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +19926 -0
- package/dist/journal/JournalDurableObject.d.ts +30 -0
- package/dist/journal/JournalDurableObject.js +481 -0
- package/dist/journal/JournalFirstTouchProbe.d.ts +190 -0
- package/dist/journal/JournalFirstTouchProbe.js +375 -0
- package/dist/journal/types.d.ts +116 -0
- package/dist/journal/types.js +52 -0
- package/dist/mv/MaterializedViewCatchUp.d.ts +60 -0
- package/dist/mv/MaterializedViewCatchUp.js +353 -0
- package/dist/mv/MaterializedViewRuntime.d.ts +80 -0
- package/dist/mv/MaterializedViewRuntime.js +328 -0
- package/dist/mv/MaterializedViewStore.d.ts +257 -0
- package/dist/mv/MaterializedViewStore.js +875 -0
- package/dist/mv/UnsafeWindowMaterializedView.d.ts +112 -0
- package/dist/mv/UnsafeWindowMaterializedView.js +495 -0
- package/dist/mv/index.d.ts +8 -0
- package/dist/mv/index.js +2357 -0
- package/dist/projection/LiveProjectionWorker.d.ts +86 -0
- package/dist/projection/LiveProjectionWorker.js +240 -0
- package/dist/projection/ProjectionRuntime.d.ts +113 -0
- package/dist/projection/ProjectionRuntime.js +346 -0
- package/dist/projection/ProjectorRegistry.d.ts +51 -0
- package/dist/projection/ProjectorRegistry.js +116 -0
- package/dist/query/ProjectionQueryStore.d.ts +102 -0
- package/dist/query/ProjectionQueryStore.js +185 -0
- package/dist/query/QueryRegistry.d.ts +22 -0
- package/dist/query/QueryRegistry.js +36 -0
- package/dist/read/SerializedReadWorker.d.ts +36 -0
- package/dist/read/SerializedReadWorker.js +225 -0
- package/dist/repair/RepairWorker.d.ts +72 -0
- package/dist/repair/RepairWorker.js +461 -0
- package/dist/safeWindow.d.ts +20 -0
- package/dist/safeWindow.js +36 -0
- package/dist/scope/ControlRouteScope.d.ts +22 -0
- package/dist/scope/ControlRouteScope.js +36 -0
- package/dist/scope/ScopeName.d.ts +34 -0
- package/dist/scope/ScopeName.js +67 -0
- package/dist/service/ServiceIdentityProvider.d.ts +35 -0
- package/dist/service/ServiceIdentityProvider.js +56 -0
- package/dist/store/CosmosEventStore.d.ts +126 -0
- package/dist/store/CosmosEventStore.js +950 -0
- package/dist/store/D1EventStore.d.ts +111 -0
- package/dist/store/D1EventStore.js +1111 -0
- package/dist/store/PostgresEventStore.d.ts +35 -0
- package/dist/store/PostgresEventStore.js +811 -0
- package/dist/store/provider.d.ts +25 -0
- package/dist/store/provider.js +32 -0
- package/dist/store/types.d.ts +205 -0
- package/dist/store/types.js +18 -0
- package/dist/tag/EventDigest.d.ts +40 -0
- package/dist/tag/EventDigest.js +158 -0
- package/dist/tag/TagDurableObject.d.ts +286 -0
- package/dist/tag/TagDurableObject.js +4071 -0
- package/dist/tag/TagSqlMeasurement.d.ts +26 -0
- package/dist/tag/TagSqlMeasurement.js +44 -0
- package/dist/tag/TagSqlSchema.d.ts +29 -0
- package/dist/tag/TagSqlSchema.js +211 -0
- package/dist/tag/types.d.ts +173 -0
- package/dist/tag/types.js +3 -0
- package/dist/tagstate/TagStateDurableObject.d.ts +57 -0
- package/dist/tagstate/TagStateDurableObject.js +499 -0
- package/dist/trace/AttributionRatio.d.ts +12 -0
- package/dist/trace/AttributionRatio.js +68 -0
- package/dist/trace/CloudflareTracing.d.ts +7 -0
- package/dist/trace/CloudflareTracing.js +9 -0
- package/dist/trace/CommitTrace.d.ts +405 -0
- package/dist/trace/CommitTrace.js +1008 -0
- package/dist/trace/CommitTraceConsoleSink.d.ts +68 -0
- package/dist/trace/CommitTraceConsoleSink.js +114 -0
- package/dist/trace/CommitTraceVerifier.d.ts +22 -0
- package/dist/trace/CommitTraceVerifier.js +295 -0
- package/dist/trace/ObservationStream.d.ts +118 -0
- package/dist/trace/ObservationStream.js +148 -0
- package/package.json +71 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { safeWindowCeilingExceeded, safeWindowMs } from "../projection/ProjectionRuntime";
|
|
2
|
+
import { isSortableUniqueIdSafeAt } from "../safeWindow";
|
|
3
|
+
import { assertSortableUniqueId } from "../allocator/SortableUniqueId";
|
|
4
|
+
const MAX_CHECKPOINT_CAS_RETRIES = 8;
|
|
5
|
+
/** A failed change operation is never represented as a successful no-op. */
|
|
6
|
+
export class MaterializedViewOperationError extends Error {
|
|
7
|
+
operation;
|
|
8
|
+
code;
|
|
9
|
+
constructor(operation, code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.operation = operation;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = "MaterializedViewOperationError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function compareSuid(left, right) {
|
|
17
|
+
const leftBytes = new TextEncoder().encode(left);
|
|
18
|
+
const rightBytes = new TextEncoder().encode(right);
|
|
19
|
+
const sharedLength = Math.min(leftBytes.length, rightBytes.length);
|
|
20
|
+
for (let index = 0; index < sharedLength; index += 1) {
|
|
21
|
+
const difference = leftBytes[index] - rightBytes[index];
|
|
22
|
+
if (difference !== 0) {
|
|
23
|
+
return difference;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return leftBytes.length - rightBytes.length;
|
|
27
|
+
}
|
|
28
|
+
function nonEmpty(value) {
|
|
29
|
+
return value.length > 0;
|
|
30
|
+
}
|
|
31
|
+
/** Stable durable namespace for the promoted materialized view. */
|
|
32
|
+
export function materializedViewId(definitionId) {
|
|
33
|
+
return `mv:${definitionId}`;
|
|
34
|
+
}
|
|
35
|
+
/** Durable namespace for a rebuild candidate that has not yet been promoted. */
|
|
36
|
+
export function materializedViewCandidateId(definitionId, rebuildId) {
|
|
37
|
+
return `${materializedViewId(definitionId)}:rebuild:${rebuildId}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A materialized view consumes the same read-only global SUID source and
|
|
41
|
+
* SafeWindow as a live projection. Its state and source position share one
|
|
42
|
+
* durable checkpoint transaction, so a restart cannot double-apply a source
|
|
43
|
+
* event that has already changed state.
|
|
44
|
+
*/
|
|
45
|
+
export class MaterializedViewRuntime {
|
|
46
|
+
store;
|
|
47
|
+
constructor(store) {
|
|
48
|
+
this.store = store;
|
|
49
|
+
}
|
|
50
|
+
async read(serviceId, definition) {
|
|
51
|
+
const viewId = this.checkedViewId(definition, "follow");
|
|
52
|
+
const checkpoint = await this.store.readProjectionCheckpoint(serviceId, viewId);
|
|
53
|
+
return checkpoint === undefined ? undefined : this.snapshot(serviceId, viewId, checkpoint, definition, "follow");
|
|
54
|
+
}
|
|
55
|
+
/** Creates the initial durable state then follows through the SafeWindow. */
|
|
56
|
+
async build(serviceId, definition, nowMs, hooks = {}) {
|
|
57
|
+
try {
|
|
58
|
+
const viewId = this.checkedViewId(definition, "build");
|
|
59
|
+
await this.ensureInitialCheckpoint(serviceId, viewId, definition, nowMs, "build", true);
|
|
60
|
+
return await this.followCheckpoint(serviceId, viewId, definition, nowMs, "build", hooks);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
throw this.typedFailure("build", error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Continues the promoted durable view from its own global SUID checkpoint. */
|
|
67
|
+
async follow(serviceId, definition, nowMs, hooks = {}) {
|
|
68
|
+
try {
|
|
69
|
+
const viewId = this.checkedViewId(definition, "follow");
|
|
70
|
+
await this.ensureInitialCheckpoint(serviceId, viewId, definition, nowMs, "follow", false);
|
|
71
|
+
return await this.followCheckpoint(serviceId, viewId, definition, nowMs, "follow", hooks);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
throw this.typedFailure("follow", error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Builds a separate durable candidate from the beginning of the global
|
|
79
|
+
* source. The caller must explicitly promote it; no rebuild silently swaps
|
|
80
|
+
* production state.
|
|
81
|
+
*/
|
|
82
|
+
async rebuild(serviceId, definition, rebuildId, nowMs, hooks = {}) {
|
|
83
|
+
try {
|
|
84
|
+
const definitionId = this.checkedDefinitionId(definition, "rebuild");
|
|
85
|
+
if (!nonEmpty(rebuildId)) {
|
|
86
|
+
throw this.failure("rebuild", "MV_REBUILD_ID_INVALID", "Materialized-view rebuildId is required");
|
|
87
|
+
}
|
|
88
|
+
const candidateId = materializedViewCandidateId(definitionId, rebuildId);
|
|
89
|
+
await this.ensureInitialCheckpoint(serviceId, candidateId, definition, nowMs, "rebuild", true);
|
|
90
|
+
return {
|
|
91
|
+
candidateId,
|
|
92
|
+
result: await this.followCheckpoint(serviceId, candidateId, definition, nowMs, "rebuild", hooks),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
throw this.typedFailure("rebuild", error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Resets the promoted view to a durable initial state, never a no-op. */
|
|
100
|
+
async reset(serviceId, definition, nowMs) {
|
|
101
|
+
try {
|
|
102
|
+
const viewId = this.checkedViewId(definition, "reset");
|
|
103
|
+
const existing = await this.store.readProjectionCheckpoint(serviceId, viewId);
|
|
104
|
+
if (existing === undefined) {
|
|
105
|
+
throw this.failure("reset", "MV_RESET_MISSING", "Cannot reset a materialized view that was never built");
|
|
106
|
+
}
|
|
107
|
+
const initial = this.checkpointFor(serviceId, viewId, "", this.initialState(definition, "reset"), definition, nowMs, "reset");
|
|
108
|
+
if (existing.lastSuid === initial.lastSuid
|
|
109
|
+
&& existing.stateJson === initial.stateJson
|
|
110
|
+
&& existing.version === initial.version) {
|
|
111
|
+
throw this.failure("reset", "MV_RESET_NOTHING_TO_RESET", "Materialized view is already at its durable initial state");
|
|
112
|
+
}
|
|
113
|
+
const advanced = await this.store.advanceProjectionCheckpoint({
|
|
114
|
+
...initial,
|
|
115
|
+
expectedLastSuid: existing.lastSuid,
|
|
116
|
+
});
|
|
117
|
+
if (!advanced) {
|
|
118
|
+
throw this.failure("reset", "MV_RESET_CONFLICT", "Materialized-view checkpoint changed before reset could commit");
|
|
119
|
+
}
|
|
120
|
+
return this.snapshot(serviceId, viewId, initial, definition, "reset");
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
throw this.typedFailure("reset", error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Promotes a previously rebuilt durable candidate with a compare-and-swap. */
|
|
127
|
+
async promote(serviceId, definition, rebuildId, nowMs) {
|
|
128
|
+
try {
|
|
129
|
+
const definitionId = this.checkedDefinitionId(definition, "promote");
|
|
130
|
+
if (!nonEmpty(rebuildId)) {
|
|
131
|
+
throw this.failure("promote", "MV_REBUILD_ID_INVALID", "Materialized-view rebuildId is required");
|
|
132
|
+
}
|
|
133
|
+
const viewId = materializedViewId(definitionId);
|
|
134
|
+
const candidateId = materializedViewCandidateId(definitionId, rebuildId);
|
|
135
|
+
const candidate = await this.store.readProjectionCheckpoint(serviceId, candidateId);
|
|
136
|
+
if (candidate === undefined) {
|
|
137
|
+
throw this.failure("promote", "MV_PROMOTE_CANDIDATE_MISSING", "Materialized-view rebuild candidate does not exist");
|
|
138
|
+
}
|
|
139
|
+
const candidateSnapshot = this.snapshot(serviceId, candidateId, candidate, definition, "promote");
|
|
140
|
+
const current = await this.store.readProjectionCheckpoint(serviceId, viewId);
|
|
141
|
+
if (current !== undefined
|
|
142
|
+
&& current.lastSuid === candidate.lastSuid
|
|
143
|
+
&& current.stateJson === candidate.stateJson
|
|
144
|
+
&& current.version === candidate.version) {
|
|
145
|
+
throw this.failure("promote", "MV_PROMOTE_NOTHING_TO_PROMOTE", "Materialized-view candidate is already promoted");
|
|
146
|
+
}
|
|
147
|
+
const promoted = {
|
|
148
|
+
...candidateSnapshot.checkpoint,
|
|
149
|
+
projectionId: viewId,
|
|
150
|
+
updatedAt: nowMs,
|
|
151
|
+
};
|
|
152
|
+
const advanced = await this.store.advanceProjectionCheckpoint({
|
|
153
|
+
...promoted,
|
|
154
|
+
expectedLastSuid: current?.lastSuid ?? null,
|
|
155
|
+
});
|
|
156
|
+
if (!advanced) {
|
|
157
|
+
throw this.failure("promote", "MV_PROMOTE_CONFLICT", "Materialized-view checkpoint changed before candidate promotion could commit");
|
|
158
|
+
}
|
|
159
|
+
return this.snapshot(serviceId, viewId, promoted, definition, "promote");
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
throw this.typedFailure("promote", error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async ensureInitialCheckpoint(serviceId, viewId, definition, nowMs, operation, failIfPresent) {
|
|
166
|
+
const existing = await this.store.readProjectionCheckpoint(serviceId, viewId);
|
|
167
|
+
if (existing !== undefined) {
|
|
168
|
+
if (failIfPresent) {
|
|
169
|
+
throw this.failure(operation, operation === "build" ? "MV_BUILD_ALREADY_EXISTS" : "MV_REBUILD_CANDIDATE_EXISTS", operation === "build"
|
|
170
|
+
? "Materialized view already has durable state; use follow or rebuild"
|
|
171
|
+
: "Materialized-view rebuild candidate already exists");
|
|
172
|
+
}
|
|
173
|
+
return existing;
|
|
174
|
+
}
|
|
175
|
+
const initial = this.checkpointFor(serviceId, viewId, "", this.initialState(definition, operation), definition, nowMs, operation);
|
|
176
|
+
const advanced = await this.store.advanceProjectionCheckpoint({
|
|
177
|
+
...initial,
|
|
178
|
+
expectedLastSuid: null,
|
|
179
|
+
});
|
|
180
|
+
if (advanced) {
|
|
181
|
+
return initial;
|
|
182
|
+
}
|
|
183
|
+
if (failIfPresent) {
|
|
184
|
+
throw this.failure(operation, operation === "build" ? "MV_BUILD_CONFLICT" : "MV_REBUILD_CONFLICT", "Materialized-view checkpoint appeared before the requested durable operation could commit");
|
|
185
|
+
}
|
|
186
|
+
const raced = await this.store.readProjectionCheckpoint(serviceId, viewId);
|
|
187
|
+
if (raced === undefined) {
|
|
188
|
+
throw this.failure("follow", "MV_FOLLOW_CHECKPOINT_UNAVAILABLE", "Materialized-view checkpoint could not be established");
|
|
189
|
+
}
|
|
190
|
+
return raced;
|
|
191
|
+
}
|
|
192
|
+
async followCheckpoint(serviceId, viewId, definition, nowMs, operation, hooks) {
|
|
193
|
+
const dynamicLagBoundMs = await this.store.currentLagBound(serviceId, nowMs);
|
|
194
|
+
const windowMs = safeWindowMs(dynamicLagBoundMs);
|
|
195
|
+
for (let retry = 0; retry < MAX_CHECKPOINT_CAS_RETRIES; retry += 1) {
|
|
196
|
+
const checkpoint = await this.store.readProjectionCheckpoint(serviceId, viewId);
|
|
197
|
+
if (checkpoint === undefined) {
|
|
198
|
+
throw this.failure("follow", "MV_FOLLOW_CHECKPOINT_UNAVAILABLE", "Materialized-view checkpoint disappeared during follow");
|
|
199
|
+
}
|
|
200
|
+
if (safeWindowCeilingExceeded(dynamicLagBoundMs)) {
|
|
201
|
+
return {
|
|
202
|
+
...this.snapshot(serviceId, viewId, checkpoint, definition, operation),
|
|
203
|
+
dynamicLagBoundMs,
|
|
204
|
+
safeWindowMs: windowMs,
|
|
205
|
+
advancedSourceEvents: 0,
|
|
206
|
+
appliedEvents: 0,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
let currentCheckpoint = checkpoint;
|
|
210
|
+
let state = this.stateFrom(definition, checkpoint, operation);
|
|
211
|
+
const sourceEvents = await this.store.readAllEvents(serviceId, checkpoint.lastSuid);
|
|
212
|
+
let validatedPreviousSuid = checkpoint.lastSuid;
|
|
213
|
+
for (const event of sourceEvents) {
|
|
214
|
+
assertSortableUniqueId(event.suid);
|
|
215
|
+
if (compareSuid(validatedPreviousSuid, event.suid) >= 0) {
|
|
216
|
+
throw this.failure(operation, "MV_SOURCE_NOT_STRICTLY_SUID_ORDERED", "Materialized-view source was not strictly SUID ordered");
|
|
217
|
+
}
|
|
218
|
+
validatedPreviousSuid = event.suid;
|
|
219
|
+
}
|
|
220
|
+
let advancedSourceEvents = 0;
|
|
221
|
+
let appliedEvents = 0;
|
|
222
|
+
let casConflict = false;
|
|
223
|
+
for (const event of sourceEvents) {
|
|
224
|
+
// SafeWindow is deliberately read-only: stopping at the first unsafe
|
|
225
|
+
// SUID preserves convergence when a lower SUID is delivered late.
|
|
226
|
+
if (!isSortableUniqueIdSafeAt(event.suid, nowMs, dynamicLagBoundMs)) {
|
|
227
|
+
return {
|
|
228
|
+
...this.snapshot(serviceId, viewId, currentCheckpoint, definition, operation),
|
|
229
|
+
dynamicLagBoundMs,
|
|
230
|
+
safeWindowMs: windowMs,
|
|
231
|
+
advancedSourceEvents,
|
|
232
|
+
appliedEvents,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const nextState = this.apply(definition, state, event, operation);
|
|
236
|
+
const nextCheckpoint = this.checkpointFor(serviceId, viewId, event.suid, nextState, definition, nowMs, operation);
|
|
237
|
+
await hooks.beforeCheckpoint?.(event, nextCheckpoint);
|
|
238
|
+
const advanced = await this.store.advanceProjectionCheckpoint({
|
|
239
|
+
...nextCheckpoint,
|
|
240
|
+
expectedLastSuid: currentCheckpoint.lastSuid,
|
|
241
|
+
});
|
|
242
|
+
if (!advanced) {
|
|
243
|
+
casConflict = true;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
currentCheckpoint = nextCheckpoint;
|
|
247
|
+
state = nextState;
|
|
248
|
+
advancedSourceEvents += 1;
|
|
249
|
+
appliedEvents += 1;
|
|
250
|
+
await hooks.afterCheckpoint?.(event, nextCheckpoint);
|
|
251
|
+
}
|
|
252
|
+
if (!casConflict) {
|
|
253
|
+
return {
|
|
254
|
+
...this.snapshot(serviceId, viewId, currentCheckpoint, definition, operation),
|
|
255
|
+
dynamicLagBoundMs,
|
|
256
|
+
safeWindowMs: windowMs,
|
|
257
|
+
advancedSourceEvents,
|
|
258
|
+
appliedEvents,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
throw this.failure(operation, "MV_CHECKPOINT_CONFLICT", "Materialized-view checkpoint did not converge after concurrent updates");
|
|
263
|
+
}
|
|
264
|
+
checkedViewId(definition, operation) {
|
|
265
|
+
return materializedViewId(this.checkedDefinitionId(definition, operation));
|
|
266
|
+
}
|
|
267
|
+
checkedDefinitionId(definition, operation) {
|
|
268
|
+
if (!nonEmpty(definition.id)) {
|
|
269
|
+
throw this.failure(operation, "MV_DEFINITION_INVALID", "Materialized-view definition id is required");
|
|
270
|
+
}
|
|
271
|
+
return definition.id;
|
|
272
|
+
}
|
|
273
|
+
stateFrom(definition, checkpoint, operation) {
|
|
274
|
+
try {
|
|
275
|
+
return definition.deserializeState(checkpoint.stateJson);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view durable state was invalid: ${String(error)}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
initialState(definition, operation) {
|
|
282
|
+
try {
|
|
283
|
+
return definition.initialState();
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view initial state was invalid: ${String(error)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
apply(definition, state, event, operation) {
|
|
290
|
+
try {
|
|
291
|
+
return definition.apply(state, event);
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view reducer failed: ${String(error)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
checkpointFor(serviceId, viewId, lastSuid, state, definition, nowMs, operation) {
|
|
298
|
+
try {
|
|
299
|
+
return {
|
|
300
|
+
serviceId,
|
|
301
|
+
projectionId: viewId,
|
|
302
|
+
lastSuid,
|
|
303
|
+
stateJson: definition.serializeState(state),
|
|
304
|
+
version: definition.version(state),
|
|
305
|
+
updatedAt: nowMs,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
throw this.failure(operation, "MV_STATE_INVALID", `Materialized-view state could not be persisted: ${String(error)}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
snapshot(serviceId, viewId, checkpoint, definition, operation) {
|
|
313
|
+
return {
|
|
314
|
+
serviceId,
|
|
315
|
+
viewId,
|
|
316
|
+
checkpoint,
|
|
317
|
+
state: this.stateFrom(definition, checkpoint, operation),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
failure(operation, code, message) {
|
|
321
|
+
return new MaterializedViewOperationError(operation, code, message);
|
|
322
|
+
}
|
|
323
|
+
typedFailure(operation, error) {
|
|
324
|
+
return error instanceof MaterializedViewOperationError
|
|
325
|
+
? error
|
|
326
|
+
: this.failure(operation, "MV_OPERATION_FAILED", `Materialized-view ${operation} failed: ${String(error)}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { type JsonValue, type MaterializedViewIndexValueType, type MaterializedViewMutationPlan } from "@sekiban/dcb-core";
|
|
2
|
+
import { UnsafeWindowMaterializedViewStore, type UnsafeComposedPage } from "./UnsafeWindowMaterializedView";
|
|
3
|
+
export type MaterializedViewStoreOperation = "initialize" | "create-active" | "create-candidate" | "apply" | "promote";
|
|
4
|
+
export type MaterializedViewStoreErrorCode = "MV_STORE_NOT_INITIALIZED" | "MV_INSTANCE_EXISTS" | "MV_INSTANCE_MISSING" | "MV_CAS_MISMATCH" | "MV_PROMOTION_CAS_MISMATCH" | "MV_GENERATION_INVALID" | "MV_PATCH_ROW_MISSING" | "MV_VALUE_INVALID" | "MV_ORDERING_QUARANTINED" | "MV_REBUILD_NOT_VERIFIED" | "MV_STORE_OPERATION_FAILED";
|
|
5
|
+
export declare class MaterializedViewStoreError extends Error {
|
|
6
|
+
readonly operation: MaterializedViewStoreOperation;
|
|
7
|
+
readonly code: MaterializedViewStoreErrorCode;
|
|
8
|
+
constructor(operation: MaterializedViewStoreOperation, code: MaterializedViewStoreErrorCode, message: string, options?: ErrorOptions);
|
|
9
|
+
}
|
|
10
|
+
/** A stale MV checkpoint is a failed atomic call, never a successful no-op. */
|
|
11
|
+
export declare class MaterializedViewCasError extends MaterializedViewStoreError {
|
|
12
|
+
constructor(message?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class MaterializedViewPromotionCasError extends MaterializedViewStoreError {
|
|
15
|
+
constructor(message?: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class MaterializedViewPatchError extends MaterializedViewStoreError {
|
|
18
|
+
constructor(message?: string);
|
|
19
|
+
}
|
|
20
|
+
export interface MaterializedViewInstance {
|
|
21
|
+
readonly serviceId: string;
|
|
22
|
+
readonly viewId: string;
|
|
23
|
+
readonly generation: number;
|
|
24
|
+
readonly status: "active" | "candidate" | "retired";
|
|
25
|
+
readonly lastSuid: string;
|
|
26
|
+
readonly definitionVersion: number;
|
|
27
|
+
readonly updatedAt: number;
|
|
28
|
+
/** Non-null only after a complete, explicit rebuild proof for this generation. */
|
|
29
|
+
readonly rebuildVerifiedAt: number | null;
|
|
30
|
+
readonly rebuildRecoveryId: string | null;
|
|
31
|
+
readonly rebuildIncidentIdentity: string | null;
|
|
32
|
+
readonly rebuildIncidentGeneration: number | null;
|
|
33
|
+
readonly rebuildSourceEventCount: number | null;
|
|
34
|
+
readonly rebuildSourceMaxSuid: string | null;
|
|
35
|
+
readonly rebuildSourceHistoryDigest: string | null;
|
|
36
|
+
}
|
|
37
|
+
export interface MaterializedViewRow {
|
|
38
|
+
readonly serviceId: string;
|
|
39
|
+
readonly viewId: string;
|
|
40
|
+
readonly generation: number;
|
|
41
|
+
readonly rowKey: string;
|
|
42
|
+
readonly value: JsonValue;
|
|
43
|
+
readonly rowVersion: number;
|
|
44
|
+
readonly sourceSuid: string;
|
|
45
|
+
}
|
|
46
|
+
export interface MaterializedViewIndexEntry {
|
|
47
|
+
readonly serviceId: string;
|
|
48
|
+
readonly viewId: string;
|
|
49
|
+
readonly generation: number;
|
|
50
|
+
readonly indexId: string;
|
|
51
|
+
readonly valueType: MaterializedViewIndexValueType;
|
|
52
|
+
readonly value: string | number;
|
|
53
|
+
readonly rowKey: string;
|
|
54
|
+
}
|
|
55
|
+
export interface MaterializedViewCreateInput {
|
|
56
|
+
readonly serviceId: string;
|
|
57
|
+
readonly viewId: string;
|
|
58
|
+
readonly generation?: number;
|
|
59
|
+
readonly definitionVersion: number;
|
|
60
|
+
readonly updatedAt: number;
|
|
61
|
+
readonly lastSuid?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface MaterializedViewCandidateInput extends MaterializedViewCreateInput {
|
|
64
|
+
readonly generation: number;
|
|
65
|
+
}
|
|
66
|
+
export interface MaterializedViewApplyInput {
|
|
67
|
+
readonly serviceId: string;
|
|
68
|
+
readonly viewId: string;
|
|
69
|
+
readonly generation: number;
|
|
70
|
+
readonly expectedLastSuid: string | null;
|
|
71
|
+
readonly lastSuid: string;
|
|
72
|
+
readonly definitionVersion: number;
|
|
73
|
+
readonly updatedAt: number;
|
|
74
|
+
readonly mutations: MaterializedViewMutationPlan;
|
|
75
|
+
}
|
|
76
|
+
export interface MaterializedViewApplyResult {
|
|
77
|
+
readonly instance: MaterializedViewInstance;
|
|
78
|
+
readonly rowUpserts: number;
|
|
79
|
+
readonly rowDeletes: number;
|
|
80
|
+
readonly rowPatches: number;
|
|
81
|
+
readonly indexEntries: number;
|
|
82
|
+
}
|
|
83
|
+
export interface MaterializedViewPromoteInput {
|
|
84
|
+
readonly serviceId: string;
|
|
85
|
+
readonly viewId: string;
|
|
86
|
+
readonly candidateGeneration: number;
|
|
87
|
+
readonly expectedActiveGeneration: number | null;
|
|
88
|
+
readonly updatedAt: number;
|
|
89
|
+
}
|
|
90
|
+
export interface MaterializedViewQueryOptions {
|
|
91
|
+
readonly generation?: number;
|
|
92
|
+
readonly indexId?: string;
|
|
93
|
+
readonly valueType?: MaterializedViewIndexValueType;
|
|
94
|
+
/** `null` means unbounded; omitted retains the port's 100-row default. */
|
|
95
|
+
readonly limit?: number | null;
|
|
96
|
+
readonly offset?: number;
|
|
97
|
+
/** Fixed boolean order selector; request data never becomes a SQL fragment. */
|
|
98
|
+
readonly descending?: boolean;
|
|
99
|
+
}
|
|
100
|
+
/** The list surface must choose whether it can see the exceptional unsafe lane. */
|
|
101
|
+
export type MaterializedViewListConsistency = "safe" | "unsafe";
|
|
102
|
+
export interface MaterializedViewListOptions extends MaterializedViewQueryOptions {
|
|
103
|
+
/** Omitted is deliberately safe; unsafe visibility is never an accidental fallback. */
|
|
104
|
+
readonly consistency?: MaterializedViewListConsistency;
|
|
105
|
+
}
|
|
106
|
+
/** A server-paged list response and the watermark that actually backs that response. */
|
|
107
|
+
export interface MaterializedViewListPage {
|
|
108
|
+
readonly rows: readonly MaterializedViewRow[];
|
|
109
|
+
readonly totalCount: number;
|
|
110
|
+
readonly readHead: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* One active-generation snapshot used by the SDT-G31 d1-mv waitFor path.
|
|
114
|
+
* `targetReceipt` is deliberately separate from `safeContiguousHead`: the
|
|
115
|
+
* latter can only complete a wait after the source target has been proved
|
|
116
|
+
* unique by the paired PipelineStore point lookup.
|
|
117
|
+
*/
|
|
118
|
+
export interface MaterializedViewWaitForState {
|
|
119
|
+
readonly activeGeneration: number | undefined;
|
|
120
|
+
readonly activeDefinitionVersion: number | undefined;
|
|
121
|
+
readonly safeContiguousHead: string;
|
|
122
|
+
readonly targetReceipt: boolean;
|
|
123
|
+
readonly checkpointAhead: boolean;
|
|
124
|
+
readonly rebuildRequired: boolean;
|
|
125
|
+
readonly poison: boolean;
|
|
126
|
+
}
|
|
127
|
+
export type MaterializedViewOrderingQuarantineClassification = "LATE_LOWER_SUID" | "ORDER_VIOLATION";
|
|
128
|
+
export interface MaterializedViewOrderingQuarantine {
|
|
129
|
+
readonly serviceId: string;
|
|
130
|
+
readonly viewId: string;
|
|
131
|
+
readonly generation: number;
|
|
132
|
+
readonly checkpointSuid: string;
|
|
133
|
+
readonly lateSuid: string;
|
|
134
|
+
readonly eventId: string;
|
|
135
|
+
readonly classification: MaterializedViewOrderingQuarantineClassification;
|
|
136
|
+
readonly status: "open" | "resolved";
|
|
137
|
+
readonly observedAt: number;
|
|
138
|
+
readonly resolvedAt: number | null;
|
|
139
|
+
readonly incidentIdentity: string | null;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Production D1 persistence for row-backed materialized views. Every apply
|
|
143
|
+
* call is a single predeclared D1 batch. The NOT NULL guard statement is
|
|
144
|
+
* deliberately first: if the expected checkpoint is stale it aborts the
|
|
145
|
+
* batch, so row/index/checkpoint regions cannot partially advance.
|
|
146
|
+
*/
|
|
147
|
+
export declare class D1MaterializedViewStore {
|
|
148
|
+
private readonly database;
|
|
149
|
+
private initialized;
|
|
150
|
+
private readonly unsafe;
|
|
151
|
+
constructor(database: D1Database);
|
|
152
|
+
initialize(): Promise<void>;
|
|
153
|
+
createActive(input: MaterializedViewCreateInput): Promise<MaterializedViewInstance>;
|
|
154
|
+
createCandidate(input: MaterializedViewCandidateInput): Promise<MaterializedViewInstance>;
|
|
155
|
+
beginRebuild(input: Omit<MaterializedViewCandidateInput, "generation"> & {
|
|
156
|
+
readonly generation?: number;
|
|
157
|
+
}): Promise<MaterializedViewInstance>;
|
|
158
|
+
nextGeneration(serviceId: string, viewId: string): Promise<number>;
|
|
159
|
+
readInstance(serviceId: string, viewId: string, generation: number): Promise<MaterializedViewInstance | undefined>;
|
|
160
|
+
readActive(serviceId: string, viewId: string): Promise<MaterializedViewInstance | undefined>;
|
|
161
|
+
readRows(serviceId: string, viewId: string, generation?: number): Promise<MaterializedViewRow[]>;
|
|
162
|
+
readIndexEntries(serviceId: string, viewId: string, generation?: number): Promise<MaterializedViewIndexEntry[]>;
|
|
163
|
+
/** Read rows through a declared index without exposing SQL identifiers to callers. */
|
|
164
|
+
queryRows(serviceId: string, viewId: string, options?: MaterializedViewQueryOptions): Promise<MaterializedViewRow[]>;
|
|
165
|
+
/** SDT-G23 composed server page; count and page rows share one statement. */
|
|
166
|
+
queryRowsWithTotal(serviceId: string, viewId: string, options?: MaterializedViewQueryOptions): Promise<UnsafeComposedPage>;
|
|
167
|
+
/**
|
|
168
|
+
* SDT-G55's explicit list-read port. Existing composed reads intentionally
|
|
169
|
+
* keep their G23 semantics; only this port can include unsafe rows, and it
|
|
170
|
+
* can include only rows newer than the active safe checkpoint.
|
|
171
|
+
*/
|
|
172
|
+
readListPage(serviceId: string, viewId: string, options?: MaterializedViewListOptions): Promise<MaterializedViewListPage>;
|
|
173
|
+
/** Explicit unsafe port: callers must choose this exceptional immediate lane. */
|
|
174
|
+
unsafeWindow(): UnsafeWindowMaterializedViewStore;
|
|
175
|
+
/** Runs repairable unsafe GC after an idle or advancing safe catch-up pass. */
|
|
176
|
+
collectUnsafeGarbage(serviceId: string, viewId: string, generation: number, definitionVersion: number, safeHead: string): Promise<number>;
|
|
177
|
+
hasTargetReceipt(serviceId: string, viewId: string, eventId: string, suid: string): Promise<boolean>;
|
|
178
|
+
/**
|
|
179
|
+
* Read all G31 wait facts in one static D1 statement. Every table access is
|
|
180
|
+
* keyed by service/view/active generation (and target identity where
|
|
181
|
+
* applicable); no row or source-history scan is permitted on this path.
|
|
182
|
+
*/
|
|
183
|
+
readWaitForState(serviceId: string, viewId: string, target: {
|
|
184
|
+
readonly eventId?: string;
|
|
185
|
+
readonly suid: string;
|
|
186
|
+
}): Promise<MaterializedViewWaitForState>;
|
|
187
|
+
/** Idempotent operational fact: a stored Queue event needs unsafe retry/DLQ attention. */
|
|
188
|
+
recordUnsafeFailureFinding(input: {
|
|
189
|
+
readonly serviceId: string;
|
|
190
|
+
readonly viewId: string;
|
|
191
|
+
/** The failed view generation when known; omitted binds the active one. */
|
|
192
|
+
readonly generation?: number;
|
|
193
|
+
readonly eventId: string;
|
|
194
|
+
readonly suid: string;
|
|
195
|
+
readonly observedAt: number;
|
|
196
|
+
}): Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* Persist the source-reset observation exactly once for the affected
|
|
199
|
+
* generation. This deliberately never rewinds `mv_instances.last_suid`.
|
|
200
|
+
*/
|
|
201
|
+
recordCheckpointAhead(input: {
|
|
202
|
+
readonly serviceId: string;
|
|
203
|
+
readonly viewId: string;
|
|
204
|
+
readonly generation: number;
|
|
205
|
+
readonly checkpointSuid: string;
|
|
206
|
+
readonly storeMaxSuid: string;
|
|
207
|
+
readonly observedAt: number;
|
|
208
|
+
}): Promise<void>;
|
|
209
|
+
/** An open finding on the active generation fail-closes composed MV reads. */
|
|
210
|
+
hasCheckpointAheadFinding(serviceId: string, viewId: string): Promise<boolean>;
|
|
211
|
+
/** Open quarantine is keyed to the active generation; old generations are not a public read gate. */
|
|
212
|
+
readOrderingQuarantine(serviceId: string, viewId: string): Promise<MaterializedViewOrderingQuarantine | undefined>;
|
|
213
|
+
readActiveGeneration(serviceId: string, viewId: string): Promise<number | undefined>;
|
|
214
|
+
/** Persist the detector result before stopping the safe pass. Repeated observations are idempotent. */
|
|
215
|
+
recordOrderingQuarantine(input: {
|
|
216
|
+
readonly serviceId: string;
|
|
217
|
+
readonly viewId: string;
|
|
218
|
+
readonly generation: number;
|
|
219
|
+
readonly checkpointSuid: string;
|
|
220
|
+
readonly lateSuid: string;
|
|
221
|
+
readonly eventId: string;
|
|
222
|
+
readonly classification: MaterializedViewOrderingQuarantineClassification;
|
|
223
|
+
readonly observedAt: number;
|
|
224
|
+
}): Promise<void>;
|
|
225
|
+
/**
|
|
226
|
+
* Mark a candidate only after the real rebuild path observed the complete
|
|
227
|
+
* source history. The proof is incident-bound when an ordering quarantine is
|
|
228
|
+
* open, and an empty row set is valid when the source history itself is
|
|
229
|
+
* complete (for example, a rebuild whose materializer deletes every row).
|
|
230
|
+
*/
|
|
231
|
+
markGenerationRebuilt(input: {
|
|
232
|
+
readonly serviceId: string;
|
|
233
|
+
readonly viewId: string;
|
|
234
|
+
readonly generation: number;
|
|
235
|
+
readonly verifiedAt: number;
|
|
236
|
+
readonly rebuildId: string;
|
|
237
|
+
readonly sourceEventCount: number;
|
|
238
|
+
readonly sourceEventIds: readonly string[];
|
|
239
|
+
readonly sourceSuids: readonly string[];
|
|
240
|
+
readonly sourceMaxSuid: string;
|
|
241
|
+
readonly sourceHistoryDigest: string;
|
|
242
|
+
}): Promise<void>;
|
|
243
|
+
/**
|
|
244
|
+
* Apply rows/index entries and advance one generation checkpoint atomically.
|
|
245
|
+
* The guard row intentionally uses a NOT NULL failure to abort a stale CAS.
|
|
246
|
+
*/
|
|
247
|
+
applyMutationsAndAdvanceCheckpoint(input: MaterializedViewApplyInput): Promise<MaterializedViewApplyResult>;
|
|
248
|
+
promoteGeneration(input: MaterializedViewPromoteInput): Promise<MaterializedViewInstance>;
|
|
249
|
+
/** Short aliases make the port convenient for runtime composition and tests. */
|
|
250
|
+
readonly apply: (input: MaterializedViewApplyInput) => Promise<MaterializedViewApplyResult>;
|
|
251
|
+
readonly promote: (input: MaterializedViewPromoteInput) => Promise<MaterializedViewInstance>;
|
|
252
|
+
private rowFrom;
|
|
253
|
+
private indexFrom;
|
|
254
|
+
private validateGeneration;
|
|
255
|
+
private ready;
|
|
256
|
+
}
|
|
257
|
+
export type MaterializedViewStore = Pick<D1MaterializedViewStore, "initialize" | "createActive" | "createCandidate" | "beginRebuild" | "nextGeneration" | "readInstance" | "readActive" | "readRows" | "readIndexEntries" | "queryRows" | "queryRowsWithTotal" | "readListPage" | "hasTargetReceipt" | "readOrderingQuarantine" | "recordOrderingQuarantine" | "markGenerationRebuilt" | "readWaitForState" | "recordCheckpointAhead" | "hasCheckpointAheadFinding" | "applyMutationsAndAdvanceCheckpoint" | "promoteGeneration">;
|