@sekiban/create-dcb 0.0.0-stage → 0.1.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 (52) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +18 -2
  3. package/bin/create-dcb.mjs +91 -0
  4. package/package.json +17 -4
  5. package/template/AGENTS.md +23 -0
  6. package/template/LICENSE +90 -0
  7. package/template/README.md +76 -0
  8. package/template/REPLACE.md +34 -0
  9. package/template/cloudflare.config.ts +13 -0
  10. package/template/migrations/d1/g32/0001_dcb_events.sql +119 -0
  11. package/template/migrations/d1/g32/0002_g44_global_completeness.sql +88 -0
  12. package/template/migrations/d1/g32/0003_g58_safe_lane_health.sql +12 -0
  13. package/template/migrations/d1/g32/0004_g58_live_poll_health.sql +18 -0
  14. package/template/migrations/d1/g32/0005_g58_safe_lane_history.sql +18 -0
  15. package/template/migrations/d1/g32/0006_g60_durable_hop_measurements.sql +31 -0
  16. package/template/migrations/d1/g32/0007_g60_post_admission_decomposition.sql +30 -0
  17. package/template/migrations/d1/g32/0008_g60_unsafe_writer_boundaries.sql +24 -0
  18. package/template/migrations/d1/g32/0009_g65_admission_attempts.sql +23 -0
  19. package/template/migrations/d1/g32/0010_g65_direct_rings.sql +26 -0
  20. package/template/migrations/d1/g32/0011_g67_safe_lane_passes.sql +25 -0
  21. package/template/migrations/d1/g32/0012_g67_safe_lane_pass_ownership.sql +20 -0
  22. package/template/migrations/d1/g32/0013_g67_safe_lane_catch_up_observations.sql +6 -0
  23. package/template/migrations/d1/g32/0014_g67_safe_lane_fence_expiry.sql +6 -0
  24. package/template/migrations/d1/g32/0015_g69_admission_attempts.sql +33 -0
  25. package/template/migrations/d1/g32/0016_g69_consultation003.sql +12 -0
  26. package/template/migrations/d1/g32/0017_g69_receipt_identity.sql +61 -0
  27. package/template/migrations/d1/g32/0018_g69_mutation_evidence.sql +5 -0
  28. package/template/migrations/d1/g32/0019_g69_mutation_receipt_label.sql +6 -0
  29. package/template/migrations/d1/g32/0020_g69_hotpath_costs.sql +6 -0
  30. package/template/migrations/mv/0001_materialized_views.sql +87 -0
  31. package/template/migrations/mv/0002_unsafe_window_materialized_views.sql +58 -0
  32. package/template/migrations/mv/0003_checkpoint_ahead_hardening.sql +14 -0
  33. package/template/migrations/mv/0004_unsafe_window_failure_findings.sql +12 -0
  34. package/template/migrations/mv/0005_g31_wait_receipts.sql +25 -0
  35. package/template/migrations/mv/0006_g31_wait_target_poison.sql +48 -0
  36. package/template/migrations/mv/0007_g69_ordering_quarantine.sql +21 -0
  37. package/template/migrations/mv/0008_g69_rebuild_verification.sql +4 -0
  38. package/template/migrations/mv/0009_g69_rebuild_proof.sql +11 -0
  39. package/template/package.json +24 -0
  40. package/template/public/.assetsignore +2 -0
  41. package/template/public/app.js +40 -0
  42. package/template/public/index.html +58 -0
  43. package/template/public/styles.css +20 -0
  44. package/template/scripts/deploy.sh +5 -0
  45. package/template/scripts/migrate.sh +5 -0
  46. package/template/src/booking-domain.ts +285 -0
  47. package/template/src/booking-mv.ts +166 -0
  48. package/template/src/booking-routes.ts +160 -0
  49. package/template/src/booking-transport.ts +169 -0
  50. package/template/src/worker.ts +38 -0
  51. package/template/tsconfig.json +13 -0
  52. package/template/wrangler.jsonc +55 -0
