@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.
Files changed (37) hide show
  1. package/README.md +5 -1
  2. package/dist/allocator/AllocatorDurableObject.js +25 -2
  3. package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +2 -0
  4. package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +64 -1
  5. package/dist/bootstrap/types.d.ts +6 -0
  6. package/dist/cloudflare.d.ts +3 -0
  7. package/dist/cloudflare.js +1493 -451
  8. package/dist/commit/CommitWorker.d.ts +19 -0
  9. package/dist/commit/CommitWorker.js +176 -27
  10. package/dist/completeness/IncidentLifecycle.d.ts +75 -0
  11. package/dist/completeness/IncidentLifecycle.js +503 -0
  12. package/dist/completeness/types.d.ts +67 -0
  13. package/dist/cosmos.d.ts +34 -4
  14. package/dist/cosmos.js +214 -49
  15. package/dist/downstream/OutboxDrain.d.ts +4 -1
  16. package/dist/downstream/OutboxDrain.js +24 -2
  17. package/dist/generated/cosmos-layout.d.ts +87 -0
  18. package/dist/generated/cosmos-layout.js +99 -0
  19. package/dist/http/IncidentMaintenance.d.ts +8 -0
  20. package/dist/http/IncidentMaintenance.js +145 -0
  21. package/dist/index.d.ts +11 -0
  22. package/dist/index.js +1548 -451
  23. package/dist/read/SerializedReadWorker.d.ts +3 -0
  24. package/dist/read/SerializedReadWorker.js +3 -3
  25. package/dist/shard/ShardRotation.d.ts +48 -0
  26. package/dist/shard/ShardRotation.js +120 -0
  27. package/dist/store/CosmosEventStore.js +56 -46
  28. package/dist/store/PostgresEventStore.d.ts +20 -0
  29. package/dist/store/PostgresEventStore.js +53 -0
  30. package/dist/store/provider.d.ts +4 -0
  31. package/dist/tag/TagDurableObject.js +66 -14
  32. package/dist/tag/TagSqlSchema.d.ts +6 -1
  33. package/dist/tag/TagSqlSchema.js +15 -0
  34. package/dist/tag/types.d.ts +9 -0
  35. package/dist/trace/CommitTrace.d.ts +3 -3
  36. package/dist/trace/CommitTraceVerifier.js +1 -1
  37. 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
- return { value: { importId: value.importId, leaseEpoch: value.leaseEpoch, highWatermark: value.highWatermark } };
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
- const updated = { ...state, allocatedWatermark: input.highWatermark, bootstrapSeed: input };
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;
@@ -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";