@voyantjs/workflows-orchestrator-cloudflare 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -11
- package/dist/cloudflare-edge-driver.d.ts +49 -0
- package/dist/cloudflare-edge-driver.d.ts.map +1 -0
- package/dist/cloudflare-edge-driver.js +317 -0
- package/dist/dispatchers.d.ts +87 -0
- package/dist/dispatchers.d.ts.map +1 -0
- package/dist/dispatchers.js +83 -0
- package/dist/do-store.d.ts.map +1 -1
- package/dist/do-store.js +12 -0
- package/dist/durable-object.d.ts +13 -6
- package/dist/durable-object.d.ts.map +1 -1
- package/dist/durable-object.js +13 -4
- package/dist/event-handler.d.ts +23 -0
- package/dist/event-handler.d.ts.map +1 -0
- package/dist/event-handler.js +241 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/manifest-handler.d.ts +16 -0
- package/dist/manifest-handler.d.ts.map +1 -0
- package/dist/manifest-handler.js +92 -0
- package/dist/manifest-kv-store.d.ts +59 -0
- package/dist/manifest-kv-store.d.ts.map +1 -0
- package/dist/manifest-kv-store.js +134 -0
- package/dist/types.d.ts +7 -15
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.d.ts +19 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +41 -0
- package/package.json +3 -3
- package/src/cloudflare-edge-driver.ts +435 -0
- package/src/dispatchers.ts +162 -0
- package/src/do-store.ts +13 -0
- package/src/durable-object.ts +30 -9
- package/src/event-handler.ts +302 -0
- package/src/index.ts +32 -8
- package/src/manifest-handler.ts +113 -0
- package/src/manifest-kv-store.ts +186 -0
- package/src/types.ts +7 -19
- package/src/worker.ts +64 -0
- package/dist/dispatch-handler.d.ts +0 -20
- package/dist/dispatch-handler.d.ts.map +0 -1
- package/dist/dispatch-handler.js +0 -31
- package/src/dispatch-handler.ts +0 -51
package/README.md
CHANGED
|
@@ -1,21 +1,36 @@
|
|
|
1
1
|
# @voyantjs/workflows-orchestrator-cloudflare
|
|
2
2
|
|
|
3
3
|
Cloudflare Worker + Durable Object adapter for
|
|
4
|
-
[`@voyantjs/workflows-orchestrator`](../workflows-orchestrator). Composes
|
|
5
|
-
protocol-agnostic state machine with DO-backed storage and a
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
[`@voyantjs/workflows-orchestrator`](../workflows-orchestrator). Composes
|
|
5
|
+
the protocol-agnostic state machine with DO-backed storage and a
|
|
6
|
+
pluggable **step dispatcher** that delivers step requests to wherever
|
|
7
|
+
workflow code lives.
|
|
8
8
|
|
|
9
9
|
This package is the building block; the deployable artifact lives in
|
|
10
|
-
[`apps/workflows-orchestrator-worker`](../../apps/workflows-orchestrator-worker),
|
|
11
|
-
wires it into a `wrangler.jsonc` + default-exports.
|
|
10
|
+
[`apps/workflows-orchestrator-worker`](../../apps/workflows-orchestrator-worker),
|
|
11
|
+
which wires it into a `wrangler.jsonc` + default-exports.
|
|
12
|
+
|
|
13
|
+
## Picking a dispatcher
|
|
14
|
+
|
|
15
|
+
The orchestrator forwards step requests through a `StepDispatcher`. Pick
|
|
16
|
+
the factory that matches your deployment:
|
|
17
|
+
|
|
18
|
+
| Factory | Use case | Bindings needed |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `createInlineDispatcher` | Single-Worker (workflows + API in same isolate) | None |
|
|
21
|
+
| `createServiceBindingDispatcher` | Two-Worker (orchestrator + sibling workflows Worker) | Service binding |
|
|
22
|
+
| `createHttpDispatcher` | Cross-host (e.g. CF orchestrator → Node-side workflows) | HTTP endpoint |
|
|
23
|
+
|
|
24
|
+
Hosted multi-tenant providers implement custom `StepDispatcher`s in
|
|
25
|
+
their own deployment code — multi-tenancy is a deployment concern, not
|
|
26
|
+
a runtime one, so it doesn't ship here.
|
|
12
27
|
|
|
13
28
|
```ts
|
|
14
29
|
import {
|
|
15
30
|
handleWorkerRequest,
|
|
16
31
|
handleDurableObjectRequest,
|
|
17
32
|
handleDurableObjectAlarm,
|
|
18
|
-
|
|
33
|
+
createServiceBindingDispatcher,
|
|
19
34
|
} from "@voyantjs/workflows-orchestrator-cloudflare";
|
|
20
35
|
|
|
21
36
|
export default {
|
|
@@ -38,10 +53,7 @@ export class WorkflowRunDO implements DurableObject {
|
|
|
38
53
|
private deps() {
|
|
39
54
|
return {
|
|
40
55
|
storage: this.state.storage,
|
|
41
|
-
|
|
42
|
-
createDispatchStepHandler(tenantScript, {
|
|
43
|
-
dispatcher: this.env.DISPATCHER,
|
|
44
|
-
}),
|
|
56
|
+
dispatcher: createServiceBindingDispatcher({ binding: this.env.WORKFLOWS }),
|
|
45
57
|
};
|
|
46
58
|
}
|
|
47
59
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EnvironmentName } from "@voyantjs/workflows";
|
|
2
|
+
import type { DriverFactory } from "@voyantjs/workflows/driver";
|
|
3
|
+
import { type KvNamespaceLike } from "./manifest-kv-store.js";
|
|
4
|
+
import type { DurableObjectNamespaceLike } from "./worker.js";
|
|
5
|
+
export interface CloudflareEdgeDriverOptions {
|
|
6
|
+
/** Durable Object namespace holding one DO per run. */
|
|
7
|
+
orchestratorNamespace: DurableObjectNamespaceLike;
|
|
8
|
+
/** KV namespace storing serialized manifests. */
|
|
9
|
+
manifestKv: KvNamespaceLike;
|
|
10
|
+
/**
|
|
11
|
+
* Adapter-specific tenant identifier stamped onto every triggered
|
|
12
|
+
* run as `tenantMeta.tenantScript`. Opaque to the OSS runtime —
|
|
13
|
+
* surfaces on `StepDispatcherContext` for custom dispatchers that
|
|
14
|
+
* need a routing key. Built-in dispatchers (inline, service-binding,
|
|
15
|
+
* HTTP) ignore it.
|
|
16
|
+
*/
|
|
17
|
+
tenantScript?: string;
|
|
18
|
+
/** Default environment for `trigger()` calls without an explicit one. */
|
|
19
|
+
defaultEnvironment?: EnvironmentName;
|
|
20
|
+
/** Tenant metadata stamped onto every triggered run. Defaults to "default" tripled. */
|
|
21
|
+
tenantMeta?: {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
projectId: string;
|
|
24
|
+
organizationId: string;
|
|
25
|
+
};
|
|
26
|
+
/** Injectable clock; defaults to Date.now. */
|
|
27
|
+
now?: () => number;
|
|
28
|
+
/** id generator for runs; defaults to `run_<random>`. */
|
|
29
|
+
idGenerator?: () => string;
|
|
30
|
+
/** Optional structured logger; falls back to the framework logger. */
|
|
31
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the Cloudflare-edge driver factory. The returned `DriverFactory`
|
|
35
|
+
* is invoked once by `createApp()` with `DriverFactoryDeps`.
|
|
36
|
+
*
|
|
37
|
+
* Usage in a Worker template:
|
|
38
|
+
*
|
|
39
|
+
* createApp({
|
|
40
|
+
* workflows: {
|
|
41
|
+
* driver: createCloudflareEdgeDriver({
|
|
42
|
+
* orchestratorNamespace: env.WORKFLOW_RUN_DO,
|
|
43
|
+
* manifestKv: env.WORKFLOW_MANIFESTS,
|
|
44
|
+
* }),
|
|
45
|
+
* },
|
|
46
|
+
* })
|
|
47
|
+
*/
|
|
48
|
+
export declare function createCloudflareEdgeDriver(opts: CloudflareEdgeDriverOptions): DriverFactory;
|
|
49
|
+
//# sourceMappingURL=cloudflare-edge-driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-edge-driver.d.ts","sourceRoot":"","sources":["../src/cloudflare-edge-driver.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EACV,eAAe,EAMhB,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EACV,aAAa,EAOd,MAAM,4BAA4B,CAAA;AAKnC,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,wBAAwB,CAAA;AAC/B,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAI7D,MAAM,WAAW,2BAA2B;IAC1C,uDAAuD;IACvD,qBAAqB,EAAE,0BAA0B,CAAA;IACjD,iDAAiD;IACjD,UAAU,EAAE,eAAe,CAAA;IAC3B;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,yEAAyE;IACzE,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,uFAAuF;IACvF,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;KACvB,CAAA;IACD,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChF;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,2BAA2B,GAAG,aAAa,CA8T3F"}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
// Mode 1 driver — Cloudflare edge composition.
|
|
2
|
+
//
|
|
3
|
+
// `createApp({ workflows: { driver: createCloudflareEdgeDriver({ ... }) } })`
|
|
4
|
+
// is the entry point for any deployment that runs the orchestrator on
|
|
5
|
+
// Cloudflare Workers + Durable Objects. Composes:
|
|
6
|
+
//
|
|
7
|
+
// * `voyant_run_DO` (Durable Object namespace) — primary state
|
|
8
|
+
// * `WORKFLOW_MANIFESTS` KV namespace — manifest store
|
|
9
|
+
//
|
|
10
|
+
// Step delivery is configured separately on the run DO via a
|
|
11
|
+
// `StepDispatcher` — see `./dispatchers.ts` for built-in factories
|
|
12
|
+
// (inline / service binding / HTTP).
|
|
13
|
+
//
|
|
14
|
+
// The factory is invoked by `createApp()` after the framework's
|
|
15
|
+
// `ModuleContainer` is built — see architecture doc §6.3 for the
|
|
16
|
+
// `DriverFactory` contract.
|
|
17
|
+
//
|
|
18
|
+
// See architecture doc §8.
|
|
19
|
+
import { deriveStableEventId } from "@voyantjs/workflows/events";
|
|
20
|
+
import { routeEvent } from "@voyantjs/workflows-orchestrator";
|
|
21
|
+
import { createKvManifestStore, } from "./manifest-kv-store.js";
|
|
22
|
+
const DEFAULT_TENANT_META = {
|
|
23
|
+
tenantId: "default",
|
|
24
|
+
projectId: "default",
|
|
25
|
+
organizationId: "default",
|
|
26
|
+
};
|
|
27
|
+
// ---- Public factory ----
|
|
28
|
+
/**
|
|
29
|
+
* Build the Cloudflare-edge driver factory. The returned `DriverFactory`
|
|
30
|
+
* is invoked once by `createApp()` with `DriverFactoryDeps`.
|
|
31
|
+
*
|
|
32
|
+
* Usage in a Worker template:
|
|
33
|
+
*
|
|
34
|
+
* createApp({
|
|
35
|
+
* workflows: {
|
|
36
|
+
* driver: createCloudflareEdgeDriver({
|
|
37
|
+
* orchestratorNamespace: env.WORKFLOW_RUN_DO,
|
|
38
|
+
* manifestKv: env.WORKFLOW_MANIFESTS,
|
|
39
|
+
* }),
|
|
40
|
+
* },
|
|
41
|
+
* })
|
|
42
|
+
*/
|
|
43
|
+
export function createCloudflareEdgeDriver(opts) {
|
|
44
|
+
return (deps) => {
|
|
45
|
+
const manifestStore = createKvManifestStore({ kv: opts.manifestKv });
|
|
46
|
+
const now = opts.now ?? deps.now ?? (() => Date.now());
|
|
47
|
+
const tenantMeta = {
|
|
48
|
+
...DEFAULT_TENANT_META,
|
|
49
|
+
...(opts.tenantMeta ?? {}),
|
|
50
|
+
...(opts.tenantScript ? { tenantScript: opts.tenantScript } : {}),
|
|
51
|
+
};
|
|
52
|
+
const defaultEnv = opts.defaultEnvironment ?? "development";
|
|
53
|
+
const logger = opts.logger ?? deps.logger;
|
|
54
|
+
let shuttingDown = false;
|
|
55
|
+
// ---- Helpers ----
|
|
56
|
+
function assertNotShutdown() {
|
|
57
|
+
if (shuttingDown) {
|
|
58
|
+
throw new Error("CloudflareEdgeDriver: shutdown() has been called; new operations are refused.");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function forwardToRunDO(runId, request) {
|
|
62
|
+
const id = opts.orchestratorNamespace.idFromName(runId);
|
|
63
|
+
const stub = opts.orchestratorNamespace.get(id);
|
|
64
|
+
return stub.fetch(request);
|
|
65
|
+
}
|
|
66
|
+
function genRunId(seed) {
|
|
67
|
+
if (seed !== undefined)
|
|
68
|
+
return seed;
|
|
69
|
+
if (opts.idGenerator)
|
|
70
|
+
return opts.idGenerator();
|
|
71
|
+
const ts = now().toString(36);
|
|
72
|
+
const rand = Math.floor(Math.random() * 1_000_000)
|
|
73
|
+
.toString(36)
|
|
74
|
+
.padStart(4, "0");
|
|
75
|
+
return `run_${ts}_${rand}`;
|
|
76
|
+
}
|
|
77
|
+
// ---- WorkflowDriver implementation ----
|
|
78
|
+
async function registerManifest(args) {
|
|
79
|
+
assertNotShutdown();
|
|
80
|
+
return manifestStore.registerManifest({
|
|
81
|
+
environment: args.environment,
|
|
82
|
+
versionId: args.manifest.versionId,
|
|
83
|
+
manifest: args.manifest,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function getManifest(args) {
|
|
87
|
+
const envelope = await manifestStore.getCurrent(args.environment);
|
|
88
|
+
if (!envelope)
|
|
89
|
+
return null;
|
|
90
|
+
return envelope.manifest;
|
|
91
|
+
}
|
|
92
|
+
async function trigger(workflow, input, triggerOpts) {
|
|
93
|
+
assertNotShutdown();
|
|
94
|
+
const workflowId = typeof workflow === "string" ? workflow : workflow.id;
|
|
95
|
+
const env = triggerOpts?.environment ?? defaultEnv;
|
|
96
|
+
const runId = triggerOpts?.idempotencyKey !== undefined
|
|
97
|
+
? `idem-${workflowId}-${triggerOpts.idempotencyKey}`
|
|
98
|
+
: genRunId();
|
|
99
|
+
const payload = {
|
|
100
|
+
runId,
|
|
101
|
+
workflowId,
|
|
102
|
+
workflowVersion: triggerOpts?.lockToVersion ?? "v1",
|
|
103
|
+
input: input,
|
|
104
|
+
tenantMeta,
|
|
105
|
+
environment: env,
|
|
106
|
+
tags: triggerOpts?.tags,
|
|
107
|
+
idempotencyKey: triggerOpts?.idempotencyKey,
|
|
108
|
+
triggeredBy: { kind: "api" },
|
|
109
|
+
};
|
|
110
|
+
const resp = await forwardToRunDO(runId, new Request("https://do-internal/trigger", {
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers: { "content-type": "application/json" },
|
|
113
|
+
body: JSON.stringify(payload),
|
|
114
|
+
}));
|
|
115
|
+
if (!resp.ok) {
|
|
116
|
+
const body = await safeText(resp);
|
|
117
|
+
throw new Error(`CloudflareEdgeDriver: trigger DO returned ${resp.status}: ${body.slice(0, 256)}`);
|
|
118
|
+
}
|
|
119
|
+
const record = (await resp.json());
|
|
120
|
+
return {
|
|
121
|
+
id: record.id,
|
|
122
|
+
workflowId: record.workflowId,
|
|
123
|
+
status: record.status,
|
|
124
|
+
startedAt: record.startedAt,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function ingestEvent(args) {
|
|
128
|
+
assertNotShutdown();
|
|
129
|
+
const stored = await manifestStore.getCurrent(args.environment);
|
|
130
|
+
if (!stored) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
reason: "manifest_not_registered",
|
|
134
|
+
message: `No manifest is registered for environment "${args.environment}".`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const manifest = stored.manifest;
|
|
138
|
+
const eventId = args.envelope.metadata?.eventId ?? (await deriveStableEventId(args.envelope));
|
|
139
|
+
const routed = routeEvent({
|
|
140
|
+
manifest,
|
|
141
|
+
envelope: {
|
|
142
|
+
name: args.envelope.name,
|
|
143
|
+
data: args.envelope.data,
|
|
144
|
+
metadata: args.envelope.metadata,
|
|
145
|
+
emittedAt: args.envelope.emittedAt,
|
|
146
|
+
},
|
|
147
|
+
eventId,
|
|
148
|
+
idempotencyOverride: args.idempotencyKey,
|
|
149
|
+
});
|
|
150
|
+
const matches = [];
|
|
151
|
+
let anyTriggered = false;
|
|
152
|
+
let anyFailed = false;
|
|
153
|
+
for (const entry of routed) {
|
|
154
|
+
if (entry.status === "skipped") {
|
|
155
|
+
matches.push({
|
|
156
|
+
filterId: entry.filterId,
|
|
157
|
+
status: "skipped",
|
|
158
|
+
reason: entry.reason,
|
|
159
|
+
details: entry.details,
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const runId = `idem-${entry.targetWorkflowId}-${entry.idempotencyKey}`;
|
|
164
|
+
const payload = {
|
|
165
|
+
runId,
|
|
166
|
+
workflowId: entry.targetWorkflowId,
|
|
167
|
+
workflowVersion: "v1",
|
|
168
|
+
input: entry.input,
|
|
169
|
+
tenantMeta,
|
|
170
|
+
environment: args.environment,
|
|
171
|
+
idempotencyKey: entry.idempotencyKey,
|
|
172
|
+
triggeredBy: {
|
|
173
|
+
kind: "event",
|
|
174
|
+
eventId,
|
|
175
|
+
eventType: args.envelope.name,
|
|
176
|
+
filterId: entry.filterId,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
try {
|
|
180
|
+
const resp = await forwardToRunDO(runId, new Request("https://do-internal/trigger", {
|
|
181
|
+
method: "POST",
|
|
182
|
+
headers: { "content-type": "application/json" },
|
|
183
|
+
body: JSON.stringify(payload),
|
|
184
|
+
}));
|
|
185
|
+
if (resp.ok) {
|
|
186
|
+
matches.push({
|
|
187
|
+
filterId: entry.filterId,
|
|
188
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
189
|
+
runId,
|
|
190
|
+
idempotencyKey: entry.idempotencyKey,
|
|
191
|
+
status: "queued",
|
|
192
|
+
});
|
|
193
|
+
anyTriggered = true;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const body = await safeText(resp);
|
|
197
|
+
logger?.("error", "CloudflareEdgeDriver: trigger DO failed", {
|
|
198
|
+
status: resp.status,
|
|
199
|
+
body: body.slice(0, 256),
|
|
200
|
+
});
|
|
201
|
+
matches.push({
|
|
202
|
+
filterId: entry.filterId,
|
|
203
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
204
|
+
status: "error",
|
|
205
|
+
reason: `do_returned_${resp.status}`,
|
|
206
|
+
});
|
|
207
|
+
anyFailed = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
matches.push({
|
|
212
|
+
filterId: entry.filterId,
|
|
213
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
214
|
+
status: "error",
|
|
215
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
216
|
+
});
|
|
217
|
+
anyFailed = true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (matches.length > 0 && !anyTriggered && anyFailed) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
reason: "trigger_failed_for_all_matches",
|
|
224
|
+
message: "every matched filter failed to trigger",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return { ok: true, eventId, matches };
|
|
228
|
+
}
|
|
229
|
+
async function shutdown() {
|
|
230
|
+
shuttingDown = true;
|
|
231
|
+
}
|
|
232
|
+
// ---- WorkflowAdmin (partial — Mode 1 has no native cross-run query
|
|
233
|
+
// layer; getRun + cancelRun are direct DO RPC; listRuns +
|
|
234
|
+
// streamRun are explicitly unsupported per architecture
|
|
235
|
+
// doc §8.3) ----
|
|
236
|
+
const admin = {
|
|
237
|
+
async getRun(runId) {
|
|
238
|
+
try {
|
|
239
|
+
const resp = await forwardToRunDO(runId, new Request("https://do-internal/get", { method: "GET" }));
|
|
240
|
+
if (resp.status === 404)
|
|
241
|
+
return null;
|
|
242
|
+
if (!resp.ok)
|
|
243
|
+
return null;
|
|
244
|
+
const rec = (await resp.json());
|
|
245
|
+
return {
|
|
246
|
+
id: rec.id,
|
|
247
|
+
workflowId: rec.workflowId,
|
|
248
|
+
status: rec.status,
|
|
249
|
+
startedAt: rec.startedAt,
|
|
250
|
+
completedAt: rec.completedAt,
|
|
251
|
+
tags: [...rec.tags],
|
|
252
|
+
environment: rec.environment,
|
|
253
|
+
version: rec.workflowVersion,
|
|
254
|
+
input: rec.input,
|
|
255
|
+
output: rec.output,
|
|
256
|
+
error: rec.error,
|
|
257
|
+
durationMs: rec.completedAt !== undefined
|
|
258
|
+
? Math.max(0, rec.completedAt - rec.startedAt)
|
|
259
|
+
: undefined,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
async cancelRun(runId, cancelOpts) {
|
|
267
|
+
// Per architecture doc §21.21, cancel does NOT run compensations
|
|
268
|
+
// by default; the `compensate` flag is accepted but no-op in v1.
|
|
269
|
+
void cancelOpts?.compensate;
|
|
270
|
+
await forwardToRunDO(runId, new Request("https://do-internal/cancel", {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers: { "content-type": "application/json" },
|
|
273
|
+
body: JSON.stringify({ reason: cancelOpts?.reason }),
|
|
274
|
+
}));
|
|
275
|
+
},
|
|
276
|
+
async listRuns(_listOpts) {
|
|
277
|
+
// Self-host Mode 1 has no native cross-run query layer; voyant-cloud
|
|
278
|
+
// provides one in its index repo. Surface the limit to consumers
|
|
279
|
+
// (the dashboard) so they can fall back gracefully.
|
|
280
|
+
return { runs: [], nextCursor: undefined };
|
|
281
|
+
},
|
|
282
|
+
streamRun(_runId) {
|
|
283
|
+
// Live journal-event streaming is a follow-up; return an
|
|
284
|
+
// immediately-exhausted iterable so probes see a clean empty
|
|
285
|
+
// stream rather than undefined.
|
|
286
|
+
return {
|
|
287
|
+
[Symbol.asyncIterator]() {
|
|
288
|
+
return {
|
|
289
|
+
next: async () => ({ value: undefined, done: true }),
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
return {
|
|
296
|
+
registerManifest,
|
|
297
|
+
trigger,
|
|
298
|
+
ingestEvent,
|
|
299
|
+
getManifest,
|
|
300
|
+
shutdown,
|
|
301
|
+
admin,
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// ---- Internal helpers ----
|
|
306
|
+
// Fallback id derivation lives in `@voyantjs/workflows/events`'s
|
|
307
|
+
// `deriveStableEventId` and is used inline at the call site above —
|
|
308
|
+
// content-derived so external callers without a forwarder still get
|
|
309
|
+
// dedup across retries (architecture doc §15.2).
|
|
310
|
+
async function safeText(resp) {
|
|
311
|
+
try {
|
|
312
|
+
return await resp.text();
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return "";
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { type StepHandler } from "@voyantjs/workflows-orchestrator";
|
|
2
|
+
/**
|
|
3
|
+
* Context the run DO supplies when asking a dispatcher for a
|
|
4
|
+
* StepHandler. Most dispatchers ignore it; it carries the run's
|
|
5
|
+
* adapter-specific tenant identifier (when set) and the workflow id
|
|
6
|
+
* for logging / per-tenant routing in custom dispatchers.
|
|
7
|
+
*/
|
|
8
|
+
export interface StepDispatcherContext {
|
|
9
|
+
/**
|
|
10
|
+
* Adapter-specific tenant identifier from the run's `tenantMeta`.
|
|
11
|
+
* Opaque to the OSS runtime — interpretation is up to whichever
|
|
12
|
+
* dispatcher consumes it (e.g. a custom multi-tenant dispatcher
|
|
13
|
+
* may use it as a routing key).
|
|
14
|
+
*/
|
|
15
|
+
tenantScript?: string;
|
|
16
|
+
/** Workflow id, useful for label / logging. */
|
|
17
|
+
workflowId?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pluggable step-dispatch primitive. The run DO calls the dispatcher
|
|
21
|
+
* once per drive and forwards step requests through the handler it
|
|
22
|
+
* returns.
|
|
23
|
+
*
|
|
24
|
+
* Pick a factory below based on where workflow code lives in your
|
|
25
|
+
* deployment, or implement the type directly for custom transports.
|
|
26
|
+
*/
|
|
27
|
+
export type StepDispatcher = (ctx: StepDispatcherContext) => StepHandler;
|
|
28
|
+
/**
|
|
29
|
+
* Subset of a Cloudflare service-binding interface (`env.SOMETHING`
|
|
30
|
+
* declared as `services: [{ binding, service }]` in wrangler.jsonc).
|
|
31
|
+
* `fetch(req)` delivers to the bound Worker.
|
|
32
|
+
*/
|
|
33
|
+
export interface ServiceBindingLike {
|
|
34
|
+
fetch(request: Request): Promise<Response>;
|
|
35
|
+
}
|
|
36
|
+
export interface ServiceBindingDispatcherOptions {
|
|
37
|
+
/** Service binding to a sibling Worker that hosts the workflow code. */
|
|
38
|
+
binding: ServiceBindingLike;
|
|
39
|
+
/** Optional HMAC signer. */
|
|
40
|
+
sign?: (body: string) => Promise<string> | string;
|
|
41
|
+
/** Optional structured logger. */
|
|
42
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
|
|
43
|
+
/** URL presented to the bound Worker. Defaults to `https://tenant.voyant.internal`. */
|
|
44
|
+
baseUrl?: string;
|
|
45
|
+
/** Optional label for logs. Defaults to `"service-binding"`. */
|
|
46
|
+
label?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Service-binding dispatcher. Routes step requests to a sibling Worker
|
|
50
|
+
* via `services: [{ binding, service }]` in the orchestrator's
|
|
51
|
+
* wrangler.jsonc. Use this for self-host two-Worker deployments
|
|
52
|
+
* (orchestrator + workflows are separate Workers in the same account).
|
|
53
|
+
* No WfP needed; works on the standard Workers paid plan.
|
|
54
|
+
*/
|
|
55
|
+
export declare function createServiceBindingDispatcher(opts: ServiceBindingDispatcherOptions): StepDispatcher;
|
|
56
|
+
/**
|
|
57
|
+
* Inline dispatcher. Returns the supplied StepHandler directly — used
|
|
58
|
+
* when workflow code lives in the SAME Worker as the orchestrator
|
|
59
|
+
* (single-Worker self-host). No HTTP, no DO traversal, just a function
|
|
60
|
+
* call. Pair with `handleStepRequest` from `@voyantjs/workflows/handler`
|
|
61
|
+
* for the typical setup.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createInlineDispatcher(handler: StepHandler): StepDispatcher;
|
|
64
|
+
export interface HttpDispatcherOptions {
|
|
65
|
+
/** Absolute URL of the workflow-step endpoint. */
|
|
66
|
+
url: string;
|
|
67
|
+
/** Optional HMAC signer. */
|
|
68
|
+
sign?: (body: string) => Promise<string> | string;
|
|
69
|
+
/** Optional structured logger. */
|
|
70
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Optional fetch override (e.g. `env.SOMETHING.fetch.bind(env.SOMETHING)`
|
|
73
|
+
* for typed bindings, or a custom client for testing). Defaults to
|
|
74
|
+
* `globalThis.fetch`.
|
|
75
|
+
*/
|
|
76
|
+
fetch?: (request: Request) => Promise<Response>;
|
|
77
|
+
/** Optional label for logs; defaults to the URL host. */
|
|
78
|
+
label?: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* HTTP dispatcher. Routes step requests to a configurable URL via
|
|
82
|
+
* `globalThis.fetch` (or a supplied fetch). Use for cross-host setups
|
|
83
|
+
* (e.g. a CF orchestrator forwarding to a Node-side workflows Worker)
|
|
84
|
+
* or test fakes.
|
|
85
|
+
*/
|
|
86
|
+
export declare function createHttpDispatcher(opts: HttpDispatcherOptions): StepDispatcher;
|
|
87
|
+
//# sourceMappingURL=dispatchers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatchers.d.ts","sourceRoot":"","sources":["../src/dispatchers.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAyB,KAAK,WAAW,EAAE,MAAM,kCAAkC,CAAA;AAE1F;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,qBAAqB,KAAK,WAAW,CAAA;AAIxE;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3C;AAED,MAAM,WAAW,+BAA+B;IAC9C,wEAAwE;IACxE,OAAO,EAAE,kBAAkB,CAAA;IAC3B,4BAA4B;IAC5B,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,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,+BAA+B,GACpC,cAAc,CAiBhB;AAID;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CAE3E;AAID,MAAM,WAAW,qBAAqB;IACpC,kDAAkD;IAClD,GAAG,EAAE,MAAM,CAAA;IACX,4BAA4B;IAC5B,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;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC/C,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,qBAAqB,GAAG,cAAc,CAwBhF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Step dispatchers — abstract "given a run's context, produce a
|
|
2
|
+
// StepHandler that delivers step requests to whatever Worker (or
|
|
3
|
+
// isolate) hosts the workflow code."
|
|
4
|
+
//
|
|
5
|
+
// The OSS runtime ships three universal factories:
|
|
6
|
+
// * createInlineDispatcher — same isolate as the orchestrator
|
|
7
|
+
// * createServiceBindingDispatcher — sibling Worker via service binding
|
|
8
|
+
// * createHttpDispatcher — arbitrary HTTP endpoint
|
|
9
|
+
//
|
|
10
|
+
// Hosted multi-tenant providers (Voyant Cloud, etc.) implement their
|
|
11
|
+
// own dispatchers in their private deployment code — Workers-for-
|
|
12
|
+
// Platforms is one such option, but it doesn't belong in the OSS
|
|
13
|
+
// runtime because it bakes in a CF-specific multi-tenancy story that
|
|
14
|
+
// most self-host users don't need or want.
|
|
15
|
+
//
|
|
16
|
+
// See issue #528 + docs/architecture/workflows-runtime-architecture.md §8.
|
|
17
|
+
import { createHttpStepHandler } from "@voyantjs/workflows-orchestrator";
|
|
18
|
+
/**
|
|
19
|
+
* Service-binding dispatcher. Routes step requests to a sibling Worker
|
|
20
|
+
* via `services: [{ binding, service }]` in the orchestrator's
|
|
21
|
+
* wrangler.jsonc. Use this for self-host two-Worker deployments
|
|
22
|
+
* (orchestrator + workflows are separate Workers in the same account).
|
|
23
|
+
* No WfP needed; works on the standard Workers paid plan.
|
|
24
|
+
*/
|
|
25
|
+
export function createServiceBindingDispatcher(opts) {
|
|
26
|
+
const baseUrl = opts.baseUrl ?? "https://tenant.voyant.internal";
|
|
27
|
+
const label = opts.label ?? "service-binding";
|
|
28
|
+
return (_ctx) => createHttpStepHandler({
|
|
29
|
+
sign: opts.sign ? (body) => opts.sign(body) : undefined,
|
|
30
|
+
logger: opts.logger,
|
|
31
|
+
resolveTarget() {
|
|
32
|
+
return {
|
|
33
|
+
url: `${baseUrl}/__voyant/workflow-step`,
|
|
34
|
+
label,
|
|
35
|
+
fetch(request) {
|
|
36
|
+
return opts.binding.fetch(request);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// ---- Factory: Inline ----
|
|
43
|
+
/**
|
|
44
|
+
* Inline dispatcher. Returns the supplied StepHandler directly — used
|
|
45
|
+
* when workflow code lives in the SAME Worker as the orchestrator
|
|
46
|
+
* (single-Worker self-host). No HTTP, no DO traversal, just a function
|
|
47
|
+
* call. Pair with `handleStepRequest` from `@voyantjs/workflows/handler`
|
|
48
|
+
* for the typical setup.
|
|
49
|
+
*/
|
|
50
|
+
export function createInlineDispatcher(handler) {
|
|
51
|
+
return () => handler;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* HTTP dispatcher. Routes step requests to a configurable URL via
|
|
55
|
+
* `globalThis.fetch` (or a supplied fetch). Use for cross-host setups
|
|
56
|
+
* (e.g. a CF orchestrator forwarding to a Node-side workflows Worker)
|
|
57
|
+
* or test fakes.
|
|
58
|
+
*/
|
|
59
|
+
export function createHttpDispatcher(opts) {
|
|
60
|
+
const fetchImpl = opts.fetch ?? ((req) => globalThis.fetch(req));
|
|
61
|
+
let label = opts.label;
|
|
62
|
+
if (!label) {
|
|
63
|
+
try {
|
|
64
|
+
label = new URL(opts.url).host;
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
label = opts.url;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return (_ctx) => createHttpStepHandler({
|
|
71
|
+
sign: opts.sign ? (body) => opts.sign(body) : undefined,
|
|
72
|
+
logger: opts.logger,
|
|
73
|
+
resolveTarget() {
|
|
74
|
+
return {
|
|
75
|
+
url: opts.url,
|
|
76
|
+
label,
|
|
77
|
+
fetch(request) {
|
|
78
|
+
return fetchImpl(request);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
package/dist/do-store.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,CAkC7F"}
|
package/dist/do-store.js
CHANGED
|
@@ -20,6 +20,18 @@ export function createDurableObjectRunStore(storage) {
|
|
|
20
20
|
await storage.put(RECORD_KEY, record);
|
|
21
21
|
return record;
|
|
22
22
|
},
|
|
23
|
+
async tryInsert(record) {
|
|
24
|
+
// DO storage is single-threaded per DO instance: concurrent fetches
|
|
25
|
+
// to `idFromName(runId)` are serialized inside the DO's request
|
|
26
|
+
// queue, so get-then-put is naturally atomic. This is just the
|
|
27
|
+
// contract-shape implementation.
|
|
28
|
+
const existing = await storage.get(RECORD_KEY);
|
|
29
|
+
if (existing && existing.id === record.id) {
|
|
30
|
+
return { record: existing, created: false };
|
|
31
|
+
}
|
|
32
|
+
await storage.put(RECORD_KEY, record);
|
|
33
|
+
return { record, created: true };
|
|
34
|
+
},
|
|
23
35
|
async list(filter = {}) {
|
|
24
36
|
const r = await storage.get(RECORD_KEY);
|
|
25
37
|
if (!r)
|
package/dist/durable-object.d.ts
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
|
-
import { applyWaitpointInjection, driveUntilPaused, type RunRecord
|
|
1
|
+
import { applyWaitpointInjection, driveUntilPaused, type RunRecord } from "@voyantjs/workflows-orchestrator";
|
|
2
|
+
import type { StepDispatcher } from "./dispatchers.js";
|
|
2
3
|
import type { DurableObjectStorageLike } from "./types.js";
|
|
3
4
|
export interface DurableObjectDeps {
|
|
4
5
|
storage: DurableObjectStorageLike;
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Pluggable dispatcher producing a StepHandler for a given run's
|
|
8
|
+
* context. The DO calls `dispatcher({ tenantScript, workflowId })`
|
|
9
|
+
* once per drive; the returned handler delivers step requests to
|
|
10
|
+
* whatever Worker (or isolate) hosts the workflow code.
|
|
11
|
+
*
|
|
12
|
+
* Pick a factory from `./dispatchers.ts`:
|
|
13
|
+
* - `createWfpDispatcher` — multi-tenant via dispatch namespace
|
|
14
|
+
* - `createServiceBindingDispatcher` — sibling Worker via service binding
|
|
15
|
+
* - `createInlineDispatcher` — same Worker / direct call
|
|
16
|
+
* - `createHttpDispatcher` — arbitrary HTTP endpoint
|
|
10
17
|
*/
|
|
11
|
-
|
|
18
|
+
dispatcher: StepDispatcher;
|
|
12
19
|
now?: () => number;
|
|
13
20
|
}
|
|
14
21
|
export declare function handleDurableObjectRequest(req: Request, deps: DurableObjectDeps): Promise<Response>;
|
|
@@ -1 +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,
|
|
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,EAEf,MAAM,kCAAkC,CAAA;AAEzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAEtD,OAAO,KAAK,EAEV,wBAAwB,EAGzB,MAAM,YAAY,CAAA;AAEnB,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,wBAAwB,CAAA;IACjC;;;;;;;;;;;OAWG;IACH,UAAU,EAAE,cAAc,CAAA;IAC1B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CACnB;AAYD,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,iBAAiB,GACtB,OAAO,CAAC,QAAQ,CAAC,CAmEnB;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"}
|