@@ -0,0 +1,160 @@
1
+ import { ClientError, createSekibanExecutor, type ExecuteResult } from "@sekiban/dcb-client";
2
+ import {
3
+ createV1Transport,
4
+ executeBookingCommand,
5
+ parseBookingCommandRequest,
6
+ type InternalRuntimeFetcher,
7
+ } from "./booking-transport";
8
+ import { reservationTag, roomTag } from "./booking-domain";
9
+ import type { CloudflareOnlyEnv } from "@sekiban/dcb-runtime/cloudflare";
10
+
11
+ export interface StarterEnvironment extends CloudflareOnlyEnv {
12
+ readonly ASSETS?: Fetcher;
13
+ readonly SDT_SERVICE_ID: string;
14
+ }
15
+
16
+ export interface StarterRuntime {
17
+ readonly fetch: (request: Request, env: StarterEnvironment, ctx: ExecutionContext) => Promise<Response>;
18
+ }
19
+
20
+ function json(body: unknown, status = 200): Response {
21
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json; charset=utf-8" } });
22
+ }
23
+
24
+ function runtimeFetcher(runtime: StarterRuntime, env: StarterEnvironment, ctx: ExecutionContext): InternalRuntimeFetcher {
25
+ return {
26
+ fetch: (input, init) => runtime.fetch(new Request(input, init), env, ctx),
27
+ };
28
+ }
29
+
30
+ function resultBody(result: ExecuteResult): Record<string, unknown> {
31
+ const body = { ...result } as Record<string, unknown>;
32
+ delete body.cause;
33
+ return body;
34
+ }
35
+
36
+ function resultResponse(result: ExecuteResult): Response {
37
+ const body = resultBody(result);
38
+ switch (result.kind) {
39
+ case "committed":
40
+ case "noop":
41
+ return json(body);
42
+ case "conflict":
43
+ return json({ error: result.error ?? "Command conflicted", code: result.code ?? result.kind, ...body }, 409);
44
+ case "rejected":
45
+ case "invalid":
46
+ return json({ error: result.error ?? "Command was rejected", code: result.code ?? result.kind, ...body }, 400);
47
+ case "partial":
48
+ return json({ error: result.error ?? "Commit was partial", code: result.code ?? result.kind, ...body }, 500);
49
+ case "timeout":
50
+ return json({ error: result.error ?? "Command outcome is undetermined", code: result.code ?? result.kind, ...body }, 504);
51
+ case "unavailable":
52
+ return json({ error: result.error ?? "Projection is unavailable", code: result.code ?? result.kind, ...body }, 503);
53
+ case "transport":
54
+ return json({ error: result.error ?? "Command transport failed", code: result.code ?? result.kind, ...body }, 502);
55
+ }
56
+ }
57
+
58
+ async function command(request: Request, env: StarterEnvironment, ctx: ExecutionContext, runtime: StarterRuntime): Promise<Response> {
59
+ if (request.method !== "POST") return json({ error: "Command route requires POST", code: "validation_error" }, 405);
60
+ const commandId = new URL(request.url).pathname.slice("/api/commands/".length);
61
+ let body: unknown;
62
+ try {
63
+ body = await request.json();
64
+ } catch {
65
+ return json({ error: "Command request must be JSON", code: "validation_error" }, 400);
66
+ }
67
+ try {
68
+ const parsed = parseBookingCommandRequest(body);
69
+ const result = await executeBookingCommand(commandId, parsed.input, {
70
+ RUNTIME: runtimeFetcher(runtime, env, ctx),
71
+ serviceId: env.SDT_SERVICE_ID,
72
+ }, parsed.options);
73
+ return resultResponse(result);
74
+ } catch (error) {
75
+ return json({ error: error instanceof Error ? error.message : "Command request was invalid", code: "validation_error" }, 400);
76
+ }
77
+ }
78
+
79
+ function decodePayload(value: unknown): unknown {
80
+ if (typeof value !== "string") return value;
81
+ try {
82
+ const binary = atob(value);
83
+ const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
84
+ return JSON.parse(new TextDecoder().decode(bytes));
85
+ } catch {
86
+ try { return JSON.parse(value); } catch { return value; }
87
+ }
88
+ }
89
+
90
+ async function readProjection(request: Request, env: StarterEnvironment, ctx: ExecutionContext, runtime: StarterRuntime): Promise<Response> {
91
+ if (request.method !== "GET") return json({ error: "Projection routes require GET", code: "validation_error" }, 405);
92
+ const url = new URL(request.url);
93
+ const isRoom = url.pathname === "/api/read/room";
94
+ const isReservation = url.pathname === "/api/read/reservation";
95
+ const parameter = isRoom ? "roomId" : "reservationId";
96
+ const value = url.searchParams.get(parameter);
97
+ if (!isRoom && !isReservation) return json({ error: "Projection route was not found", code: "not_found" }, 404);
98
+ if (value === null || value.length === 0) return json({ error: `${parameter} is required`, code: "validation_error" }, 400);
99
+ const tag = isRoom ? roomTag(value) : reservationTag(value);
100
+ const projector = isRoom ? "RoomProjector" : "ReservationProjector";
101
+ const response = await runtime.fetch(new Request("https://runtime.internal/api/sekiban/serialized/tag-state", {
102
+ method: "POST",
103
+ headers: { "content-type": "application/json", accept: "application/json" },
104
+ body: JSON.stringify({ tagStateId: `${tag.id}:${projector}` }),
105
+ }), env, ctx);
106
+ let body: unknown;
107
+ try { body = await response.json(); } catch { return json({ error: "Projection read returned invalid JSON", code: "transport" }, 502); }
108
+ if (!response.ok) return json(body, response.status);
109
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return json({ error: "Projection read returned an invalid body", code: "transport" }, 502);
110
+ const record = body as Record<string, unknown>;
111
+ return json({ projection: isRoom ? "room" : "reservation", [parameter]: value, tagStateId: `${tag.id}:${projector}`, state: decodePayload(record.payload), version: record.version, lastSortableUniqueId: record.lastSortableUniqueId });
112
+ }
113
+
114
+ function positiveInteger(value: string | null, fallback: number): number | Response {
115
+ if (value === null) return fallback;
116
+ const parsed = Number(value);
117
+ if (!Number.isSafeInteger(parsed) || parsed < 1) return json({ error: "page values must be positive integers", code: "validation_error" }, 400);
118
+ return parsed;
119
+ }
120
+
121
+ async function readQuery(request: Request, env: StarterEnvironment, ctx: ExecutionContext, runtime: StarterRuntime): Promise<Response> {
122
+ if (request.method !== "GET") return json({ error: "Query routes require GET", code: "validation_error" }, 405);
123
+ const url = new URL(request.url);
124
+ const reservations = url.pathname === "/api/read/reservations";
125
+ const rooms = url.pathname === "/api/read/room-query";
126
+ if (!reservations && !rooms) return json({ error: "Query route was not found", code: "not_found" }, 404);
127
+ const transport = createV1Transport(runtimeFetcher(runtime, env, ctx), env.SDT_SERVICE_ID);
128
+ const executor = createSekibanExecutor(transport, { serviceId: env.SDT_SERVICE_ID });
129
+ try {
130
+ if (reservations) {
131
+ const pageNumber = positiveInteger(url.searchParams.get("pageNumber"), 1);
132
+ const pageSize = positiveInteger(url.searchParams.get("pageSize"), 20);
133
+ if (pageNumber instanceof Response) return pageNumber;
134
+ if (pageSize instanceof Response) return pageSize;
135
+ const result = await executor.listQuery({
136
+ queryType: "GetReservationListQuery",
137
+ queryParamsJson: JSON.stringify({ PageNumber: pageNumber, PageSize: pageSize, NewestFirst: url.searchParams.get("newestFirst") === "true" }),
138
+ }, { consistency: "unsafe" });
139
+ return json(result);
140
+ }
141
+ const result = await executor.query({
142
+ queryType: "GetRoomStateQuery",
143
+ queryParamsJson: JSON.stringify({ roomId: url.searchParams.get("roomId") ?? undefined }),
144
+ });
145
+ return json(result);
146
+ } catch (error) {
147
+ if (error instanceof ClientError) return json({ error: error.message, code: error.code }, error.status ?? 502);
148
+ return json({ error: "Query read failed", code: "transport" }, 502);
149
+ }
150
+ }
151
+
152
+ export async function applicationFetch(request: Request, env: StarterEnvironment, ctx: ExecutionContext, runtime: StarterRuntime): Promise<Response> {
153
+ const path = new URL(request.url).pathname;
154
+ if (path.startsWith("/api/commands/")) return command(request, env, ctx, runtime);
155
+ if (path === "/api/read/room" || path === "/api/read/reservation") return readProjection(request, env, ctx, runtime);
156
+ if (path === "/api/read/reservations" || path === "/api/read/room-query") return readQuery(request, env, ctx, runtime);
157
+ if (env.ASSETS !== undefined) return env.ASSETS.fetch(request);
158
+ if (path === "/" || path === "/index.html") return new Response("Booking starter", { headers: { "content-type": "text/plain; charset=utf-8" } });
159
+ return new Response("Not found", { status: 404 });
160
+ }
@@ -0,0 +1,169 @@
1
+ import {
2
+ createInProcessTransport,
3
+ createSekibanExecutor,
4
+ type CommitEnvelope,
5
+ type CommitHttpResult,
6
+ type ExecuteCommandOptions,
7
+ type ExecuteCommandResult,
8
+ type ReadonlyTagStateResponse,
9
+ type SerializedDcbTransport,
10
+ } from "@sekiban/dcb-client";
11
+ import { normalizeTag, type PortableSnapshot } from "@sekiban/dcb-domain";
12
+ import { assertJsonValue } from "@sekiban/dcb-core";
13
+ import {
14
+ bookingCommands,
15
+ cancelReservationCommand,
16
+ createRoomCommand,
17
+ releaseRoomCommand,
18
+ reserveRoomCommand,
19
+ } from "./booking-domain";
20
+
21
+ export interface InternalRuntimeFetcher {
22
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
23
+ }
24
+
25
+ export interface BookingCommandEnvironment {
26
+ readonly RUNTIME: InternalRuntimeFetcher;
27
+ readonly serviceId: string;
28
+ }
29
+
30
+ export interface BookingCommandRequest {
31
+ readonly input: unknown;
32
+ readonly options?: ExecuteCommandOptions;
33
+ }
34
+
35
+ function jsonBytes(value: unknown): string {
36
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
37
+ let binary = "";
38
+ for (const byte of bytes) binary += String.fromCharCode(byte);
39
+ return btoa(binary);
40
+ }
41
+
42
+ function v1ConsistencyEntry(entry: { readonly tag: string; readonly lastSortableUniqueId: string }): { readonly tag: string; readonly lastSortableUniqueId: string } {
43
+ return { tag: entry.tag, lastSortableUniqueId: entry.lastSortableUniqueId };
44
+ }
45
+
46
+ async function readBody(response: Response): Promise<unknown> {
47
+ try {
48
+ return await response.json();
49
+ } catch {
50
+ return { error: `HTTP ${response.status}`, code: "transport" };
51
+ }
52
+ }
53
+
54
+ /** Small V1 transport used by the demo's query routes. */
55
+ export function createV1Transport(fetcher: InternalRuntimeFetcher, serviceId: string): SerializedDcbTransport {
56
+ const call = async (path: string, body: unknown, signal?: AbortSignal): Promise<CommitHttpResult> => {
57
+ const response = await fetcher.fetch(`https://runtime.internal${path}`, {
58
+ method: "POST",
59
+ headers: { "content-type": "application/json", accept: "application/json" },
60
+ body: JSON.stringify(body),
61
+ signal,
62
+ });
63
+ const headers: Record<string, string> = {};
64
+ response.headers.forEach((value, key) => { headers[key] = value; });
65
+ return { status: response.status, headers, body: await readBody(response) };
66
+ };
67
+ return {
68
+ serviceId,
69
+ async readTagState(request, signal) {
70
+ const result = await call("/api/sekiban/serialized/tag-state", request, signal);
71
+ return result.status >= 200 && result.status < 300 ? result.body as ReadonlyTagStateResponse : result;
72
+ },
73
+ async readTagLatestSortable(request, signal) {
74
+ const result = await call("/api/sekiban/serialized/tag-latest-sortable", request, signal);
75
+ return result.status >= 200 && result.status < 300
76
+ ? result.body as { readonly exists: boolean; readonly lastSortableUniqueId: string }
77
+ : result;
78
+ },
79
+ async commit(request: CommitEnvelope, signal) {
80
+ return call("/api/sekiban/serialized/commit", {
81
+ version: 1,
82
+ eventCandidates: request.candidates.map((candidate) => ({
83
+ payload: jsonBytes(candidate.payload),
84
+ eventPayloadName: candidate.eventPayloadName,
85
+ tags: [...candidate.tags],
86
+ })),
87
+ consistencyTags: request.consistency.map(v1ConsistencyEntry),
88
+ }, signal);
89
+ },
90
+ async query(request, signal) {
91
+ const result = await call("/api/sekiban/serialized/query", request, signal);
92
+ return result.status >= 200 && result.status < 300 ? result.body as { readonly resultJson: string } : result;
93
+ },
94
+ async listQuery(request, signal) {
95
+ const result = await call("/api/sekiban/serialized/list-query", request, signal);
96
+ return result.status >= 200 && result.status < 300 ? result.body as {
97
+ readonly itemsJson: string;
98
+ readonly totalCount: number;
99
+ readonly totalPages: number;
100
+ readonly currentPage: number;
101
+ readonly pageSize: number;
102
+ } : result;
103
+ },
104
+ };
105
+ }
106
+
107
+ function isRecord(value: unknown): value is Record<string, unknown> {
108
+ return typeof value === "object" && value !== null && !Array.isArray(value);
109
+ }
110
+
111
+ function portableSnapshot(value: unknown): PortableSnapshot {
112
+ if (!isRecord(value) || typeof value.projectorId !== "string" || typeof value.tag !== "string" ||
113
+ (value.head !== null && typeof value.head !== "string") || typeof value.exists !== "boolean" || !("state" in value)) {
114
+ throw new Error("Portable snapshot fields are invalid");
115
+ }
116
+ return Object.freeze({
117
+ projectorId: value.projectorId,
118
+ tag: normalizeTag(value.tag),
119
+ head: value.head as string | null,
120
+ state: assertJsonValue(value.state, "state-persistence"),
121
+ exists: value.exists,
122
+ });
123
+ }
124
+
125
+ export function parseBookingCommandRequest(body: unknown): BookingCommandRequest {
126
+ if (!isRecord(body) || !("input" in body)) return { input: body };
127
+ const executor = body.executor;
128
+ if (executor === undefined) return { input: body.input };
129
+ if (!isRecord(executor)) throw new Error("executor must be an object");
130
+ const readMode = executor.readMode;
131
+ if (readMode !== undefined && readMode !== "read-through" && readMode !== "snapshot-only") {
132
+ throw new Error("Executor readMode must be read-through or snapshot-only");
133
+ }
134
+ const rawSnapshots = executor.snapshots;
135
+ if (rawSnapshots !== undefined && !Array.isArray(rawSnapshots)) throw new Error("Executor snapshots must be an array");
136
+ const snapshots = rawSnapshots === undefined ? undefined : rawSnapshots.map(portableSnapshot);
137
+ return {
138
+ input: body.input,
139
+ options: {
140
+ ...(snapshots === undefined ? {} : { snapshots }),
141
+ ...(readMode === undefined ? {} : { readMode }),
142
+ },
143
+ };
144
+ }
145
+
146
+ function commandFor(commandId: string) {
147
+ switch (commandId) {
148
+ case "create-room": return createRoomCommand;
149
+ case "reserve-room": return reserveRoomCommand;
150
+ case "cancel-reservation": return cancelReservationCommand;
151
+ case "release-room": return releaseRoomCommand;
152
+ default: return undefined;
153
+ }
154
+ }
155
+
156
+ export async function executeBookingCommand(
157
+ commandId: string,
158
+ input: unknown,
159
+ environment: BookingCommandEnvironment,
160
+ options?: ExecuteCommandOptions,
161
+ ): Promise<ExecuteCommandResult> {
162
+ const command = commandFor(commandId);
163
+ if (command === undefined || !(commandId in bookingCommands)) {
164
+ return { kind: "invalid", attempts: 0, error: "Unknown booking command", code: "command_not_found" } as ExecuteCommandResult;
165
+ }
166
+ const transport = createInProcessTransport({ RUNTIME: environment.RUNTIME }, { serviceId: environment.serviceId });
167
+ const executor = createSekibanExecutor(transport, { serviceId: environment.serviceId });
168
+ return executor.execute(command, input as never, options);
169
+ }
@@ -0,0 +1,38 @@
1
+ import { composeHandlers, requireHandlers } from "@sekiban/dcb-cloudflare";
2
+ import { createCloudflareOnlyRuntimeWorker } from "@sekiban/dcb-runtime/cloudflare";
3
+ import { applicationFetch, type StarterEnvironment } from "./booking-routes";
4
+ import { bookingDomain, bookingRuntimeConfig } from "./booking-domain";
5
+ import { bookingDeliveryViews, catchUpBookingViews, ensureBookingViews } from "./booking-mv";
6
+
7
+ export {
8
+ AllocatorDurableObject,
9
+ BootstrapCoordinatorDurableObject,
10
+ JournalDurableObject,
11
+ TagDurableObject,
12
+ TagStateDurableObject,
13
+ } from "@sekiban/dcb-cloudflare";
14
+
15
+ const runtime = requireHandlers(createCloudflareOnlyRuntimeWorker({
16
+ domain: bookingDomain,
17
+ config: bookingRuntimeConfig,
18
+ afterBootstrapVerify: async ({ serviceId, env }) => {
19
+ await ensureBookingViews(env, serviceId);
20
+ await catchUpBookingViews(env, serviceId);
21
+ },
22
+ deliveryViews: ({ env }) => bookingDeliveryViews(env),
23
+ }));
24
+
25
+ const worker: ExportedHandler<StarterEnvironment> = composeHandlers<StarterEnvironment>({
26
+ application: {
27
+ fetch: (request, env, ctx) => applicationFetch(request, env, ctx, runtime),
28
+ },
29
+ sekiban: {
30
+ prefix: "/internal/sekiban",
31
+ fetch: runtime.fetch,
32
+ authorize: () => true,
33
+ queue: runtime.queue,
34
+ scheduled: runtime.scheduled,
35
+ },
36
+ });
37
+
38
+ export default worker;
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "WebWorker"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "types": ["@cloudflare/workers-types"]
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "$schema": "./node_modules/wrangler/config-schema.json",
3
+ "name": "{{WORKER_NAME}}",
4
+ "main": "src/worker.ts",
5
+ "compatibility_date": "2026-08-18",
6
+ "compatibility_flags": ["nodejs_compat"],
7
+ "vars": {
8
+ "DOMAIN_DELIVERY_CLASS": "immediate-preferred",
9
+ "SDT_SERVICE_ID": "{{SERVICE_ID}}"
10
+ },
11
+ "assets": { "directory": "public" },
12
+ "triggers": { "crons": ["* * * * *"] },
13
+ "d1_databases": [
14
+ {
15
+ "binding": "D1",
16
+ "database_name": "{{PIPELINE_DB}}",
17
+ "database_id": "REPLACE_WITH_PIPELINE_D1_ID",
18
+ "migrations_dir": "migrations/d1/g32"
19
+ },
20
+ {
21
+ "binding": "D1_MV",
22
+ "database_name": "{{MV_DB}}",
23
+ "database_id": "REPLACE_WITH_MV_D1_ID",
24
+ "migrations_dir": "migrations/mv"
25
+ }
26
+ ],
27
+ "queues": {
28
+ "producers": [{ "binding": "DOWNSTREAM_QUEUE", "queue": "{{QUEUE_NAME}}" }],
29
+ "consumers": [{
30
+ "queue": "{{QUEUE_NAME}}",
31
+ "max_batch_timeout": 1,
32
+ "max_retries": 3,
33
+ "dead_letter_queue": "{{DLQ_NAME}}"
34
+ }]
35
+ },
36
+ "durable_objects": {
37
+ "bindings": [
38
+ { "name": "ALLOCATOR", "class_name": "AllocatorDurableObject" },
39
+ { "name": "BOOTSTRAP", "class_name": "BootstrapCoordinatorDurableObject" },
40
+ { "name": "JOURNAL", "class_name": "JournalDurableObject" },
41
+ { "name": "TAG", "class_name": "TagDurableObject" },
42
+ { "name": "TAG_STATE", "class_name": "TagStateDurableObject" }
43
+ ]
44
+ },
45
+ "migrations": [{
46
+ "tag": "v1",
47
+ "new_sqlite_classes": ["AllocatorDurableObject", "JournalDurableObject", "TagDurableObject"]
48
+ }, {
49
+ "tag": "v2",
50
+ "new_sqlite_classes": ["BootstrapCoordinatorDurableObject"]
51
+ }, {
52
+ "tag": "v3",
53
+ "new_sqlite_classes": ["TagStateDurableObject"]
54
+ }]
55
+ }