@voyantjs/workflows-orchestrator-cloudflare 0.6.7

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.
@@ -0,0 +1,298 @@
1
+ // Cloudflare Container-backed step runner for `runtime: "node"`.
2
+ //
3
+ // Given a Durable Object namespace that proxies a Cloudflare Container
4
+ // class (the `@cloudflare/containers` pattern), this factory returns a
5
+ // `StepRunner` the workflows handler can wire via its `nodeStepRunner`
6
+ // dep. The returned runner:
7
+ //
8
+ // 1. Serializes the step identity + input into a compact request.
9
+ // 2. Picks a container by deterministic id (`<runId>:<stepId>` so
10
+ // parallel step invocations land on distinct instances).
11
+ // 3. POSTs the request to `/step` on that container via `fetch`
12
+ // through the DO namespace binding.
13
+ // 4. Reads the container's response — which *is* a `StepJournalEntry`
14
+ // (status, output/error, timestamps) — and returns it verbatim.
15
+ //
16
+ // The container side is expected to:
17
+ // - import the tenant workflow bundle,
18
+ // - expose `POST /step` that accepts `{ workflowId, workflowVersion,
19
+ // stepId, attempt, input, envVars? }` and runs the step body,
20
+ // - return a `StepJournalEntry` JSON.
21
+ //
22
+ // The container entry point is a short adapter around
23
+ // `@voyantjs/workflows/handler`'s `executeWorkflowStep` with the
24
+ // workflow registry already loaded. See
25
+ // `apps/workflows-node-step-container/` for the reference image.
26
+
27
+ import type { StepJournalEntry } from "@voyantjs/workflows/handler";
28
+ import type { StepRunner } from "@voyantjs/workflows/handler";
29
+
30
+ /**
31
+ * Minimal subset of `DurableObjectNamespace` that the runner actually
32
+ * uses. Matches the shape exposed by `@cloudflare/containers`'
33
+ * `getContainer(namespace, id).fetch()` pattern — we keep it local so
34
+ * tests can pass a stub.
35
+ */
36
+ export interface ContainerNamespaceLike {
37
+ idFromName(name: string): { toString(): string };
38
+ get(id: { toString(): string }): { fetch(request: Request): Promise<Response> };
39
+ }
40
+
41
+ export interface BundleLocation {
42
+ /**
43
+ * Short-lived signed URL the container uses to fetch the bundle
44
+ * from R2. Expected TTL is minutes, not hours — scoped tightly to
45
+ * the specific `<projectId>/<workflowVersion>/container.mjs` key.
46
+ */
47
+ url: string;
48
+ /**
49
+ * SHA-256 hex of the bundle bytes, computed at deploy time and
50
+ * stored alongside the bundle. The container verifies the
51
+ * downloaded bytes match this hash before importing — both as an
52
+ * integrity check and as a pin preventing stale-cache confusion.
53
+ * Accepts both plain hex and `sha256:<hex>` formats.
54
+ */
55
+ hash: string;
56
+ }
57
+
58
+ export interface CfContainerRunnerDeps {
59
+ /**
60
+ * DO namespace backing the Cloudflare Container class. Typically
61
+ * `env.NODE_STEP_POOL` wired in wrangler.jsonc to a `Container`-
62
+ * extending class (from `@cloudflare/containers`).
63
+ */
64
+ namespace: ContainerNamespaceLike;
65
+ /**
66
+ * Resolve a signed R2 URL + manifest hash for the bundle the
67
+ * container should import for this dispatch. Called on every
68
+ * invocation (cache in the resolver if URL minting is expensive).
69
+ *
70
+ * If omitted, the dispatch payload has no `bundle` field and the
71
+ * container must have its bundle baked into the image (via the
72
+ * `WORKFLOW_BUNDLE` env var). Multi-tenant production uses the
73
+ * resolver; single-tenant / dev images can skip it.
74
+ */
75
+ resolveBundle?: (args: {
76
+ runId: string;
77
+ workflowId: string;
78
+ workflowVersion: string;
79
+ projectId: string;
80
+ organizationId: string;
81
+ }) => Promise<BundleLocation> | BundleLocation;
82
+ /**
83
+ * Base URL presented to the container. The container's Worker
84
+ * proxy only inspects the path, so this is cosmetic — defaults to
85
+ * `https://node-step.voyant.internal`.
86
+ */
87
+ baseUrl?: string;
88
+ /**
89
+ * Optional HMAC signer for the `X-Voyant-Step-Auth` header so the
90
+ * container can verify the request came from a Voyant orchestrator.
91
+ * Shape matches `createHmacSigner` from `@voyantjs/workflows/auth`.
92
+ */
93
+ sign?: (body: string) => Promise<string> | string;
94
+ /** Optional structured logger. */
95
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
96
+ /**
97
+ * Build the container-addressing id for a given step invocation.
98
+ * Default: `"<runId>:<attempt>:<stepId>"` — deterministic and
99
+ * parallel-safe (distinct step invocations get distinct
100
+ * containers, so the CF addressing model isolates them).
101
+ */
102
+ containerId?: (args: {
103
+ runId: string;
104
+ workflowId: string;
105
+ workflowVersion: string;
106
+ stepId: string;
107
+ attempt: number;
108
+ }) => string;
109
+ }
110
+
111
+ interface StepDispatchPayload {
112
+ runId: string;
113
+ workflowId: string;
114
+ workflowVersion: string;
115
+ projectId: string;
116
+ organizationId: string;
117
+ stepId: string;
118
+ attempt: number;
119
+ input: unknown;
120
+ options: {
121
+ machine?: string;
122
+ timeout?: string | number;
123
+ };
124
+ /** Signed R2 URL + hash for the bundle to import. Absent when the
125
+ * container is single-tenant (bundle baked into the image). */
126
+ bundle?: BundleLocation;
127
+ /**
128
+ * Journal slice at dispatch time. The container uses it to
129
+ * short-circuit already-completed steps on body replay and to stop
130
+ * the drive cleanly after the target step. Passed as opaque JSON
131
+ * here; the container knows how to consume it.
132
+ */
133
+ journal?: unknown;
134
+ }
135
+
136
+ /**
137
+ * Build a `StepRunner` that dispatches each step invocation to a
138
+ * Cloudflare Container in the given namespace.
139
+ *
140
+ * Wire this into the orchestrator's step handler:
141
+ *
142
+ * createStepHandler({
143
+ * nodeStepRunner: createCfContainerStepRunner({
144
+ * namespace: env.NODE_STEP_POOL,
145
+ * }),
146
+ * });
147
+ *
148
+ * Steps that declare `runtime: "node"` will route through here;
149
+ * `runtime: "edge"` (or unset) continues to run inline.
150
+ */
151
+ export function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRunner {
152
+ const baseUrl = deps.baseUrl ?? "https://node-step.voyant.internal";
153
+ const idOf =
154
+ deps.containerId
155
+ ?? (({ runId, attempt, stepId }) => `${runId}:${attempt}:${stepId}`);
156
+
157
+ return async ({
158
+ stepId,
159
+ attempt,
160
+ input,
161
+ stepCtx,
162
+ runId,
163
+ workflowId,
164
+ workflowVersion,
165
+ projectId,
166
+ organizationId,
167
+ options,
168
+ journal,
169
+ }): Promise<StepJournalEntry> => {
170
+ const startedAt = Date.now();
171
+ let bundle: BundleLocation | undefined;
172
+ if (deps.resolveBundle) {
173
+ try {
174
+ bundle = await deps.resolveBundle({
175
+ runId,
176
+ workflowId,
177
+ workflowVersion,
178
+ projectId,
179
+ organizationId,
180
+ });
181
+ } catch (err) {
182
+ deps.logger?.("error", "cf-container: resolveBundle threw", {
183
+ runId,
184
+ stepId,
185
+ error: err instanceof Error ? err.message : String(err),
186
+ });
187
+ return failed(attempt, startedAt, "BUNDLE_RESOLVE_FAILED", err);
188
+ }
189
+ }
190
+ const payload: StepDispatchPayload = {
191
+ runId,
192
+ workflowId,
193
+ workflowVersion,
194
+ projectId,
195
+ organizationId,
196
+ stepId,
197
+ attempt,
198
+ input,
199
+ options: {
200
+ machine: options.machine,
201
+ timeout:
202
+ typeof options.timeout === "string" || typeof options.timeout === "number"
203
+ ? (options.timeout as string | number)
204
+ : undefined,
205
+ },
206
+ bundle,
207
+ journal,
208
+ };
209
+ const body = JSON.stringify(payload);
210
+ const headers: Record<string, string> = {
211
+ "content-type": "application/json; charset=utf-8",
212
+ };
213
+ if (deps.sign) {
214
+ headers["x-voyant-step-auth"] = await deps.sign(body);
215
+ }
216
+
217
+ const id = deps.namespace.idFromName(
218
+ idOf({ runId, workflowId, workflowVersion, stepId, attempt }),
219
+ );
220
+ const stub = deps.namespace.get(id);
221
+ const request = new Request(`${baseUrl}/step`, {
222
+ method: "POST",
223
+ headers,
224
+ body,
225
+ signal: stepCtx.signal,
226
+ });
227
+
228
+ deps.logger?.("info", "cf-container: dispatching step", {
229
+ runId,
230
+ workflowId,
231
+ stepId,
232
+ attempt,
233
+ });
234
+
235
+ let response: Response;
236
+ try {
237
+ response = await stub.fetch(request);
238
+ } catch (err) {
239
+ deps.logger?.("error", "cf-container: fetch threw", {
240
+ runId,
241
+ stepId,
242
+ error: err instanceof Error ? err.message : String(err),
243
+ });
244
+ return failed(attempt, startedAt, "CONTAINER_DISPATCH_FAILED", err);
245
+ }
246
+
247
+ const text = await response.text();
248
+ if (response.status !== 200) {
249
+ deps.logger?.("warn", "cf-container: non-200 response", {
250
+ runId,
251
+ stepId,
252
+ status: response.status,
253
+ body: text.slice(0, 500),
254
+ });
255
+ return failed(
256
+ attempt,
257
+ startedAt,
258
+ "CONTAINER_HTTP_ERROR",
259
+ new Error(`container returned HTTP ${response.status}: ${text}`),
260
+ );
261
+ }
262
+ try {
263
+ const entry = JSON.parse(text) as StepJournalEntry;
264
+ // Trust the container's own timestamps; they reflect the actual
265
+ // step body execution, not the dispatch round-trip.
266
+ return entry;
267
+ } catch (err) {
268
+ return failed(
269
+ attempt,
270
+ startedAt,
271
+ "CONTAINER_INVALID_RESPONSE",
272
+ new Error(`container returned non-JSON body: ${String(err)}`),
273
+ );
274
+ }
275
+ };
276
+ }
277
+
278
+ function failed(
279
+ attempt: number,
280
+ startedAt: number,
281
+ code: string,
282
+ err: unknown,
283
+ ): StepJournalEntry {
284
+ const e = err instanceof Error ? err : new Error(String(err));
285
+ return {
286
+ attempt,
287
+ status: "err",
288
+ startedAt,
289
+ finishedAt: Date.now(),
290
+ error: {
291
+ category: "RUNTIME_ERROR",
292
+ code,
293
+ message: e.message,
294
+ name: e.name,
295
+ stack: e.stack,
296
+ },
297
+ };
298
+ }
@@ -0,0 +1,53 @@
1
+ // Builds a `StepHandler` (the thing the orchestrator calls on every
2
+ // invocation) by routing the request through a Workers-for-Platforms
3
+ // dispatch namespace to the tenant's bundled Worker.
4
+ //
5
+ // The tenant Worker is expected to mount `@voyantjs/workflows/handler`
6
+ // at `POST /__voyant/workflow-step` (see docs/runtime-protocol.md §2.1).
7
+
8
+ import {
9
+ createHttpStepHandler,
10
+ type StepHandler,
11
+ type WorkflowStepRequest,
12
+ } from "@voyantjs/workflows-orchestrator";
13
+ import type { DispatchNamespaceLike } from "./types.js";
14
+
15
+ export interface DispatchHandlerDeps {
16
+ dispatcher: DispatchNamespaceLike;
17
+ /** Optional HMAC signer for the X-Voyant-Dispatch-Auth header. */
18
+ sign?: (body: string) => Promise<string> | string;
19
+ /** Optional logger for step-level observability. */
20
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
21
+ /** Base URL to present to the tenant. Defaults to `https://tenant.voyant.internal`. */
22
+ baseUrl?: string;
23
+ }
24
+
25
+ /**
26
+ * The dispatcher binding expects a name corresponding to the tenant
27
+ * script slot. `createDispatchStepHandler` binds the step handler to
28
+ * a specific tenant script; different tenants need different handlers
29
+ * (trivial via `createDispatchStepHandler(...)` with the script name
30
+ * resolved from the run's tenantMeta).
31
+ */
32
+ export function createDispatchStepHandler(
33
+ tenantScript: string,
34
+ deps: DispatchHandlerDeps,
35
+ ): StepHandler {
36
+ const baseUrl = deps.baseUrl ?? "https://tenant.voyant.internal";
37
+ return createHttpStepHandler({
38
+ sign: deps.sign
39
+ ? (body) => deps.sign!(body)
40
+ : undefined,
41
+ logger: deps.logger,
42
+ resolveTarget(_req: WorkflowStepRequest) {
43
+ const binding = deps.dispatcher.get(tenantScript);
44
+ return {
45
+ url: `${baseUrl}/__voyant/workflow-step`,
46
+ label: tenantScript,
47
+ fetch(request: Request) {
48
+ return binding.fetch(request);
49
+ },
50
+ };
51
+ },
52
+ });
53
+ }
@@ -0,0 +1,39 @@
1
+ // A RunRecordStore backed by `DurableObjectStorage`. In production
2
+ // this is the DO's transactional KV; in tests, any
3
+ // DurableObjectStorageLike (e.g. in-memory Map wrapper) works.
4
+ //
5
+ // Layout: one run record per DO. Stored under the fixed key
6
+ // `record`. The adapter assumes one DO per runId, so "list" scans a
7
+ // single key. If a caller wants multi-run listing they should go
8
+ // through the global run index (lives in voyant-cloud's Postgres,
9
+ // not here).
10
+
11
+ import type { RunRecord, RunRecordStore } from "@voyantjs/workflows-orchestrator";
12
+ import type { DurableObjectStorageLike } from "./types.js";
13
+
14
+ const RECORD_KEY = "record";
15
+
16
+ export function createDurableObjectRunStore(
17
+ storage: DurableObjectStorageLike,
18
+ ): RunRecordStore {
19
+ return {
20
+ async get(id) {
21
+ const r = await storage.get<RunRecord>(RECORD_KEY);
22
+ if (!r || r.id !== id) return undefined;
23
+ return r;
24
+ },
25
+
26
+ async save(record) {
27
+ await storage.put<RunRecord>(RECORD_KEY, record);
28
+ return record;
29
+ },
30
+
31
+ async list(filter = {}) {
32
+ const r = await storage.get<RunRecord>(RECORD_KEY);
33
+ if (!r) return [];
34
+ if (filter.workflowId && r.workflowId !== filter.workflowId) return [];
35
+ if (filter.status && r.status !== filter.status) return [];
36
+ return [r];
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,224 @@
1
+ // The WorkflowRunDO — one Durable Object per run. Holds the
2
+ // RunRecord in storage, drives the run through the tenant via a
3
+ // dispatch-namespace StepHandler, exposes a tiny HTTP surface to
4
+ // the outer Worker, and schedules DO alarms for DATETIME waitpoints
5
+ // so `ctx.sleep(...)` wakes back up.
6
+ //
7
+ // Routes:
8
+ // POST /trigger { ...TriggerPayload } → RunRecord
9
+ // POST /resume { injection } → RunRecord | error
10
+ // POST /cancel { reason? } → RunRecord | error
11
+ // GET /get → RunRecord | 404
12
+ //
13
+ // Alarm entry point: handleDurableObjectAlarm(deps). Fired by the CF
14
+ // runtime at the wake time scheduled by setAlarm; resolves due
15
+ // DATETIME waitpoints, re-drives, and reschedules if needed.
16
+ //
17
+ // Callers should treat the HTTP surface as an implementation detail;
18
+ // the public surface is the outer Worker's /api/* routes.
19
+
20
+ import {
21
+ applyWaitpointInjection,
22
+ cancel as orchestratorCancel,
23
+ driveUntilPaused,
24
+ resume as orchestratorResume,
25
+ trigger as orchestratorTrigger,
26
+ type PendingWaitpoint,
27
+ type RunRecord,
28
+ type StepHandler,
29
+ } from "@voyantjs/workflows-orchestrator";
30
+ import type {
31
+ CancelPayload,
32
+ DurableObjectStorageLike,
33
+ ResumePayload,
34
+ TriggerPayload,
35
+ } from "./types.js";
36
+ import { createDurableObjectRunStore } from "./do-store.js";
37
+
38
+ export interface DurableObjectDeps {
39
+ storage: DurableObjectStorageLike;
40
+ /**
41
+ * Resolve the StepHandler to use for a given tenant script. Called
42
+ * with the tenantScript (from the trigger payload) so the DO can
43
+ * route to the correct tenant Worker. In production this closes
44
+ * over the dispatch namespace; in tests it returns a mock.
45
+ */
46
+ resolveStepHandler: (tenantScript: string) => StepHandler;
47
+ now?: () => number;
48
+ }
49
+
50
+ export async function handleDurableObjectRequest(
51
+ req: Request,
52
+ deps: DurableObjectDeps,
53
+ ): Promise<Response> {
54
+ const url = new URL(req.url);
55
+ const store = createDurableObjectRunStore(deps.storage);
56
+
57
+ if (req.method === "POST" && url.pathname === "/trigger") {
58
+ const payload = (await req.json()) as TriggerPayload;
59
+ const handler = deps.resolveStepHandler(payload.tenantMeta.tenantScript);
60
+ const record = await orchestratorTrigger(
61
+ {
62
+ workflowId: payload.workflowId,
63
+ workflowVersion: payload.workflowVersion,
64
+ input: payload.input,
65
+ tenantMeta: payload.tenantMeta,
66
+ environment: payload.environment,
67
+ tags: payload.tags,
68
+ runId: payload.runId,
69
+ },
70
+ { store, handler, now: deps.now },
71
+ );
72
+ await reconcileAlarm(record, store, deps);
73
+ return json(200, record);
74
+ }
75
+
76
+ if (req.method === "POST" && url.pathname === "/resume") {
77
+ const payload = (await req.json()) as ResumePayload;
78
+ const existing = await store.get((await getStoredRunId(store)) ?? "");
79
+ if (!existing) return json(404, { error: "not_found", message: "no run stored in this DO" });
80
+ const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "");
81
+ const out = await orchestratorResume(
82
+ { runId: existing.id, injection: payload.injection },
83
+ { store, handler, now: deps.now },
84
+ );
85
+ if (!out.ok) {
86
+ const status = out.status === "not_found" ? 404 : out.status === "no_match" ? 400 : 409;
87
+ return json(status, { error: out.status, message: out.message });
88
+ }
89
+ await reconcileAlarm(out.record, store, deps);
90
+ return json(200, out.record);
91
+ }
92
+
93
+ if (req.method === "POST" && url.pathname === "/cancel") {
94
+ const payload = (await req.json()) as CancelPayload;
95
+ const existing = await store.get((await getStoredRunId(store)) ?? "");
96
+ if (!existing) return json(404, { error: "not_found", message: "no run stored in this DO" });
97
+ const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "");
98
+ const out = await orchestratorCancel(
99
+ { runId: existing.id, reason: payload.reason },
100
+ { store, handler, now: deps.now },
101
+ );
102
+ if (!out.ok) {
103
+ const status = out.status === "not_found" ? 404 : 409;
104
+ return json(status, { error: out.status, message: out.message });
105
+ }
106
+ await reconcileAlarm(out.record, store, deps);
107
+ return json(200, out.record);
108
+ }
109
+
110
+ if (req.method === "GET" && url.pathname === "/get") {
111
+ const existing = await store.get((await getStoredRunId(store)) ?? "");
112
+ if (!existing) return json(404, { error: "not_found" });
113
+ return json(200, existing);
114
+ }
115
+
116
+ return json(404, { error: "route_not_found", path: url.pathname });
117
+ }
118
+
119
+ /**
120
+ * Fire this from your DO's `alarm()` method. Walks pending
121
+ * waitpoints, resolves every DATETIME whose `wakeAt` is <= now,
122
+ * re-drives the run, and reschedules the next alarm if the run
123
+ * parked on another DATETIME.
124
+ */
125
+ export async function handleDurableObjectAlarm(
126
+ deps: DurableObjectDeps,
127
+ ): Promise<void> {
128
+ const store = createDurableObjectRunStore(deps.storage);
129
+ const existingId = await getStoredRunId(store);
130
+ if (!existingId) return;
131
+ const record = await store.get(existingId);
132
+ if (!record) return;
133
+ if (record.status !== "waiting") {
134
+ await deps.storage.deleteAlarm?.();
135
+ return;
136
+ }
137
+
138
+ const now = (deps.now ?? (() => Date.now()))();
139
+ const stillPending: PendingWaitpoint[] = [];
140
+ let resolvedAny = false;
141
+ for (const wp of record.pendingWaitpoints) {
142
+ const wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined;
143
+ if (wp.kind === "DATETIME" && wakeAt !== undefined && wakeAt <= now) {
144
+ record.journal.waitpointsResolved[wp.clientWaitpointId] = {
145
+ kind: "DATETIME",
146
+ resolvedAt: now,
147
+ source: "replay",
148
+ };
149
+ resolvedAny = true;
150
+ } else {
151
+ stillPending.push(wp);
152
+ }
153
+ }
154
+ record.pendingWaitpoints = stillPending;
155
+ if (!resolvedAny) {
156
+ // Spurious alarm — nothing due. Persist and reconcile.
157
+ await store.save(record);
158
+ await reconcileAlarm(record, store, deps);
159
+ return;
160
+ }
161
+
162
+ record.status = "running";
163
+ const handler = deps.resolveStepHandler(record.tenantMeta.tenantScript ?? "");
164
+ await driveUntilPaused(record, { handler, now: deps.now });
165
+ await store.save(record);
166
+ await reconcileAlarm(record, store, deps);
167
+ }
168
+
169
+ /**
170
+ * Look at the record's pending waitpoints; if any are DATETIME,
171
+ * stamp a `wakeAt` into meta (if not already set) and schedule the
172
+ * earliest via `setAlarm`. Clears any prior alarm when the run is
173
+ * terminal or has no DATETIME waitpoints left.
174
+ */
175
+ async function reconcileAlarm(
176
+ record: RunRecord,
177
+ store: ReturnType<typeof createDurableObjectRunStore>,
178
+ deps: DurableObjectDeps,
179
+ ): Promise<void> {
180
+ const now = (deps.now ?? (() => Date.now()))();
181
+ if (record.status !== "waiting") {
182
+ await deps.storage.deleteAlarm?.();
183
+ return;
184
+ }
185
+ let earliest: number | undefined;
186
+ let dirty = false;
187
+ for (const wp of record.pendingWaitpoints) {
188
+ if (wp.kind !== "DATETIME") continue;
189
+ let wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined;
190
+ if (wakeAt === undefined) {
191
+ const ms = wp.timeoutMs ?? (typeof wp.meta.durationMs === "number" ? wp.meta.durationMs : 0);
192
+ wakeAt = now + ms;
193
+ wp.meta.wakeAt = wakeAt;
194
+ dirty = true;
195
+ }
196
+ earliest = earliest === undefined ? wakeAt : Math.min(earliest, wakeAt);
197
+ }
198
+ if (dirty) await store.save(record);
199
+ if (earliest !== undefined) {
200
+ await deps.storage.setAlarm?.(earliest);
201
+ } else {
202
+ await deps.storage.deleteAlarm?.();
203
+ }
204
+ }
205
+
206
+ /**
207
+ * One DO holds one run, keyed by `record`. Returns the id of that
208
+ * stored run, if any.
209
+ */
210
+ async function getStoredRunId(store: ReturnType<typeof createDurableObjectRunStore>): Promise<string | undefined> {
211
+ const all = await store.list();
212
+ return all[0]?.id;
213
+ }
214
+
215
+ function json(status: number, body: unknown): Response {
216
+ return new Response(JSON.stringify(body), {
217
+ status,
218
+ headers: { "content-type": "application/json; charset=utf-8" },
219
+ });
220
+ }
221
+
222
+ // Re-exported for callers building their own DO class via composition.
223
+ export { applyWaitpointInjection, driveUntilPaused };
224
+ export type { RunRecord };
package/src/index.ts ADDED
@@ -0,0 +1,55 @@
1
+ // @voyantjs/workflows-orchestrator-cloudflare
2
+ //
3
+ // Cloudflare Worker + Durable Object adapter for @voyantjs/workflows-orchestrator.
4
+ // Composes the protocol-agnostic state machine in @voyantjs/workflows-orchestrator
5
+ // with a DO-backed run store and a dispatch-namespace step handler.
6
+ //
7
+ // Typical wrangler.jsonc layout:
8
+ //
9
+ // {
10
+ // "name": "voyant-orchestrator",
11
+ // "main": "src/worker.ts",
12
+ // "compatibility_date": "2025-01-01",
13
+ // "durable_objects": {
14
+ // "bindings": [
15
+ // { "name": "WORKFLOW_RUN_DO", "class_name": "WorkflowRunDO" }
16
+ // ]
17
+ // },
18
+ // "dispatch_namespaces": [
19
+ // { "binding": "DISPATCHER", "namespace": "voyant-tenants" }
20
+ // ],
21
+ // "migrations": [
22
+ // { "tag": "v1", "new_sqlite_classes": ["WorkflowRunDO"] }
23
+ // ]
24
+ // }
25
+ //
26
+ // See docs/runtime-protocol.md §2 and docs/design.md §6 for the
27
+ // design this adapter implements.
28
+
29
+ export * from "./types.js";
30
+ export { createDurableObjectRunStore } from "./do-store.js";
31
+ export {
32
+ createDispatchStepHandler,
33
+ type DispatchHandlerDeps,
34
+ } from "./dispatch-handler.js";
35
+ export {
36
+ handleDurableObjectRequest,
37
+ handleDurableObjectAlarm,
38
+ type DurableObjectDeps,
39
+ } from "./durable-object.js";
40
+ export {
41
+ handleWorkerRequest,
42
+ type WorkerFetchDeps,
43
+ type DurableObjectNamespaceLike,
44
+ } from "./worker.js";
45
+ export {
46
+ createCfContainerStepRunner,
47
+ type BundleLocation,
48
+ type CfContainerRunnerDeps,
49
+ type ContainerNamespaceLike,
50
+ } from "./cf-container-runner.js";
51
+ export {
52
+ createR2Presigner,
53
+ type R2PresignerOptions,
54
+ type PresignArgs,
55
+ } from "./r2-sign.js";