@sekiban/dcb-runtime 0.2.0 → 0.2.1
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/README.md +5 -1
- package/dist/allocator/AllocatorDurableObject.js +25 -2
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +2 -0
- package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +64 -1
- package/dist/bootstrap/types.d.ts +6 -0
- package/dist/cloudflare.d.ts +3 -0
- package/dist/cloudflare.js +1493 -451
- package/dist/commit/CommitWorker.d.ts +19 -0
- package/dist/commit/CommitWorker.js +176 -27
- package/dist/completeness/IncidentLifecycle.d.ts +75 -0
- package/dist/completeness/IncidentLifecycle.js +503 -0
- package/dist/completeness/types.d.ts +67 -0
- package/dist/cosmos.d.ts +34 -4
- package/dist/cosmos.js +214 -49
- package/dist/downstream/OutboxDrain.d.ts +4 -1
- package/dist/downstream/OutboxDrain.js +24 -2
- package/dist/generated/cosmos-layout.d.ts +87 -0
- package/dist/generated/cosmos-layout.js +99 -0
- package/dist/http/IncidentMaintenance.d.ts +8 -0
- package/dist/http/IncidentMaintenance.js +145 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +1548 -451
- package/dist/read/SerializedReadWorker.d.ts +3 -0
- package/dist/read/SerializedReadWorker.js +3 -3
- package/dist/shard/ShardRotation.d.ts +48 -0
- package/dist/shard/ShardRotation.js +120 -0
- package/dist/store/CosmosEventStore.js +56 -46
- package/dist/store/PostgresEventStore.d.ts +20 -0
- package/dist/store/PostgresEventStore.js +53 -0
- package/dist/store/provider.d.ts +4 -0
- package/dist/tag/TagDurableObject.js +66 -14
- package/dist/tag/TagSqlSchema.d.ts +6 -1
- package/dist/tag/TagSqlSchema.js +15 -0
- package/dist/tag/types.d.ts +9 -0
- package/dist/trace/CommitTrace.d.ts +3 -3
- package/dist/trace/CommitTraceVerifier.js +1 -1
- package/package.json +6 -15
package/README.md
CHANGED
|
@@ -5,9 +5,13 @@ Cloudflare Durable Object and HTTP runtime for Serialized DCB V1.
|
|
|
5
5
|
This package is the matched-set runtime companion to `@sekiban/dcb-core`,
|
|
6
6
|
`@sekiban/dcb-domain`, and `@sekiban/dcb-client`. Consumers such as the
|
|
7
7
|
meeting-room Cloudflare sample import public entry points (`@sekiban/dcb-runtime`,
|
|
8
|
-
`@sekiban/dcb-runtime/cloudflare`, `/d1`, `/d1-mv`, `/mv`, `/cosmos`) and re-export
|
|
8
|
+
`@sekiban/dcb-runtime/cloudflare`, `/d1`, `/d1-mv`, `/mv`, and the experimental `/cosmos`) and re-export
|
|
9
9
|
Durable Object classes from their Worker module graph as Cloudflare requires.
|
|
10
10
|
|
|
11
|
+
The experimental Cosmos provider is never selected by environment values. A
|
|
12
|
+
consumer must import `createCosmosStoreProvider` from `/cosmos` and pass it as
|
|
13
|
+
`createRuntimeWorker({ storeProvider })`.
|
|
14
|
+
|
|
11
15
|
## License
|
|
12
16
|
|
|
13
17
|
Elastic License 2.0. See [LICENSE](./LICENSE).
|
|
@@ -10,6 +10,8 @@ const ATTEMPT_KEY_PREFIX = "attempt:";
|
|
|
10
10
|
const ROLLBACK_WARNING_WINDOW_MS = 1000n;
|
|
11
11
|
class AllocationTransactionFault extends Error {
|
|
12
12
|
}
|
|
13
|
+
class ImportSeedRequiredError extends Error {
|
|
14
|
+
}
|
|
13
15
|
function json(body, status = 200) {
|
|
14
16
|
return new Response(JSON.stringify(body), {
|
|
15
17
|
status,
|
|
@@ -55,7 +57,19 @@ function seedFrom(value) {
|
|
|
55
57
|
catch {
|
|
56
58
|
return { error: "highWatermark must be an allocator SUID" };
|
|
57
59
|
}
|
|
58
|
-
|
|
60
|
+
let storeMaximum;
|
|
61
|
+
if (value.storeMaximum !== undefined) {
|
|
62
|
+
if (!isNonEmptyString(value.storeMaximum))
|
|
63
|
+
return { error: "storeMaximum must be an allocator SUID" };
|
|
64
|
+
try {
|
|
65
|
+
decodeSuid(value.storeMaximum);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return { error: "storeMaximum must be an allocator SUID" };
|
|
69
|
+
}
|
|
70
|
+
storeMaximum = value.storeMaximum;
|
|
71
|
+
}
|
|
72
|
+
return { value: { importId: value.importId, leaseEpoch: value.leaseEpoch, highWatermark: value.highWatermark, ...(storeMaximum !== undefined ? { storeMaximum } : {}) } };
|
|
59
73
|
}
|
|
60
74
|
function newAllocatorLineageId() {
|
|
61
75
|
// A fresh token is generated when the allocator has no durable state, so a
|
|
@@ -118,6 +132,7 @@ function allocateFrom(value) {
|
|
|
118
132
|
...(serviceId !== undefined ? { serviceId } : {}),
|
|
119
133
|
...(bootstrapCommandId !== undefined ? { bootstrapCommandId } : {}),
|
|
120
134
|
...(bootstrapEpoch !== undefined ? { bootstrapEpoch } : {}),
|
|
135
|
+
...(value.requiresImportSeed === true ? { requiresImportSeed: true } : {}),
|
|
121
136
|
},
|
|
122
137
|
};
|
|
123
138
|
}
|
|
@@ -519,6 +534,8 @@ export class AllocatorDurableObject {
|
|
|
519
534
|
const state = persistedState === undefined
|
|
520
535
|
? currentState(lineage)
|
|
521
536
|
: { ...persistedState, allocatorLineageId: lineage, schemaVersion: 5, bootstrapSeed: persistedState.bootstrapSeed ?? null, lastRollbackWarningFingerprint: persistedState.lastRollbackWarningFingerprint ?? null };
|
|
537
|
+
if (input.requiresImportSeed === true && state.bootstrapSeed === null)
|
|
538
|
+
throw new ImportSeedRequiredError();
|
|
522
539
|
// This is intentionally before the first transaction write. A clock
|
|
523
540
|
// failure therefore cannot leave a vector, watermark, or warning fact.
|
|
524
541
|
let clockTick;
|
|
@@ -609,6 +626,9 @@ export class AllocatorDurableObject {
|
|
|
609
626
|
return json(result.vector, result.created ? 201 : 200);
|
|
610
627
|
}
|
|
611
628
|
catch (failure) {
|
|
629
|
+
if (failure instanceof ImportSeedRequiredError) {
|
|
630
|
+
return error(409, "allocator_seed_required", "commit before import seed");
|
|
631
|
+
}
|
|
612
632
|
if (failure instanceof AllocationTransactionFault) {
|
|
613
633
|
return error(503, "simulated_allocation_crash", "Simulated interruption before durable allocation transaction commit");
|
|
614
634
|
}
|
|
@@ -649,7 +669,10 @@ export class AllocatorDurableObject {
|
|
|
649
669
|
}
|
|
650
670
|
if (state.allocatedWatermark !== null)
|
|
651
671
|
return { status: 409, body: { code: "allocator_seed_rejected", error: "allocator already allocating" } };
|
|
652
|
-
|
|
672
|
+
if (input.storeMaximum !== undefined && input.highWatermark < input.storeMaximum) {
|
|
673
|
+
return { status: 409, body: { code: "allocator_seed_below_store_max", error: "seed below store maximum" } };
|
|
674
|
+
}
|
|
675
|
+
const updated = { ...state, allocatedWatermark: input.highWatermark, bootstrapSeed: { importId: input.importId, leaseEpoch: input.leaseEpoch, highWatermark: input.highWatermark } };
|
|
653
676
|
await txn.put(STATE_KEY, updated);
|
|
654
677
|
return { status: 201, body: updated };
|
|
655
678
|
});
|
|
@@ -23,6 +23,8 @@ export declare class BootstrapCoordinatorDurableObject implements DurableObject
|
|
|
23
23
|
/** Operator abort retains the durable failed plan for audit/retry; it never clears target state. */
|
|
24
24
|
private abort;
|
|
25
25
|
private route;
|
|
26
|
+
private permit;
|
|
27
|
+
private permitRelease;
|
|
26
28
|
private command;
|
|
27
29
|
}
|
|
28
30
|
export {};
|
|
@@ -13,13 +13,22 @@ const epoch = (value) => typeof value === "number" && Number.isSafeInteger(value
|
|
|
13
13
|
const response = (body, status = 200) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" } });
|
|
14
14
|
const reject = (code, message, status = 409) => response({ code, error: message }, status);
|
|
15
15
|
function empty(serviceId) {
|
|
16
|
-
return { schemaVersion: 1, status: "EMPTY", importId: null, targetServiceId: serviceId, allocatorLineageId: null, source: null, digest: null, manifest: null, progress: {}, leaseEpoch: 0, leaseUntil: null, failure: null, readyAt: null, normalInFlight: 0, normalCommands: {}, releasedCommands: {}, verifiedImportId: null, verifiedLeaseEpoch: null, storeCompletion: null };
|
|
16
|
+
return { schemaVersion: 1, status: "EMPTY", importId: null, targetServiceId: serviceId, allocatorLineageId: null, source: null, digest: null, manifest: null, progress: {}, leaseEpoch: 0, leaseUntil: null, failure: null, readyAt: null, normalInFlight: 0, normalCommands: {}, releasedCommands: {}, writePermits: {}, verifiedImportId: null, verifiedLeaseEpoch: null, storeCompletion: null };
|
|
17
17
|
}
|
|
18
18
|
function expired(control) { return control.leaseUntil !== null && control.leaseUntil <= Date.now(); }
|
|
19
19
|
function commands(control) { return { ...(control.normalCommands ?? {}) }; }
|
|
20
20
|
function released(control) { return { ...(control.releasedCommands ?? {}) }; }
|
|
21
|
+
function permits(control) { return { ...(control.writePermits ?? {}) }; }
|
|
21
22
|
function fault(body, point) { return object(body) && body.faultAt === point; }
|
|
22
23
|
function encodedBytes(value) { return new TextEncoder().encode(JSON.stringify(value)).byteLength; }
|
|
24
|
+
async function readBody(request) {
|
|
25
|
+
try {
|
|
26
|
+
return await request.json();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
23
32
|
/** Per-service authority and linearization point for bootstrap and normal writes. */
|
|
24
33
|
export class BootstrapCoordinatorDurableObject {
|
|
25
34
|
ctx;
|
|
@@ -48,6 +57,10 @@ export class BootstrapCoordinatorDurableObject {
|
|
|
48
57
|
return this.traceCommand(request, serviceId, activation, observation, "finalize");
|
|
49
58
|
if (request.method === "POST" && url.pathname === "/command/release")
|
|
50
59
|
return this.traceCommand(request, serviceId, activation, observation, "release");
|
|
60
|
+
if (request.method === "POST" && url.pathname === "/command/permit")
|
|
61
|
+
return this.permit(serviceId, await readBody(request));
|
|
62
|
+
if (request.method === "POST" && url.pathname === "/command/permit-release")
|
|
63
|
+
return this.permitRelease(serviceId, await readBody(request));
|
|
51
64
|
let body;
|
|
52
65
|
try {
|
|
53
66
|
body = await request.json();
|
|
@@ -119,6 +132,8 @@ export class BootstrapCoordinatorDurableObject {
|
|
|
119
132
|
return { error: "bootstrap_ready_permanent" };
|
|
120
133
|
if (control.status !== "EMPTY")
|
|
121
134
|
return control.importId === importId && control.digest === dump.manifest.contentDigest ? { control } : { error: "bootstrap_plan_conflict" };
|
|
135
|
+
if (Object.keys(permits(control)).length !== 0)
|
|
136
|
+
return { error: "bootstrap_write_permit_active" };
|
|
122
137
|
if (control.normalInFlight !== 0 || Object.keys(commands(control)).length !== 0)
|
|
123
138
|
return { error: "bootstrap_normal_command_in_flight" };
|
|
124
139
|
const next = { ...control, status: "PLANNED", importId, allocatorLineageId: dump.manifest.target.allocatorLineageId, source: dump.manifest.source, digest: dump.manifest.contentDigest, manifest: dump.manifest, leaseEpoch: control.leaseEpoch + 1, leaseUntil: Date.now() + 30_000, failure: null, progress: {}, verifiedImportId: null, verifiedLeaseEpoch: null, storeCompletion: null };
|
|
@@ -315,6 +330,54 @@ export class BootstrapCoordinatorDurableObject {
|
|
|
315
330
|
const control = await this.control(serviceId);
|
|
316
331
|
return control.status === "EMPTY" || control.status === "READY" ? response({ admitted: true, leaseEpoch: control.leaseEpoch, route: body.route }) : reject("bootstrap_route_rejected", `${body.route} is unavailable during bootstrap`);
|
|
317
332
|
}
|
|
333
|
+
async permit(serviceId, body) {
|
|
334
|
+
if (!object(body) || !string(body.commandId) || !string(body.digest))
|
|
335
|
+
return reject("bootstrap_permit_invalid", "commandId and digest are required", 400);
|
|
336
|
+
const commandId = body.commandId;
|
|
337
|
+
const digest = body.digest;
|
|
338
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
339
|
+
const control = (await txn.get(CONTROL)) ?? empty(serviceId);
|
|
340
|
+
const active = permits(control);
|
|
341
|
+
if (active[commandId] !== undefined) {
|
|
342
|
+
return active[commandId] === digest
|
|
343
|
+
? { control }
|
|
344
|
+
: { digestMismatch: true };
|
|
345
|
+
}
|
|
346
|
+
if (control.status !== "EMPTY" && control.status !== "READY")
|
|
347
|
+
return { rejected: true };
|
|
348
|
+
const updated = { ...control, writePermits: { ...active, [commandId]: digest } };
|
|
349
|
+
await txn.put(CONTROL, updated);
|
|
350
|
+
return { control: updated };
|
|
351
|
+
});
|
|
352
|
+
if ("digestMismatch" in result)
|
|
353
|
+
return reject("bootstrap_permit_digest_mismatch", "bootstrap permit digest does not match the held permit");
|
|
354
|
+
if ("rejected" in result)
|
|
355
|
+
return reject("bootstrap_command_rejected", "bootstrap epoch rejects normal durable mutation");
|
|
356
|
+
return response({ leaseEpoch: result.control.leaseEpoch, admitted: true });
|
|
357
|
+
}
|
|
358
|
+
async permitRelease(serviceId, body) {
|
|
359
|
+
if (!object(body) || !string(body.commandId))
|
|
360
|
+
return reject("bootstrap_permit_invalid", "commandId is required", 400);
|
|
361
|
+
const commandId = body.commandId;
|
|
362
|
+
const digest = string(body.digest) ? body.digest : undefined;
|
|
363
|
+
const result = await this.ctx.storage.transaction(async (txn) => {
|
|
364
|
+
const control = (await txn.get(CONTROL)) ?? empty(serviceId);
|
|
365
|
+
const active = permits(control);
|
|
366
|
+
if (active[commandId] !== undefined && digest !== undefined && active[commandId] !== digest) {
|
|
367
|
+
return { digestMismatch: true };
|
|
368
|
+
}
|
|
369
|
+
if (active[commandId] === undefined)
|
|
370
|
+
return { control };
|
|
371
|
+
const next = { ...active };
|
|
372
|
+
delete next[commandId];
|
|
373
|
+
const updated = { ...control, writePermits: next };
|
|
374
|
+
await txn.put(CONTROL, updated);
|
|
375
|
+
return { control: updated };
|
|
376
|
+
});
|
|
377
|
+
if ("digestMismatch" in result)
|
|
378
|
+
return reject("bootstrap_permit_digest_mismatch", "bootstrap permit digest does not match the held permit");
|
|
379
|
+
return response({ leaseEpoch: result.control.leaseEpoch, admitted: true });
|
|
380
|
+
}
|
|
318
381
|
async command(serviceId, body, action, observation) {
|
|
319
382
|
if (!object(body) || !string(body.commandId))
|
|
320
383
|
return reject("bootstrap_command_invalid", "commandId is required", 400);
|
|
@@ -55,6 +55,12 @@ export interface BootstrapControlRecord {
|
|
|
55
55
|
readonly normalCommands?: Readonly<Record<string, number>>;
|
|
56
56
|
/** Recently released entry admissions, retained only for their final epoch check. */
|
|
57
57
|
readonly releasedCommands?: Readonly<Record<string, number>>;
|
|
58
|
+
/**
|
|
59
|
+
* SDT-G36 durable write permits. Held from before allocation until every
|
|
60
|
+
* target tag of that attempt is durably resolved. Absent on old records
|
|
61
|
+
* (treated as `{}`). Only a bootstrap barrier, never commit authority.
|
|
62
|
+
*/
|
|
63
|
+
readonly writePermits?: Readonly<Record<string, string>>;
|
|
58
64
|
/** READY is a post-verification state, never merely an import completion. */
|
|
59
65
|
readonly verifiedImportId?: string | null;
|
|
60
66
|
readonly verifiedLeaseEpoch?: number | null;
|
package/dist/cloudflare.d.ts
CHANGED
|
@@ -42,6 +42,7 @@ export interface CloudflareOnlyEnv {
|
|
|
42
42
|
D1_MV: D1Database;
|
|
43
43
|
AUTO_DRAIN_OUTBOX?: string;
|
|
44
44
|
REPAIR_OPERATOR_TOKEN: string;
|
|
45
|
+
INCIDENT_MAINTAINER_TOKEN?: string;
|
|
45
46
|
REPAIR_EXCLUSION_LOOKUP?: Fetcher;
|
|
46
47
|
G11_VERIFICATION_ENABLED?: string;
|
|
47
48
|
SDT_SERVICE_ID?: string;
|
|
@@ -152,6 +153,8 @@ export type { ClosedPrefixCertificate } from "./allocator/types";
|
|
|
152
153
|
export type { SafeViewCoverageContext } from "./projection/ProjectionRuntime";
|
|
153
154
|
export { downstreamEnvelopeBytes, classifyDirectDoorbellFailure, preflightDirectDoorbell, readDomainDeliveryClass, readDirectDoorbellConfig, selectDirectDoorbellViews, MAX_SERVICE_BINDING_INVOCATIONS_PER_REQUEST, } from "./downstream/Doorbell";
|
|
154
155
|
export { deliveryCorrelationId } from "./downstream/DeliveryCore";
|
|
156
|
+
export { admitActiveWrite, assertNoSilentLedgerMerge, assertStrictlyBefore, createRotationState, maxSortableUniqueId, recordLateArrival, refuseSafeWindowSeal, refuseSealedWrite, refuseSilentLedgerMerge, replaceSealedIdentity, rotateAppend, ShardRotationError, } from "./shard/ShardRotation";
|
|
157
|
+
export type { LateArrival, SealedShard, ShardRotationDiagnostic, ShardRotationState, } from "./shard/ShardRotation";
|
|
155
158
|
export { GlobalCompletenessReconciler } from "./completeness/GlobalCompletenessReconciler";
|
|
156
159
|
export { G44_HEALTH_STALE_AFTER_MS, G44_SCANNER_VERSION, GLOBAL_COMPLETENESS_INTERIM_DISPOSITION, } from "./completeness/types";
|
|
157
160
|
export type { GlobalCompletenessHealth, GlobalCompletenessCoverage, GlobalCompletenessHealthRecord, GlobalCompletenessScanResult, SourceObligationFact, SourceObligationPage, SourcePartitionSnapshot, } from "./completeness/types";
|