@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,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retained non-commit Journal support: the fenced G42 probe, a direct legacy
|
|
3
|
+
* state diagnostic, and RepairWorker's historical-workset/audit surface.
|
|
4
|
+
*/
|
|
5
|
+
export declare class JournalDurableObject implements DurableObject {
|
|
6
|
+
private readonly ctx;
|
|
7
|
+
/** Constructor-scoped observation only; never persisted or used for control. */
|
|
8
|
+
private readonly activation;
|
|
9
|
+
constructor(ctx: DurableObjectState, _env?: unknown, _nativeTracing?: unknown);
|
|
10
|
+
fetch(request: Request): Promise<Response>;
|
|
11
|
+
/**
|
|
12
|
+
* A G42 probe can intentionally leave an alarm behind. Any non-probe alarm
|
|
13
|
+
* is a pre-cleanup remnant in C-0 and is cleared without reviving recovery.
|
|
14
|
+
*/
|
|
15
|
+
alarm(): Promise<void>;
|
|
16
|
+
private readRecord;
|
|
17
|
+
/**
|
|
18
|
+
* Implements the G42-only storage-layout screen. The outer Worker is the
|
|
19
|
+
* sole public entrypoint and authenticates/fences it before a JOURNAL stub
|
|
20
|
+
* is even resolved. This method still validates its private message so an
|
|
21
|
+
* accidental internal route cannot touch a normal Journal record.
|
|
22
|
+
*/
|
|
23
|
+
private g42Probe;
|
|
24
|
+
/** A probe alarm must be inert even if an interrupted runner never cleans it. */
|
|
25
|
+
private clearG42ProbeAlarmIfPresent;
|
|
26
|
+
private repairWorkset;
|
|
27
|
+
private repairObservations;
|
|
28
|
+
private jsonBody;
|
|
29
|
+
private recordRepairObservation;
|
|
30
|
+
}
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { DurableObjectActivation, } from "../trace/CommitTrace";
|
|
2
|
+
import { beginDurableObjectHandlerObservation, } from "../trace/ObservationStream";
|
|
3
|
+
import { G42_JOURNAL_PROBE_ALARM_KEY, G42_JOURNAL_PROBE_INTERNAL_PREFIX, G42_JOURNAL_PROBE_SCHEMA, G42_JOURNAL_PROBE_INDEX_KEY, g42ProbeStorageKey, isG42ProbeLogicalKey, } from "./JournalFirstTouchProbe";
|
|
4
|
+
const JOURNAL_KEY = "journal";
|
|
5
|
+
/** The probe alarm is deliberately far outside its measurement interval. */
|
|
6
|
+
const G42_PROBE_ALARM_DELAY_MS = 24 * 60 * 60 * 1_000;
|
|
7
|
+
function json(body, status = 200) {
|
|
8
|
+
return new Response(JSON.stringify(body), {
|
|
9
|
+
status,
|
|
10
|
+
headers: { "content-type": "application/json; charset=utf-8" },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
function error(status, code, message) {
|
|
14
|
+
return json({ error: message, code }, status);
|
|
15
|
+
}
|
|
16
|
+
function isObject(value) {
|
|
17
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
function isNonEmptyString(value) {
|
|
20
|
+
return typeof value === "string" && value.length > 0;
|
|
21
|
+
}
|
|
22
|
+
function isNonNegativeInteger(value) {
|
|
23
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
24
|
+
}
|
|
25
|
+
function g42ProbeInternalRequest(value, requestBytes) {
|
|
26
|
+
if (!isObject(value) || value.schema !== G42_JOURNAL_PROBE_SCHEMA || !isNonEmptyString(value.trialId)) {
|
|
27
|
+
return { error: "G42 probe request requires its schema and trialId" };
|
|
28
|
+
}
|
|
29
|
+
if (value.action !== "ping" && value.action !== "state" && value.action !== "write" && value.action !== "cleanup" && value.action !== "inventory") {
|
|
30
|
+
return { error: "G42 probe action is not recognized" };
|
|
31
|
+
}
|
|
32
|
+
if ((value.action === "state" || value.action === "write") && !isG42ProbeLogicalKey(value.logicalKey)) {
|
|
33
|
+
return { error: "G42 probe logical key lacks the reserved fixed-length prefix" };
|
|
34
|
+
}
|
|
35
|
+
if (value.action === "write") {
|
|
36
|
+
if ((value.recordKind !== "warmup" && value.recordKind !== "measure") || !isNonEmptyString(value.payload)) {
|
|
37
|
+
return { error: "G42 probe write requires recordKind and payload" };
|
|
38
|
+
}
|
|
39
|
+
if (new TextEncoder().encode(value.payload).byteLength > 8_192) {
|
|
40
|
+
return { error: "G42 probe payload exceeds the fixed probe limit" };
|
|
41
|
+
}
|
|
42
|
+
if (value.alarmMode !== "on" && value.alarmMode !== "off") {
|
|
43
|
+
return { error: "G42 probe write requires a recognized alarmMode" };
|
|
44
|
+
}
|
|
45
|
+
if (value.cell !== "A" && value.cell !== "B" && value.cell !== "C" && value.cell !== "D") {
|
|
46
|
+
return { error: "G42 probe write requires a recognized fixed-width cell" };
|
|
47
|
+
}
|
|
48
|
+
if (value.recordKind === "warmup" && value.alarmMode !== "off") {
|
|
49
|
+
return { error: "G42 probe warmup must not install an alarm" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
value: {
|
|
54
|
+
action: value.action,
|
|
55
|
+
trialId: value.trialId,
|
|
56
|
+
...(value.cell === "A" || value.cell === "B" || value.cell === "C" || value.cell === "D" ? { cell: value.cell } : {}),
|
|
57
|
+
...(isG42ProbeLogicalKey(value.logicalKey) ? { logicalKey: value.logicalKey } : {}),
|
|
58
|
+
...(value.recordKind === "warmup" || value.recordKind === "measure" ? { recordKind: value.recordKind } : {}),
|
|
59
|
+
...(isNonEmptyString(value.payload) ? { payload: value.payload } : {}),
|
|
60
|
+
...(value.alarmMode === "on" || value.alarmMode === "off" ? { alarmMode: value.alarmMode } : {}),
|
|
61
|
+
requestBytes,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function g42ProbeIndex(value) {
|
|
66
|
+
if (!isObject(value) || value.schema !== G42_JOURNAL_PROBE_SCHEMA || !Array.isArray(value.logicalKeys) || !Array.isArray(value.warmupTrialIds)) {
|
|
67
|
+
return { schema: G42_JOURNAL_PROBE_SCHEMA, logicalKeys: [], warmupTrialIds: [] };
|
|
68
|
+
}
|
|
69
|
+
const logicalKeys = value.logicalKeys.filter(isG42ProbeLogicalKey);
|
|
70
|
+
const warmupTrialIds = value.warmupTrialIds.filter(isNonEmptyString);
|
|
71
|
+
if (logicalKeys.length !== value.logicalKeys.length || new Set(logicalKeys).size !== logicalKeys.length
|
|
72
|
+
|| warmupTrialIds.length !== value.warmupTrialIds.length || new Set(warmupTrialIds).size !== warmupTrialIds.length) {
|
|
73
|
+
return { schema: G42_JOURNAL_PROBE_SCHEMA, logicalKeys: [], warmupTrialIds: [] };
|
|
74
|
+
}
|
|
75
|
+
return { schema: G42_JOURNAL_PROBE_SCHEMA, logicalKeys, warmupTrialIds };
|
|
76
|
+
}
|
|
77
|
+
function g42ProbeAlarmMarker(value) {
|
|
78
|
+
if (!isObject(value) || value.schema !== G42_JOURNAL_PROBE_SCHEMA || !isNonNegativeInteger(value.dueAt))
|
|
79
|
+
return undefined;
|
|
80
|
+
return { schema: G42_JOURNAL_PROBE_SCHEMA, dueAt: value.dueAt };
|
|
81
|
+
}
|
|
82
|
+
function g42ProbeActivation(activation, firstStorageReadMs) {
|
|
83
|
+
return {
|
|
84
|
+
activationId: activation.activationId,
|
|
85
|
+
activationFirst: activation.first,
|
|
86
|
+
constructorToHandlerMs: activation.constructorToHandlerMs,
|
|
87
|
+
firstStorageReadMs,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function repairObservationFrom(value) {
|
|
91
|
+
if (!isObject(value) ||
|
|
92
|
+
!isNonEmptyString(value.owner) ||
|
|
93
|
+
!isNonNegativeInteger(value.epoch) ||
|
|
94
|
+
!isNonEmptyString(value.tag) ||
|
|
95
|
+
!isNonEmptyString(value.attemptId) ||
|
|
96
|
+
!isNonEmptyString(value.eventId) ||
|
|
97
|
+
!isNonEmptyString(value.suid) ||
|
|
98
|
+
(value.phase !== "PREPARED" && value.phase !== "VERIFIED" && value.phase !== "CLEARED")) {
|
|
99
|
+
return { error: "repair observation needs owner, epoch, tag, attemptId, eventId, suid, and phase" };
|
|
100
|
+
}
|
|
101
|
+
if (value.branch !== undefined &&
|
|
102
|
+
value.branch !== "ROLLED_FORWARD" &&
|
|
103
|
+
value.branch !== "EXCLUDED_AUDITED" &&
|
|
104
|
+
value.branch !== "FAILED_CLOSED") {
|
|
105
|
+
return { error: "repair observation branch is not recognized" };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
value: {
|
|
109
|
+
owner: value.owner,
|
|
110
|
+
epoch: value.epoch,
|
|
111
|
+
tag: value.tag,
|
|
112
|
+
attemptId: value.attemptId,
|
|
113
|
+
eventId: value.eventId,
|
|
114
|
+
suid: value.suid,
|
|
115
|
+
phase: value.phase,
|
|
116
|
+
...(value.branch === undefined ? {} : { branch: value.branch }),
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Retained non-commit Journal support: the fenced G42 probe, a direct legacy
|
|
122
|
+
* state diagnostic, and RepairWorker's historical-workset/audit surface.
|
|
123
|
+
*/
|
|
124
|
+
export class JournalDurableObject {
|
|
125
|
+
ctx;
|
|
126
|
+
/** Constructor-scoped observation only; never persisted or used for control. */
|
|
127
|
+
activation = new DurableObjectActivation();
|
|
128
|
+
constructor(ctx, _env, _nativeTracing) {
|
|
129
|
+
this.ctx = ctx;
|
|
130
|
+
// Preserve the Durable Object constructor ABI used by existing fixtures.
|
|
131
|
+
void _env;
|
|
132
|
+
void _nativeTracing;
|
|
133
|
+
}
|
|
134
|
+
async fetch(request) {
|
|
135
|
+
// Flip before this handler performs its first await.
|
|
136
|
+
const activation = this.activation.beginHandler();
|
|
137
|
+
const observation = beginDurableObjectHandlerObservation("JOURNAL", activation);
|
|
138
|
+
const path = new URL(request.url).pathname;
|
|
139
|
+
// G42 has a private, conformance-only synthetic namespace. It does not
|
|
140
|
+
// enter the normal commit trace or recovery surface: its receipts carry a
|
|
141
|
+
// distinct schema and its storage keys can never be JOURNAL_KEY.
|
|
142
|
+
if (request.method === "POST" && path.startsWith(`${G42_JOURNAL_PROBE_INTERNAL_PREFIX}/`)) {
|
|
143
|
+
return this.g42Probe(request, path, activation, observation);
|
|
144
|
+
}
|
|
145
|
+
if (request.method === "GET" && path === "/state") {
|
|
146
|
+
const record = await this.readRecord();
|
|
147
|
+
return record === undefined ? error(404, "journal_not_found", "Journal has not been admitted") : json(record);
|
|
148
|
+
}
|
|
149
|
+
if (request.method === "GET" && path === "/repair/workset") {
|
|
150
|
+
return this.repairWorkset();
|
|
151
|
+
}
|
|
152
|
+
if (request.method === "GET" && path === "/repair/observations") {
|
|
153
|
+
return this.repairObservations();
|
|
154
|
+
}
|
|
155
|
+
if (request.method === "POST" && path === "/repair/observation") {
|
|
156
|
+
const body = await this.jsonBody(request);
|
|
157
|
+
return body === undefined
|
|
158
|
+
? error(400, "malformed_journal_request", "Request body must be JSON")
|
|
159
|
+
: this.recordRepairObservation(body);
|
|
160
|
+
}
|
|
161
|
+
return error(404, "journal_route_not_found", "Journal route was not found");
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A G42 probe can intentionally leave an alarm behind. Any non-probe alarm
|
|
165
|
+
* is a pre-cleanup remnant in C-0 and is cleared without reviving recovery.
|
|
166
|
+
*/
|
|
167
|
+
async alarm() {
|
|
168
|
+
// The G38 tombstone fixture supplies only deleteAlarm. Keep that binding
|
|
169
|
+
// neutral without assuming the full DurableObjectStorage shape.
|
|
170
|
+
if (typeof this.ctx.storage.transaction !== "function") {
|
|
171
|
+
await this.ctx.storage.deleteAlarm();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (await this.clearG42ProbeAlarmIfPresent())
|
|
175
|
+
return;
|
|
176
|
+
await this.ctx.storage.deleteAlarm();
|
|
177
|
+
}
|
|
178
|
+
async readRecord() {
|
|
179
|
+
return this.ctx.storage.get(JOURNAL_KEY);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Implements the G42-only storage-layout screen. The outer Worker is the
|
|
183
|
+
* sole public entrypoint and authenticates/fences it before a JOURNAL stub
|
|
184
|
+
* is even resolved. This method still validates its private message so an
|
|
185
|
+
* accidental internal route cannot touch a normal Journal record.
|
|
186
|
+
*/
|
|
187
|
+
async g42Probe(request, path, activation, observation) {
|
|
188
|
+
let raw;
|
|
189
|
+
try {
|
|
190
|
+
raw = await request.text();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return error(400, "g42_probe_malformed", "G42 probe request body is unavailable");
|
|
194
|
+
}
|
|
195
|
+
const requestBytes = new TextEncoder().encode(raw).byteLength;
|
|
196
|
+
let body;
|
|
197
|
+
try {
|
|
198
|
+
body = JSON.parse(raw);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return error(400, "g42_probe_malformed", "G42 probe request body must be JSON");
|
|
202
|
+
}
|
|
203
|
+
const parsed = g42ProbeInternalRequest(body, requestBytes);
|
|
204
|
+
if (parsed.value === undefined)
|
|
205
|
+
return error(400, "g42_probe_invalid", parsed.error ?? "G42 probe request is invalid");
|
|
206
|
+
const input = parsed.value;
|
|
207
|
+
if (path !== `${G42_JOURNAL_PROBE_INTERNAL_PREFIX}/${input.action}`) {
|
|
208
|
+
return error(404, "g42_probe_route_not_found", "G42 probe action does not match its internal route");
|
|
209
|
+
}
|
|
210
|
+
let firstStorageReadMs = null;
|
|
211
|
+
const markFirstStorageRead = () => {
|
|
212
|
+
if (firstStorageReadMs !== null)
|
|
213
|
+
return;
|
|
214
|
+
observation.markFirstStorageRead();
|
|
215
|
+
firstStorageReadMs = Math.max(0, Date.now() - activation.handlerStartedAtMs);
|
|
216
|
+
};
|
|
217
|
+
const respond = (action, detail = {}) => json({
|
|
218
|
+
schema: G42_JOURNAL_PROBE_SCHEMA,
|
|
219
|
+
action,
|
|
220
|
+
activation: g42ProbeActivation(activation, firstStorageReadMs),
|
|
221
|
+
handlerWallMs: Math.max(0, Date.now() - activation.handlerStartedAtMs),
|
|
222
|
+
transactionWallMs: detail.transactionWallMs ?? null,
|
|
223
|
+
requestBytes: input.requestBytes,
|
|
224
|
+
recordBytes: detail.recordBytes ?? null,
|
|
225
|
+
alarmStateBefore: detail.alarmStateBefore ?? null,
|
|
226
|
+
alarmDueAt: detail.alarmDueAt ?? null,
|
|
227
|
+
setAlarmCalls: detail.setAlarmCalls ?? 0,
|
|
228
|
+
...(detail.logicalKey === undefined ? {} : { logicalKey: detail.logicalKey }),
|
|
229
|
+
...(detail.expectedWarmupKeyPresent === undefined ? {} : { expectedWarmupKeyPresent: detail.expectedWarmupKeyPresent }),
|
|
230
|
+
...(detail.keyPresent === undefined ? {} : { keyPresent: detail.keyPresent }),
|
|
231
|
+
...(detail.inventory === undefined ? {} : { inventory: [...detail.inventory] }),
|
|
232
|
+
...(detail.productionJournalPresent === undefined ? {} : { productionJournalPresent: detail.productionJournalPresent }),
|
|
233
|
+
});
|
|
234
|
+
if (input.action === "ping") {
|
|
235
|
+
return respond("ping");
|
|
236
|
+
}
|
|
237
|
+
if (input.action === "state") {
|
|
238
|
+
const logicalKey = input.logicalKey;
|
|
239
|
+
markFirstStorageRead();
|
|
240
|
+
const startedAtMs = Date.now();
|
|
241
|
+
const record = await this.ctx.storage.get(g42ProbeStorageKey(logicalKey));
|
|
242
|
+
const response = respond("state", {
|
|
243
|
+
transactionWallMs: Math.max(0, Date.now() - startedAtMs),
|
|
244
|
+
logicalKey,
|
|
245
|
+
keyPresent: record !== undefined,
|
|
246
|
+
});
|
|
247
|
+
// C is deliberately a real missing-key storage read. The outer probe
|
|
248
|
+
// helper accepts this schema-preserving 404 as an expected mediator,
|
|
249
|
+
// while retaining the actual status in the per-trial receipt.
|
|
250
|
+
return record === undefined
|
|
251
|
+
? new Response(response.body, { status: 404, headers: response.headers })
|
|
252
|
+
: response;
|
|
253
|
+
}
|
|
254
|
+
if (input.action === "write") {
|
|
255
|
+
const logicalKey = input.logicalKey;
|
|
256
|
+
const recordKind = input.recordKind;
|
|
257
|
+
const payload = input.payload;
|
|
258
|
+
const alarmMode = input.alarmMode;
|
|
259
|
+
markFirstStorageRead();
|
|
260
|
+
const startedAtMs = Date.now();
|
|
261
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
262
|
+
// The G42 namespace is deliberately disjoint. Do not share a physical
|
|
263
|
+
// object with a normal Journal even if an internal caller is buggy.
|
|
264
|
+
const productionJournal = await txn.get(JOURNAL_KEY);
|
|
265
|
+
if (productionJournal !== undefined)
|
|
266
|
+
return { conflict: "production_journal_present" };
|
|
267
|
+
const storageKey = g42ProbeStorageKey(logicalKey);
|
|
268
|
+
const existing = await txn.get(storageKey);
|
|
269
|
+
if (existing !== undefined)
|
|
270
|
+
return { conflict: "logical_key_already_present" };
|
|
271
|
+
const alarmStateBefore = await txn.getAlarm();
|
|
272
|
+
if (alarmStateBefore !== null)
|
|
273
|
+
return { conflict: "probe_alarm_prestate_not_empty" };
|
|
274
|
+
const record = {
|
|
275
|
+
schema: G42_JOURNAL_PROBE_SCHEMA,
|
|
276
|
+
trialId: input.trialId,
|
|
277
|
+
logicalKey,
|
|
278
|
+
recordKind,
|
|
279
|
+
payload,
|
|
280
|
+
createdAtMs: Date.now(),
|
|
281
|
+
};
|
|
282
|
+
const index = g42ProbeIndex(await txn.get(G42_JOURNAL_PROBE_INDEX_KEY));
|
|
283
|
+
// This is deliberately an index-only check. A and D execute the
|
|
284
|
+
// same measured storage reads and request shape; D's already-read
|
|
285
|
+
// index proves its distinct alarm-free warm-up by trial ID.
|
|
286
|
+
let expectedWarmupKeyPresent;
|
|
287
|
+
if (recordKind === "measure" && input.cell === "D") {
|
|
288
|
+
expectedWarmupKeyPresent = index.warmupTrialIds.includes(input.trialId);
|
|
289
|
+
if (!expectedWarmupKeyPresent)
|
|
290
|
+
return { conflict: "expected_warmup_record_missing" };
|
|
291
|
+
}
|
|
292
|
+
const nextIndex = {
|
|
293
|
+
schema: G42_JOURNAL_PROBE_SCHEMA,
|
|
294
|
+
logicalKeys: [...index.logicalKeys, logicalKey].sort(),
|
|
295
|
+
warmupTrialIds: recordKind === "warmup"
|
|
296
|
+
? [...index.warmupTrialIds, input.trialId].sort()
|
|
297
|
+
: index.warmupTrialIds,
|
|
298
|
+
};
|
|
299
|
+
await txn.put(storageKey, record);
|
|
300
|
+
await txn.put(G42_JOURNAL_PROBE_INDEX_KEY, nextIndex);
|
|
301
|
+
let alarmDueAt = null;
|
|
302
|
+
let setAlarmCalls = 0;
|
|
303
|
+
if (alarmMode === "on") {
|
|
304
|
+
alarmDueAt = Date.now() + G42_PROBE_ALARM_DELAY_MS;
|
|
305
|
+
await txn.put(G42_JOURNAL_PROBE_ALARM_KEY, {
|
|
306
|
+
schema: G42_JOURNAL_PROBE_SCHEMA,
|
|
307
|
+
dueAt: alarmDueAt,
|
|
308
|
+
});
|
|
309
|
+
await txn.setAlarm(alarmDueAt);
|
|
310
|
+
setAlarmCalls = 1;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
conflict: undefined,
|
|
314
|
+
recordBytes: new TextEncoder().encode(JSON.stringify(record)).byteLength,
|
|
315
|
+
alarmStateBefore,
|
|
316
|
+
alarmDueAt,
|
|
317
|
+
setAlarmCalls,
|
|
318
|
+
expectedWarmupKeyPresent,
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
if (result.conflict !== undefined) {
|
|
322
|
+
return error(409, "g42_probe_conflict", result.conflict);
|
|
323
|
+
}
|
|
324
|
+
return respond("write", {
|
|
325
|
+
transactionWallMs: Math.max(0, Date.now() - startedAtMs),
|
|
326
|
+
recordBytes: result.recordBytes,
|
|
327
|
+
alarmStateBefore: result.alarmStateBefore,
|
|
328
|
+
alarmDueAt: result.alarmDueAt,
|
|
329
|
+
setAlarmCalls: result.setAlarmCalls,
|
|
330
|
+
logicalKey,
|
|
331
|
+
...(result.expectedWarmupKeyPresent === undefined ? {} : { expectedWarmupKeyPresent: result.expectedWarmupKeyPresent }),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (input.action === "cleanup") {
|
|
335
|
+
markFirstStorageRead();
|
|
336
|
+
const startedAtMs = Date.now();
|
|
337
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
338
|
+
const productionJournal = await txn.get(JOURNAL_KEY);
|
|
339
|
+
if (productionJournal !== undefined)
|
|
340
|
+
return { conflict: "production_journal_present" };
|
|
341
|
+
const index = g42ProbeIndex(await txn.get(G42_JOURNAL_PROBE_INDEX_KEY));
|
|
342
|
+
for (const logicalKey of index.logicalKeys) {
|
|
343
|
+
await txn.delete(g42ProbeStorageKey(logicalKey));
|
|
344
|
+
}
|
|
345
|
+
await txn.delete(G42_JOURNAL_PROBE_INDEX_KEY);
|
|
346
|
+
const marker = g42ProbeAlarmMarker(await txn.get(G42_JOURNAL_PROBE_ALARM_KEY));
|
|
347
|
+
if (marker !== undefined) {
|
|
348
|
+
await txn.delete(G42_JOURNAL_PROBE_ALARM_KEY);
|
|
349
|
+
await txn.deleteAlarm();
|
|
350
|
+
}
|
|
351
|
+
return { conflict: undefined, cleared: index.logicalKeys.length, hadAlarm: marker !== undefined };
|
|
352
|
+
});
|
|
353
|
+
if (result.conflict !== undefined)
|
|
354
|
+
return error(409, "g42_probe_conflict", result.conflict);
|
|
355
|
+
return respond("cleanup", {
|
|
356
|
+
transactionWallMs: Math.max(0, Date.now() - startedAtMs),
|
|
357
|
+
inventory: [],
|
|
358
|
+
alarmDueAt: null,
|
|
359
|
+
productionJournalPresent: false,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
markFirstStorageRead();
|
|
363
|
+
const startedAtMs = Date.now();
|
|
364
|
+
const inventory = await this.ctx.storage.transaction(async (txn) => {
|
|
365
|
+
const index = g42ProbeIndex(await txn.get(G42_JOURNAL_PROBE_INDEX_KEY));
|
|
366
|
+
const marker = g42ProbeAlarmMarker(await txn.get(G42_JOURNAL_PROBE_ALARM_KEY));
|
|
367
|
+
const alarmDueAt = await txn.getAlarm();
|
|
368
|
+
const productionJournal = await txn.get(JOURNAL_KEY);
|
|
369
|
+
return {
|
|
370
|
+
logicalKeys: index.logicalKeys,
|
|
371
|
+
alarmDueAt: marker === undefined ? null : alarmDueAt,
|
|
372
|
+
productionJournalPresent: productionJournal !== undefined,
|
|
373
|
+
};
|
|
374
|
+
});
|
|
375
|
+
return respond("inventory", {
|
|
376
|
+
transactionWallMs: Math.max(0, Date.now() - startedAtMs),
|
|
377
|
+
inventory: inventory.logicalKeys,
|
|
378
|
+
alarmDueAt: inventory.alarmDueAt,
|
|
379
|
+
productionJournalPresent: inventory.productionJournalPresent,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/** A probe alarm must be inert even if an interrupted runner never cleans it. */
|
|
383
|
+
async clearG42ProbeAlarmIfPresent() {
|
|
384
|
+
return this.ctx.storage.transaction(async (txn) => {
|
|
385
|
+
const marker = g42ProbeAlarmMarker(await txn.get(G42_JOURNAL_PROBE_ALARM_KEY));
|
|
386
|
+
if (marker === undefined)
|
|
387
|
+
return false;
|
|
388
|
+
await txn.delete(G42_JOURNAL_PROBE_ALARM_KEY);
|
|
389
|
+
await txn.deleteAlarm();
|
|
390
|
+
return true;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async repairWorkset() {
|
|
394
|
+
const record = await this.readRecord();
|
|
395
|
+
if (record === undefined) {
|
|
396
|
+
return error(404, "journal_not_found", "Journal has not been admitted");
|
|
397
|
+
}
|
|
398
|
+
if (record.state !== "PARTIAL") {
|
|
399
|
+
return error(409, "journal_not_partial", "Repair work is available only for a PARTIAL Journal outcome");
|
|
400
|
+
}
|
|
401
|
+
const vector = record.reconciliation?.allocatorVector;
|
|
402
|
+
if (vector === undefined || vector.length !== record.candidates.length) {
|
|
403
|
+
return error(409, "repair_workset_indeterminate", "PARTIAL Journal lacks its durable allocator vector");
|
|
404
|
+
}
|
|
405
|
+
return json({
|
|
406
|
+
attemptId: record.commitContext?.attemptId,
|
|
407
|
+
missingTags: record.reconciliation?.missingTags ?? [],
|
|
408
|
+
candidates: record.candidates.map((candidate, index) => ({
|
|
409
|
+
...candidate,
|
|
410
|
+
suid: vector[index],
|
|
411
|
+
...(record.commitContext?.allocatorLineageId === undefined
|
|
412
|
+
? {}
|
|
413
|
+
: { allocatorLineageId: record.commitContext.allocatorLineageId }),
|
|
414
|
+
})),
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
async repairObservations() {
|
|
418
|
+
const record = await this.readRecord();
|
|
419
|
+
if (record === undefined) {
|
|
420
|
+
return error(404, "journal_not_found", "Journal has not been admitted");
|
|
421
|
+
}
|
|
422
|
+
return json({ observations: record.repairObservations ?? [] });
|
|
423
|
+
}
|
|
424
|
+
async jsonBody(request) {
|
|
425
|
+
try {
|
|
426
|
+
return await request.json();
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
return undefined;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async recordRepairObservation(body) {
|
|
433
|
+
const parsed = repairObservationFrom(body);
|
|
434
|
+
if (parsed.value === undefined) {
|
|
435
|
+
return error(400, "invalid_repair_observation", parsed.error ?? "Invalid repair observation");
|
|
436
|
+
}
|
|
437
|
+
const input = parsed.value;
|
|
438
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
439
|
+
const record = await txn.get(JOURNAL_KEY);
|
|
440
|
+
if (record === undefined) {
|
|
441
|
+
return { ok: false, status: 404, error: "Journal has not been admitted" };
|
|
442
|
+
}
|
|
443
|
+
if (record.state !== "PARTIAL") {
|
|
444
|
+
return { ok: false, status: 409, error: "Repair observations are valid only for a PARTIAL Journal outcome" };
|
|
445
|
+
}
|
|
446
|
+
// The Journal only records observations for its admitted candidate and
|
|
447
|
+
// durable SUID. This keeps progress useful without making it authority.
|
|
448
|
+
const candidateIndex = record.candidates.findIndex((candidate) => candidate.eventId === input.eventId && candidate.payload !== undefined && candidate.tags.includes(input.tag));
|
|
449
|
+
const vector = record.reconciliation?.allocatorVector;
|
|
450
|
+
if (input.attemptId !== record.commitContext?.attemptId ||
|
|
451
|
+
candidateIndex < 0 ||
|
|
452
|
+
vector === undefined ||
|
|
453
|
+
vector[candidateIndex] !== input.suid) {
|
|
454
|
+
return { ok: false, status: 422, error: "Repair observation does not match the durable PARTIAL workset" };
|
|
455
|
+
}
|
|
456
|
+
const observations = record.repairObservations ?? [];
|
|
457
|
+
const alreadyRecorded = observations.some((observation) => observation.owner === input.owner &&
|
|
458
|
+
observation.epoch === input.epoch &&
|
|
459
|
+
observation.tag === input.tag &&
|
|
460
|
+
observation.attemptId === input.attemptId &&
|
|
461
|
+
observation.eventId === input.eventId &&
|
|
462
|
+
observation.suid === input.suid &&
|
|
463
|
+
observation.phase === input.phase &&
|
|
464
|
+
observation.branch === input.branch);
|
|
465
|
+
if (alreadyRecorded) {
|
|
466
|
+
return { ok: true, record };
|
|
467
|
+
}
|
|
468
|
+
const updated = {
|
|
469
|
+
...record,
|
|
470
|
+
repairObservations: [...observations, { ...input, observedAt: new Date().toISOString() }],
|
|
471
|
+
version: record.version + 1,
|
|
472
|
+
updatedAt: new Date().toISOString(),
|
|
473
|
+
};
|
|
474
|
+
await txn.put(JOURNAL_KEY, updated);
|
|
475
|
+
return { ok: true, record: updated };
|
|
476
|
+
});
|
|
477
|
+
return result.ok
|
|
478
|
+
? json({ status: "repair-observation-recorded", observations: result.record.repairObservations ?? [] })
|
|
479
|
+
: error(result.status, "repair_observation_rejected", result.error);
|
|
480
|
+
}
|
|
481
|
+
}
|