@voyantjs/workflows-orchestrator-cloudflare 0.6.7 → 0.6.8

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,102 @@
1
+ import type { StepRunner } from "@voyantjs/workflows/handler";
2
+ /**
3
+ * Minimal subset of `DurableObjectNamespace` that the runner actually
4
+ * uses. Matches the shape exposed by `@cloudflare/containers`'
5
+ * `getContainer(namespace, id).fetch()` pattern — we keep it local so
6
+ * tests can pass a stub.
7
+ */
8
+ export interface ContainerNamespaceLike {
9
+ idFromName(name: string): {
10
+ toString(): string;
11
+ };
12
+ get(id: {
13
+ toString(): string;
14
+ }): {
15
+ fetch(request: Request): Promise<Response>;
16
+ };
17
+ }
18
+ export interface BundleLocation {
19
+ /**
20
+ * Short-lived signed URL the container uses to fetch the bundle
21
+ * from R2. Expected TTL is minutes, not hours — scoped tightly to
22
+ * the specific `<projectId>/<workflowVersion>/container.mjs` key.
23
+ */
24
+ url: string;
25
+ /**
26
+ * SHA-256 hex of the bundle bytes, computed at deploy time and
27
+ * stored alongside the bundle. The container verifies the
28
+ * downloaded bytes match this hash before importing — both as an
29
+ * integrity check and as a pin preventing stale-cache confusion.
30
+ * Accepts both plain hex and `sha256:<hex>` formats.
31
+ */
32
+ hash: string;
33
+ }
34
+ export interface CfContainerRunnerDeps {
35
+ /**
36
+ * DO namespace backing the Cloudflare Container class. Typically
37
+ * `env.NODE_STEP_POOL` wired in wrangler.jsonc to a `Container`-
38
+ * extending class (from `@cloudflare/containers`).
39
+ */
40
+ namespace: ContainerNamespaceLike;
41
+ /**
42
+ * Resolve a signed R2 URL + manifest hash for the bundle the
43
+ * container should import for this dispatch. Called on every
44
+ * invocation (cache in the resolver if URL minting is expensive).
45
+ *
46
+ * If omitted, the dispatch payload has no `bundle` field and the
47
+ * container must have its bundle baked into the image (via the
48
+ * `WORKFLOW_BUNDLE` env var). Multi-tenant production uses the
49
+ * resolver; single-tenant / dev images can skip it.
50
+ */
51
+ resolveBundle?: (args: {
52
+ runId: string;
53
+ workflowId: string;
54
+ workflowVersion: string;
55
+ projectId: string;
56
+ organizationId: string;
57
+ }) => Promise<BundleLocation> | BundleLocation;
58
+ /**
59
+ * Base URL presented to the container. The container's Worker
60
+ * proxy only inspects the path, so this is cosmetic — defaults to
61
+ * `https://node-step.voyant.internal`.
62
+ */
63
+ baseUrl?: string;
64
+ /**
65
+ * Optional HMAC signer for the `X-Voyant-Step-Auth` header so the
66
+ * container can verify the request came from a Voyant orchestrator.
67
+ * Shape matches `createHmacSigner` from `@voyantjs/workflows/auth`.
68
+ */
69
+ sign?: (body: string) => Promise<string> | string;
70
+ /** Optional structured logger. */
71
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
72
+ /**
73
+ * Build the container-addressing id for a given step invocation.
74
+ * Default: `"<runId>:<attempt>:<stepId>"` — deterministic and
75
+ * parallel-safe (distinct step invocations get distinct
76
+ * containers, so the CF addressing model isolates them).
77
+ */
78
+ containerId?: (args: {
79
+ runId: string;
80
+ workflowId: string;
81
+ workflowVersion: string;
82
+ stepId: string;
83
+ attempt: number;
84
+ }) => string;
85
+ }
86
+ /**
87
+ * Build a `StepRunner` that dispatches each step invocation to a
88
+ * Cloudflare Container in the given namespace.
89
+ *
90
+ * Wire this into the orchestrator's step handler:
91
+ *
92
+ * createStepHandler({
93
+ * nodeStepRunner: createCfContainerStepRunner({
94
+ * namespace: env.NODE_STEP_POOL,
95
+ * }),
96
+ * });
97
+ *
98
+ * Steps that declare `runtime: "node"` will route through here;
99
+ * `runtime: "edge"` (or unset) continues to run inline.
100
+ */
101
+ export declare function createCfContainerStepRunner(deps: CfContainerRunnerDeps): StepRunner;
102
+ //# sourceMappingURL=cf-container-runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cf-container-runner.d.ts","sourceRoot":"","sources":["../src/cf-container-runner.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAoB,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAE/E;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG;QAAE,QAAQ,IAAI,MAAM,CAAA;KAAE,CAAA;IAChD,GAAG,CAAC,EAAE,EAAE;QAAE,QAAQ,IAAI,MAAM,CAAA;KAAE,GAAG;QAAE,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CAChF;AAED,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAA;IACX;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,SAAS,EAAE,sBAAsB,CAAA;IACjC;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE;QACrB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,MAAM,CAAA;QAClB,eAAe,EAAE,MAAM,CAAA;QACvB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;KACvB,KAAK,OAAO,CAAC,cAAc,CAAC,GAAG,cAAc,CAAA;IAC9C;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;IACjD,kCAAkC;IAClC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/E;;;;;OAKG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE;QACnB,KAAK,EAAE,MAAM,CAAA;QACb,UAAU,EAAE,MAAM,CAAA;QAClB,eAAe,EAAE,MAAM,CAAA;QACvB,MAAM,EAAE,MAAM,CAAA;QACd,OAAO,EAAE,MAAM,CAAA;KAChB,KAAK,MAAM,CAAA;CACb;AA2BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,qBAAqB,GAAG,UAAU,CA2HnF"}
@@ -0,0 +1,153 @@
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
+ * Build a `StepRunner` that dispatches each step invocation to a
28
+ * Cloudflare Container in the given namespace.
29
+ *
30
+ * Wire this into the orchestrator's step handler:
31
+ *
32
+ * createStepHandler({
33
+ * nodeStepRunner: createCfContainerStepRunner({
34
+ * namespace: env.NODE_STEP_POOL,
35
+ * }),
36
+ * });
37
+ *
38
+ * Steps that declare `runtime: "node"` will route through here;
39
+ * `runtime: "edge"` (or unset) continues to run inline.
40
+ */
41
+ export function createCfContainerStepRunner(deps) {
42
+ const baseUrl = deps.baseUrl ?? "https://node-step.voyant.internal";
43
+ const idOf = deps.containerId ?? (({ runId, attempt, stepId }) => `${runId}:${attempt}:${stepId}`);
44
+ return async ({ stepId, attempt, input, stepCtx, runId, workflowId, workflowVersion, projectId, organizationId, options, journal, }) => {
45
+ const startedAt = Date.now();
46
+ let bundle;
47
+ if (deps.resolveBundle) {
48
+ try {
49
+ bundle = await deps.resolveBundle({
50
+ runId,
51
+ workflowId,
52
+ workflowVersion,
53
+ projectId,
54
+ organizationId,
55
+ });
56
+ }
57
+ catch (err) {
58
+ deps.logger?.("error", "cf-container: resolveBundle threw", {
59
+ runId,
60
+ stepId,
61
+ error: err instanceof Error ? err.message : String(err),
62
+ });
63
+ return failed(attempt, startedAt, "BUNDLE_RESOLVE_FAILED", err);
64
+ }
65
+ }
66
+ const payload = {
67
+ runId,
68
+ workflowId,
69
+ workflowVersion,
70
+ projectId,
71
+ organizationId,
72
+ stepId,
73
+ attempt,
74
+ input,
75
+ options: {
76
+ machine: options.machine,
77
+ timeout: typeof options.timeout === "string" || typeof options.timeout === "number"
78
+ ? options.timeout
79
+ : undefined,
80
+ },
81
+ bundle,
82
+ journal,
83
+ };
84
+ const body = JSON.stringify(payload);
85
+ const headers = {
86
+ "content-type": "application/json; charset=utf-8",
87
+ };
88
+ if (deps.sign) {
89
+ headers["x-voyant-step-auth"] = await deps.sign(body);
90
+ }
91
+ const id = deps.namespace.idFromName(idOf({ runId, workflowId, workflowVersion, stepId, attempt }));
92
+ const stub = deps.namespace.get(id);
93
+ const request = new Request(`${baseUrl}/step`, {
94
+ method: "POST",
95
+ headers,
96
+ body,
97
+ signal: stepCtx.signal,
98
+ });
99
+ deps.logger?.("info", "cf-container: dispatching step", {
100
+ runId,
101
+ workflowId,
102
+ stepId,
103
+ attempt,
104
+ });
105
+ let response;
106
+ try {
107
+ response = await stub.fetch(request);
108
+ }
109
+ catch (err) {
110
+ deps.logger?.("error", "cf-container: fetch threw", {
111
+ runId,
112
+ stepId,
113
+ error: err instanceof Error ? err.message : String(err),
114
+ });
115
+ return failed(attempt, startedAt, "CONTAINER_DISPATCH_FAILED", err);
116
+ }
117
+ const text = await response.text();
118
+ if (response.status !== 200) {
119
+ deps.logger?.("warn", "cf-container: non-200 response", {
120
+ runId,
121
+ stepId,
122
+ status: response.status,
123
+ body: text.slice(0, 500),
124
+ });
125
+ return failed(attempt, startedAt, "CONTAINER_HTTP_ERROR", new Error(`container returned HTTP ${response.status}: ${text}`));
126
+ }
127
+ try {
128
+ const entry = JSON.parse(text);
129
+ // Trust the container's own timestamps; they reflect the actual
130
+ // step body execution, not the dispatch round-trip.
131
+ return entry;
132
+ }
133
+ catch (err) {
134
+ return failed(attempt, startedAt, "CONTAINER_INVALID_RESPONSE", new Error(`container returned non-JSON body: ${String(err)}`));
135
+ }
136
+ };
137
+ }
138
+ function failed(attempt, startedAt, code, err) {
139
+ const e = err instanceof Error ? err : new Error(String(err));
140
+ return {
141
+ attempt,
142
+ status: "err",
143
+ startedAt,
144
+ finishedAt: Date.now(),
145
+ error: {
146
+ category: "RUNTIME_ERROR",
147
+ code,
148
+ message: e.message,
149
+ name: e.name,
150
+ stack: e.stack,
151
+ },
152
+ };
153
+ }
@@ -0,0 +1,20 @@
1
+ import { type StepHandler } from "@voyantjs/workflows-orchestrator";
2
+ import type { DispatchNamespaceLike } from "./types.js";
3
+ export interface DispatchHandlerDeps {
4
+ dispatcher: DispatchNamespaceLike;
5
+ /** Optional HMAC signer for the X-Voyant-Dispatch-Auth header. */
6
+ sign?: (body: string) => Promise<string> | string;
7
+ /** Optional logger for step-level observability. */
8
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
9
+ /** Base URL to present to the tenant. Defaults to `https://tenant.voyant.internal`. */
10
+ baseUrl?: string;
11
+ }
12
+ /**
13
+ * The dispatcher binding expects a name corresponding to the tenant
14
+ * script slot. `createDispatchStepHandler` binds the step handler to
15
+ * a specific tenant script; different tenants need different handlers
16
+ * (trivial via `createDispatchStepHandler(...)` with the script name
17
+ * resolved from the run's tenantMeta).
18
+ */
19
+ export declare function createDispatchStepHandler(tenantScript: string, deps: DispatchHandlerDeps): StepHandler;
20
+ //# sourceMappingURL=dispatch-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dispatch-handler.d.ts","sourceRoot":"","sources":["../src/dispatch-handler.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,kCAAkC,CAAA;AACzC,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEvD,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,qBAAqB,CAAA;IACjC,kEAAkE;IAClE,IAAI,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAA;IACjD,oDAAoD;IACpD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IAC/E,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,mBAAmB,GACxB,WAAW,CAgBb"}
@@ -0,0 +1,31 @@
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
+ import { createHttpStepHandler, } from "@voyantjs/workflows-orchestrator";
8
+ /**
9
+ * The dispatcher binding expects a name corresponding to the tenant
10
+ * script slot. `createDispatchStepHandler` binds the step handler to
11
+ * a specific tenant script; different tenants need different handlers
12
+ * (trivial via `createDispatchStepHandler(...)` with the script name
13
+ * resolved from the run's tenantMeta).
14
+ */
15
+ export function createDispatchStepHandler(tenantScript, deps) {
16
+ const baseUrl = deps.baseUrl ?? "https://tenant.voyant.internal";
17
+ return createHttpStepHandler({
18
+ sign: deps.sign ? (body) => deps.sign(body) : undefined,
19
+ logger: deps.logger,
20
+ resolveTarget(_req) {
21
+ const binding = deps.dispatcher.get(tenantScript);
22
+ return {
23
+ url: `${baseUrl}/__voyant/workflow-step`,
24
+ label: tenantScript,
25
+ fetch(request) {
26
+ return binding.fetch(request);
27
+ },
28
+ };
29
+ },
30
+ });
31
+ }
@@ -0,0 +1,4 @@
1
+ import type { RunRecordStore } from "@voyantjs/workflows-orchestrator";
2
+ import type { DurableObjectStorageLike } from "./types.js";
3
+ export declare function createDurableObjectRunStore(storage: DurableObjectStorageLike): RunRecordStore;
4
+ //# sourceMappingURL=do-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"do-store.d.ts","sourceRoot":"","sources":["../src/do-store.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAa,cAAc,EAAE,MAAM,kCAAkC,CAAA;AACjF,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAA;AAI1D,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,wBAAwB,GAAG,cAAc,CAqB7F"}
@@ -0,0 +1,34 @@
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
+ const RECORD_KEY = "record";
11
+ export function createDurableObjectRunStore(storage) {
12
+ return {
13
+ async get(id) {
14
+ const r = await storage.get(RECORD_KEY);
15
+ if (!r || r.id !== id)
16
+ return undefined;
17
+ return r;
18
+ },
19
+ async save(record) {
20
+ await storage.put(RECORD_KEY, record);
21
+ return record;
22
+ },
23
+ async list(filter = {}) {
24
+ const r = await storage.get(RECORD_KEY);
25
+ if (!r)
26
+ return [];
27
+ if (filter.workflowId && r.workflowId !== filter.workflowId)
28
+ return [];
29
+ if (filter.status && r.status !== filter.status)
30
+ return [];
31
+ return [r];
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,24 @@
1
+ import { applyWaitpointInjection, driveUntilPaused, type RunRecord, type StepHandler } from "@voyantjs/workflows-orchestrator";
2
+ import type { DurableObjectStorageLike } from "./types.js";
3
+ export interface DurableObjectDeps {
4
+ storage: DurableObjectStorageLike;
5
+ /**
6
+ * Resolve the StepHandler to use for a given tenant script. Called
7
+ * with the tenantScript (from the trigger payload) so the DO can
8
+ * route to the correct tenant Worker. In production this closes
9
+ * over the dispatch namespace; in tests it returns a mock.
10
+ */
11
+ resolveStepHandler: (tenantScript: string) => StepHandler;
12
+ now?: () => number;
13
+ }
14
+ export declare function handleDurableObjectRequest(req: Request, deps: DurableObjectDeps): Promise<Response>;
15
+ /**
16
+ * Fire this from your DO's `alarm()` method. Walks pending
17
+ * waitpoints, resolves every DATETIME whose `wakeAt` is <= now,
18
+ * re-drives the run, and reschedules the next alarm if the run
19
+ * parked on another DATETIME.
20
+ */
21
+ export declare function handleDurableObjectAlarm(deps: DurableObjectDeps): Promise<void>;
22
+ export type { RunRecord };
23
+ export { applyWaitpointInjection, driveUntilPaused };
24
+ //# sourceMappingURL=durable-object.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-object.d.ts","sourceRoot":"","sources":["../src/durable-object.ts"],"names":[],"mappings":"AAmBA,OAAO,EACL,uBAAuB,EACvB,gBAAgB,EAKhB,KAAK,SAAS,EACd,KAAK,WAAW,EACjB,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAEV,wBAAwB,EAGzB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,wBAAwB,CAAA;IACjC;;;;;OAKG;IACH,kBAAkB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,WAAW,CAAA;IACzD,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,QAAQ,CAAC,CAgEnB;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwCrF;AAyDD,YAAY,EAAE,SAAS,EAAE,CAAA;AAEzB,OAAO,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,CAAA"}
@@ -0,0 +1,173 @@
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
+ import { applyWaitpointInjection, driveUntilPaused, cancel as orchestratorCancel, resume as orchestratorResume, trigger as orchestratorTrigger, } from "@voyantjs/workflows-orchestrator";
20
+ import { createDurableObjectRunStore } from "./do-store.js";
21
+ export async function handleDurableObjectRequest(req, deps) {
22
+ const url = new URL(req.url);
23
+ const store = createDurableObjectRunStore(deps.storage);
24
+ if (req.method === "POST" && url.pathname === "/trigger") {
25
+ const payload = (await req.json());
26
+ const handler = deps.resolveStepHandler(payload.tenantMeta.tenantScript);
27
+ const record = await orchestratorTrigger({
28
+ workflowId: payload.workflowId,
29
+ workflowVersion: payload.workflowVersion,
30
+ input: payload.input,
31
+ tenantMeta: payload.tenantMeta,
32
+ environment: payload.environment,
33
+ tags: payload.tags,
34
+ runId: payload.runId,
35
+ }, { store, handler, now: deps.now });
36
+ await reconcileAlarm(record, store, deps);
37
+ return json(200, record);
38
+ }
39
+ if (req.method === "POST" && url.pathname === "/resume") {
40
+ const payload = (await req.json());
41
+ const existing = await store.get((await getStoredRunId(store)) ?? "");
42
+ if (!existing)
43
+ return json(404, { error: "not_found", message: "no run stored in this DO" });
44
+ const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "");
45
+ const out = await orchestratorResume({ runId: existing.id, injection: payload.injection }, { store, handler, now: deps.now });
46
+ if (!out.ok) {
47
+ const status = out.status === "not_found" ? 404 : out.status === "no_match" ? 400 : 409;
48
+ return json(status, { error: out.status, message: out.message });
49
+ }
50
+ await reconcileAlarm(out.record, store, deps);
51
+ return json(200, out.record);
52
+ }
53
+ if (req.method === "POST" && url.pathname === "/cancel") {
54
+ const payload = (await req.json());
55
+ const existing = await store.get((await getStoredRunId(store)) ?? "");
56
+ if (!existing)
57
+ return json(404, { error: "not_found", message: "no run stored in this DO" });
58
+ const handler = deps.resolveStepHandler(existing.tenantMeta.tenantScript ?? "");
59
+ const out = await orchestratorCancel({ runId: existing.id, reason: payload.reason }, { store, handler, now: deps.now });
60
+ if (!out.ok) {
61
+ const status = out.status === "not_found" ? 404 : 409;
62
+ return json(status, { error: out.status, message: out.message });
63
+ }
64
+ await reconcileAlarm(out.record, store, deps);
65
+ return json(200, out.record);
66
+ }
67
+ if (req.method === "GET" && url.pathname === "/get") {
68
+ const existing = await store.get((await getStoredRunId(store)) ?? "");
69
+ if (!existing)
70
+ return json(404, { error: "not_found" });
71
+ return json(200, existing);
72
+ }
73
+ return json(404, { error: "route_not_found", path: url.pathname });
74
+ }
75
+ /**
76
+ * Fire this from your DO's `alarm()` method. Walks pending
77
+ * waitpoints, resolves every DATETIME whose `wakeAt` is <= now,
78
+ * re-drives the run, and reschedules the next alarm if the run
79
+ * parked on another DATETIME.
80
+ */
81
+ export async function handleDurableObjectAlarm(deps) {
82
+ const store = createDurableObjectRunStore(deps.storage);
83
+ const existingId = await getStoredRunId(store);
84
+ if (!existingId)
85
+ return;
86
+ const record = await store.get(existingId);
87
+ if (!record)
88
+ return;
89
+ if (record.status !== "waiting") {
90
+ await deps.storage.deleteAlarm?.();
91
+ return;
92
+ }
93
+ const now = (deps.now ?? (() => Date.now()))();
94
+ const stillPending = [];
95
+ let resolvedAny = false;
96
+ for (const wp of record.pendingWaitpoints) {
97
+ const wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined;
98
+ if (wp.kind === "DATETIME" && wakeAt !== undefined && wakeAt <= now) {
99
+ record.journal.waitpointsResolved[wp.clientWaitpointId] = {
100
+ kind: "DATETIME",
101
+ resolvedAt: now,
102
+ source: "replay",
103
+ };
104
+ resolvedAny = true;
105
+ }
106
+ else {
107
+ stillPending.push(wp);
108
+ }
109
+ }
110
+ record.pendingWaitpoints = stillPending;
111
+ if (!resolvedAny) {
112
+ // Spurious alarm — nothing due. Persist and reconcile.
113
+ await store.save(record);
114
+ await reconcileAlarm(record, store, deps);
115
+ return;
116
+ }
117
+ record.status = "running";
118
+ const handler = deps.resolveStepHandler(record.tenantMeta.tenantScript ?? "");
119
+ await driveUntilPaused(record, { handler, now: deps.now });
120
+ await store.save(record);
121
+ await reconcileAlarm(record, store, deps);
122
+ }
123
+ /**
124
+ * Look at the record's pending waitpoints; if any are DATETIME,
125
+ * stamp a `wakeAt` into meta (if not already set) and schedule the
126
+ * earliest via `setAlarm`. Clears any prior alarm when the run is
127
+ * terminal or has no DATETIME waitpoints left.
128
+ */
129
+ async function reconcileAlarm(record, store, deps) {
130
+ const now = (deps.now ?? (() => Date.now()))();
131
+ if (record.status !== "waiting") {
132
+ await deps.storage.deleteAlarm?.();
133
+ return;
134
+ }
135
+ let earliest;
136
+ let dirty = false;
137
+ for (const wp of record.pendingWaitpoints) {
138
+ if (wp.kind !== "DATETIME")
139
+ continue;
140
+ let wakeAt = typeof wp.meta.wakeAt === "number" ? wp.meta.wakeAt : undefined;
141
+ if (wakeAt === undefined) {
142
+ const ms = wp.timeoutMs ?? (typeof wp.meta.durationMs === "number" ? wp.meta.durationMs : 0);
143
+ wakeAt = now + ms;
144
+ wp.meta.wakeAt = wakeAt;
145
+ dirty = true;
146
+ }
147
+ earliest = earliest === undefined ? wakeAt : Math.min(earliest, wakeAt);
148
+ }
149
+ if (dirty)
150
+ await store.save(record);
151
+ if (earliest !== undefined) {
152
+ await deps.storage.setAlarm?.(earliest);
153
+ }
154
+ else {
155
+ await deps.storage.deleteAlarm?.();
156
+ }
157
+ }
158
+ /**
159
+ * One DO holds one run, keyed by `record`. Returns the id of that
160
+ * stored run, if any.
161
+ */
162
+ async function getStoredRunId(store) {
163
+ const all = await store.list();
164
+ return all[0]?.id;
165
+ }
166
+ function json(status, body) {
167
+ return new Response(JSON.stringify(body), {
168
+ status,
169
+ headers: { "content-type": "application/json; charset=utf-8" },
170
+ });
171
+ }
172
+ // Re-exported for callers building their own DO class via composition.
173
+ export { applyWaitpointInjection, driveUntilPaused };
@@ -0,0 +1,8 @@
1
+ export { type BundleLocation, type CfContainerRunnerDeps, type ContainerNamespaceLike, createCfContainerStepRunner, } from "./cf-container-runner.js";
2
+ export { createDispatchStepHandler, type DispatchHandlerDeps, } from "./dispatch-handler.js";
3
+ export { createDurableObjectRunStore } from "./do-store.js";
4
+ export { type DurableObjectDeps, handleDurableObjectAlarm, handleDurableObjectRequest, } from "./durable-object.js";
5
+ export { createR2Presigner, type PresignArgs, type R2PresignerOptions, } from "./r2-sign.js";
6
+ export * from "./types.js";
7
+ export { type DurableObjectNamespaceLike, handleWorkerRequest, type WorkerFetchDeps, } from "./worker.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,2BAA2B,GAC5B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,GACzB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,EACL,KAAK,iBAAiB,EACtB,wBAAwB,EACxB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,GACxB,MAAM,cAAc,CAAA;AACrB,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,KAAK,0BAA0B,EAC/B,mBAAmB,EACnB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,34 @@
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
+ export { createCfContainerStepRunner, } from "./cf-container-runner.js";
29
+ export { createDispatchStepHandler, } from "./dispatch-handler.js";
30
+ export { createDurableObjectRunStore } from "./do-store.js";
31
+ export { handleDurableObjectAlarm, handleDurableObjectRequest, } from "./durable-object.js";
32
+ export { createR2Presigner, } from "./r2-sign.js";
33
+ export * from "./types.js";
34
+ export { handleWorkerRequest, } from "./worker.js";
@@ -0,0 +1,18 @@
1
+ export interface R2PresignerOptions {
2
+ /** Cloudflare account id (32-char hex). */
3
+ accountId: string;
4
+ /** R2 Access Key ID from your CF dashboard — scope read-only. */
5
+ accessKeyId: string;
6
+ /** R2 Secret Access Key. Store as a Worker Secret. */
7
+ secretAccessKey: string;
8
+ /** R2 bucket name. */
9
+ bucket: string;
10
+ }
11
+ export interface PresignArgs {
12
+ /** Object key, e.g. `"prj_42/v1/container.mjs"`. Leading `/` is optional. */
13
+ key: string;
14
+ /** Seconds until the URL stops being valid. Min 1, max 604800. */
15
+ expiresIn: number;
16
+ }
17
+ export declare function createR2Presigner(opts: R2PresignerOptions): (args: PresignArgs) => Promise<string>;
18
+ //# sourceMappingURL=r2-sign.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"r2-sign.d.ts","sourceRoot":"","sources":["../src/r2-sign.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,kBAAkB;IACjC,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,WAAW,EAAE,MAAM,CAAA;IACnB,sDAAsD;IACtD,eAAe,EAAE,MAAM,CAAA;IACvB,sBAAsB;IACtB,MAAM,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,6EAA6E;IAC7E,GAAG,EAAE,MAAM,CAAA;IACX,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,kBAAkB,GACvB,CAAC,IAAI,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,CAsDxC"}