@voyantjs/workflows-orchestrator-cloudflare 0.28.3 → 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/dist/durable-object.js
CHANGED
|
@@ -18,12 +18,21 @@
|
|
|
18
18
|
// the public surface is the outer Worker's /api/* routes.
|
|
19
19
|
import { applyWaitpointInjection, driveUntilPaused, cancel as orchestratorCancel, resume as orchestratorResume, trigger as orchestratorTrigger, } from "@voyantjs/workflows-orchestrator";
|
|
20
20
|
import { createDurableObjectRunStore } from "./do-store.js";
|
|
21
|
+
function resolve(deps, record) {
|
|
22
|
+
return deps.dispatcher({
|
|
23
|
+
tenantScript: record.tenantMeta.tenantScript,
|
|
24
|
+
workflowId: record.workflowId,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
21
27
|
export async function handleDurableObjectRequest(req, deps) {
|
|
22
28
|
const url = new URL(req.url);
|
|
23
29
|
const store = createDurableObjectRunStore(deps.storage);
|
|
24
30
|
if (req.method === "POST" && url.pathname === "/trigger") {
|
|
25
31
|
const payload = (await req.json());
|
|
26
|
-
const handler = deps
|
|
32
|
+
const handler = resolve(deps, {
|
|
33
|
+
tenantMeta: payload.tenantMeta,
|
|
34
|
+
workflowId: payload.workflowId,
|
|
35
|
+
});
|
|
27
36
|
const record = await orchestratorTrigger({
|
|
28
37
|
workflowId: payload.workflowId,
|
|
29
38
|
workflowVersion: payload.workflowVersion,
|
|
@@ -41,7 +50,7 @@ export async function handleDurableObjectRequest(req, deps) {
|
|
|
41
50
|
const existing = await store.get((await getStoredRunId(store)) ?? "");
|
|
42
51
|
if (!existing)
|
|
43
52
|
return json(404, { error: "not_found", message: "no run stored in this DO" });
|
|
44
|
-
const handler = deps
|
|
53
|
+
const handler = resolve(deps, existing);
|
|
45
54
|
const out = await orchestratorResume({ runId: existing.id, injection: payload.injection }, { store, handler, now: deps.now });
|
|
46
55
|
if (!out.ok) {
|
|
47
56
|
const status = out.status === "not_found" ? 404 : out.status === "no_match" ? 400 : 409;
|
|
@@ -55,7 +64,7 @@ export async function handleDurableObjectRequest(req, deps) {
|
|
|
55
64
|
const existing = await store.get((await getStoredRunId(store)) ?? "");
|
|
56
65
|
if (!existing)
|
|
57
66
|
return json(404, { error: "not_found", message: "no run stored in this DO" });
|
|
58
|
-
const handler = deps
|
|
67
|
+
const handler = resolve(deps, existing);
|
|
59
68
|
const out = await orchestratorCancel({ runId: existing.id, reason: payload.reason }, { store, handler, now: deps.now });
|
|
60
69
|
if (!out.ok) {
|
|
61
70
|
const status = out.status === "not_found" ? 404 : 409;
|
|
@@ -115,7 +124,7 @@ export async function handleDurableObjectAlarm(deps) {
|
|
|
115
124
|
return;
|
|
116
125
|
}
|
|
117
126
|
record.status = "running";
|
|
118
|
-
const handler = deps
|
|
127
|
+
const handler = resolve(deps, record);
|
|
119
128
|
await driveUntilPaused(record, { handler, now: deps.now });
|
|
120
129
|
await store.save(record);
|
|
121
130
|
await reconcileAlarm(record, store, deps);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CfManifestStore } from "./manifest-kv-store.js";
|
|
2
|
+
import type { DurableObjectNamespaceLike } from "./worker.js";
|
|
3
|
+
export interface EventHandlerDeps<Id = unknown> {
|
|
4
|
+
/** KV-backed manifest store (read-only path here). */
|
|
5
|
+
manifestStore: CfManifestStore;
|
|
6
|
+
/** DO namespace used to forward each match to the run DO. */
|
|
7
|
+
runDO: DurableObjectNamespaceLike<Id>;
|
|
8
|
+
/** id generator for new triggers; defaults to `run_<random>`. */
|
|
9
|
+
idGenerator?: () => string;
|
|
10
|
+
/** Injectable clock. */
|
|
11
|
+
now?: () => number;
|
|
12
|
+
/** Tenant metadata stamped on every triggered run. */
|
|
13
|
+
tenantMeta?: {
|
|
14
|
+
tenantId: string;
|
|
15
|
+
projectId: string;
|
|
16
|
+
organizationId: string;
|
|
17
|
+
tenantScript?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Optional logger. */
|
|
20
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
|
|
21
|
+
}
|
|
22
|
+
export declare function handleIngestEvent<Id>(req: Request, deps: EventHandlerDeps<Id>): Promise<Response>;
|
|
23
|
+
//# sourceMappingURL=event-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-handler.d.ts","sourceRoot":"","sources":["../src/event-handler.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAA;AAI7D,MAAM,WAAW,gBAAgB,CAAC,EAAE,GAAG,OAAO;IAC5C,sDAAsD;IACtD,aAAa,EAAE,eAAe,CAAA;IAC9B,6DAA6D;IAC7D,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,CAAA;IACrC,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,sDAAsD;IACtD,UAAU,CAAC,EAAE;QACX,QAAQ,EAAE,MAAM,CAAA;QAChB,SAAS,EAAE,MAAM,CAAA;QACjB,cAAc,EAAE,MAAM,CAAA;QACtB,YAAY,CAAC,EAAE,MAAM,CAAA;KACtB,CAAA;IACD,uBAAuB;IACvB,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChF;AAqBD,wBAAsB,iBAAiB,CAAC,EAAE,EACxC,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,gBAAgB,CAAC,EAAE,CAAC,GACzB,OAAO,CAAC,QAAQ,CAAC,CAqInB"}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// HTTP handler for `POST /api/events` — the synchronous event-ingest
|
|
2
|
+
// endpoint. Loads the registered manifest from KV, runs the pure
|
|
3
|
+
// `routeEvent` from `@voyantjs/workflows-orchestrator`, and forwards
|
|
4
|
+
// each match into the existing `/trigger` DO surface with a derived
|
|
5
|
+
// idempotencyKey.
|
|
6
|
+
//
|
|
7
|
+
// Response shape mirrors `IngestEventResponse` from
|
|
8
|
+
// `@voyantjs/workflows/driver`:
|
|
9
|
+
//
|
|
10
|
+
// { ok: true, eventId, matches: [...] }
|
|
11
|
+
// { ok: false, reason: "manifest_not_registered" | ... }
|
|
12
|
+
//
|
|
13
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §15.
|
|
14
|
+
import { deriveStableEventId } from "@voyantjs/workflows/events";
|
|
15
|
+
import { routeEvent } from "@voyantjs/workflows-orchestrator";
|
|
16
|
+
const ALLOWED_ENVS = new Set(["production", "preview", "development"]);
|
|
17
|
+
const DEFAULT_TENANT_META = {
|
|
18
|
+
tenantId: "default",
|
|
19
|
+
projectId: "default",
|
|
20
|
+
organizationId: "default",
|
|
21
|
+
};
|
|
22
|
+
export async function handleIngestEvent(req, deps) {
|
|
23
|
+
// Body parse + validate.
|
|
24
|
+
let raw;
|
|
25
|
+
try {
|
|
26
|
+
raw = await req.json();
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return json(400, {
|
|
30
|
+
error: "invalid_json",
|
|
31
|
+
message: err instanceof Error ? err.message : String(err),
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const validation = validateBody(raw);
|
|
35
|
+
if (!validation.ok)
|
|
36
|
+
return json(400, validation.error);
|
|
37
|
+
const body = validation.body;
|
|
38
|
+
// Manifest lookup.
|
|
39
|
+
const manifestEnvelope = await deps.manifestStore.getCurrent(body.environment);
|
|
40
|
+
if (!manifestEnvelope) {
|
|
41
|
+
return json(200, {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: "manifest_not_registered",
|
|
44
|
+
message: `No manifest is registered for environment "${body.environment}".`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const manifest = manifestEnvelope.manifest;
|
|
48
|
+
// Event id derivation — use the caller-stamped one when present, fall
|
|
49
|
+
// back to a content-derived id so external callers without a forwarder
|
|
50
|
+
// still get sensible idempotency.
|
|
51
|
+
const eventId = body.envelope.metadata?.eventId ?? (await deriveStableEventId(body.envelope));
|
|
52
|
+
// Route through the manifest's filters.
|
|
53
|
+
const routed = routeEvent({
|
|
54
|
+
manifest,
|
|
55
|
+
envelope: {
|
|
56
|
+
name: body.envelope.name,
|
|
57
|
+
data: body.envelope.data,
|
|
58
|
+
metadata: body.envelope.metadata,
|
|
59
|
+
emittedAt: body.envelope.emittedAt,
|
|
60
|
+
},
|
|
61
|
+
eventId,
|
|
62
|
+
idempotencyOverride: body.idempotencyKey,
|
|
63
|
+
});
|
|
64
|
+
// Forward each match into the existing /trigger DO route.
|
|
65
|
+
const matches = [];
|
|
66
|
+
let anyTriggered = false;
|
|
67
|
+
let anyFailed = false;
|
|
68
|
+
const tenantMeta = deps.tenantMeta ?? DEFAULT_TENANT_META;
|
|
69
|
+
for (const entry of routed) {
|
|
70
|
+
if (entry.status === "skipped") {
|
|
71
|
+
matches.push({
|
|
72
|
+
filterId: entry.filterId,
|
|
73
|
+
status: "skipped",
|
|
74
|
+
reason: entry.reason,
|
|
75
|
+
details: entry.details,
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const runId = `idem-${entry.targetWorkflowId}-${entry.idempotencyKey}`;
|
|
80
|
+
const triggerPayload = {
|
|
81
|
+
runId,
|
|
82
|
+
workflowId: entry.targetWorkflowId,
|
|
83
|
+
workflowVersion: "v1",
|
|
84
|
+
input: entry.input,
|
|
85
|
+
tenantMeta,
|
|
86
|
+
environment: body.environment,
|
|
87
|
+
idempotencyKey: entry.idempotencyKey,
|
|
88
|
+
triggeredBy: {
|
|
89
|
+
kind: "event",
|
|
90
|
+
eventId,
|
|
91
|
+
eventType: body.envelope.name,
|
|
92
|
+
filterId: entry.filterId,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const forward = new Request("https://do-internal/trigger", {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "content-type": "application/json" },
|
|
99
|
+
body: JSON.stringify(triggerPayload),
|
|
100
|
+
});
|
|
101
|
+
const id = deps.runDO.idFromName(runId);
|
|
102
|
+
const stub = deps.runDO.get(id);
|
|
103
|
+
const resp = await stub.fetch(forward);
|
|
104
|
+
if (resp.status >= 200 && resp.status < 300) {
|
|
105
|
+
matches.push({
|
|
106
|
+
filterId: entry.filterId,
|
|
107
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
108
|
+
runId,
|
|
109
|
+
idempotencyKey: entry.idempotencyKey,
|
|
110
|
+
status: "queued",
|
|
111
|
+
});
|
|
112
|
+
anyTriggered = true;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const errBody = await safeReadText(resp);
|
|
116
|
+
deps.logger?.("error", "trigger DO failed", {
|
|
117
|
+
status: resp.status,
|
|
118
|
+
body: errBody.slice(0, 256),
|
|
119
|
+
});
|
|
120
|
+
matches.push({
|
|
121
|
+
filterId: entry.filterId,
|
|
122
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
123
|
+
status: "error",
|
|
124
|
+
reason: `do_returned_${resp.status}`,
|
|
125
|
+
});
|
|
126
|
+
anyFailed = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
deps.logger?.("error", "trigger forward threw", {
|
|
131
|
+
error: err instanceof Error ? err.message : String(err),
|
|
132
|
+
});
|
|
133
|
+
matches.push({
|
|
134
|
+
filterId: entry.filterId,
|
|
135
|
+
targetWorkflowId: entry.targetWorkflowId,
|
|
136
|
+
status: "error",
|
|
137
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
138
|
+
});
|
|
139
|
+
anyFailed = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (matches.length > 0 && !anyTriggered && anyFailed) {
|
|
143
|
+
return json(502, {
|
|
144
|
+
ok: false,
|
|
145
|
+
reason: "trigger_failed_for_all_matches",
|
|
146
|
+
message: "every matched filter failed to trigger",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return json(200, { ok: true, eventId, matches });
|
|
150
|
+
}
|
|
151
|
+
// ---- Validation ----
|
|
152
|
+
function validateBody(raw) {
|
|
153
|
+
if (typeof raw !== "object" || raw === null) {
|
|
154
|
+
return { ok: false, error: { error: "invalid_body", message: "expected JSON object" } };
|
|
155
|
+
}
|
|
156
|
+
const r = raw;
|
|
157
|
+
if (typeof r.environment !== "string" || !ALLOWED_ENVS.has(r.environment)) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: {
|
|
161
|
+
error: "invalid_body",
|
|
162
|
+
message: `"environment" must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (typeof r.envelope !== "object" || r.envelope === null) {
|
|
167
|
+
return {
|
|
168
|
+
ok: false,
|
|
169
|
+
error: { error: "invalid_body", message: '"envelope" must be an object' },
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const envelope = r.envelope;
|
|
173
|
+
if (typeof envelope.name !== "string" || envelope.name.length === 0) {
|
|
174
|
+
return {
|
|
175
|
+
ok: false,
|
|
176
|
+
error: { error: "invalid_body", message: '"envelope.name" must be a non-empty string' },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (typeof envelope.emittedAt !== "string" || envelope.emittedAt.length === 0) {
|
|
180
|
+
return {
|
|
181
|
+
ok: false,
|
|
182
|
+
error: {
|
|
183
|
+
error: "invalid_body",
|
|
184
|
+
message: '"envelope.emittedAt" must be an ISO timestamp string',
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (envelope.metadata !== undefined &&
|
|
189
|
+
(typeof envelope.metadata !== "object" || envelope.metadata === null)) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
error: {
|
|
193
|
+
error: "invalid_body",
|
|
194
|
+
message: '"envelope.metadata" must be an object when supplied',
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (r.idempotencyKey !== undefined && typeof r.idempotencyKey !== "string") {
|
|
199
|
+
return {
|
|
200
|
+
ok: false,
|
|
201
|
+
error: { error: "invalid_body", message: '"idempotencyKey" must be a string when supplied' },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
ok: true,
|
|
206
|
+
body: {
|
|
207
|
+
environment: r.environment,
|
|
208
|
+
envelope: {
|
|
209
|
+
name: envelope.name,
|
|
210
|
+
data: envelope.data,
|
|
211
|
+
metadata: envelope.metadata,
|
|
212
|
+
emittedAt: envelope.emittedAt,
|
|
213
|
+
},
|
|
214
|
+
idempotencyKey: r.idempotencyKey,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// ---- Internal helpers ----
|
|
219
|
+
// Fallback id derivation lives in `@voyantjs/workflows/events`'s
|
|
220
|
+
// `deriveStableEventId` and is used inline above — content-derived so
|
|
221
|
+
// external callers (HTTP retries, third-party webhooks) dedupe naturally
|
|
222
|
+
// across re-deliveries (architecture doc §15.2).
|
|
223
|
+
async function safeReadText(resp) {
|
|
224
|
+
try {
|
|
225
|
+
return await resp.text();
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return "";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function json(status, body) {
|
|
232
|
+
return new Response(JSON.stringify(body), {
|
|
233
|
+
status,
|
|
234
|
+
headers: {
|
|
235
|
+
"content-type": "application/json; charset=utf-8",
|
|
236
|
+
"access-control-allow-origin": "*",
|
|
237
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
238
|
+
"access-control-allow-headers": "content-type, x-voyant-protocol",
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { type BundleLocation, type CfContainerRunnerDeps, type ContainerNamespaceLike, createCfContainerStepRunner, } from "./cf-container-runner.js";
|
|
2
|
-
export {
|
|
2
|
+
export { type CloudflareEdgeDriverOptions, createCloudflareEdgeDriver, } from "./cloudflare-edge-driver.js";
|
|
3
|
+
export { createHttpDispatcher, createInlineDispatcher, createServiceBindingDispatcher, type HttpDispatcherOptions, type ServiceBindingDispatcherOptions, type ServiceBindingLike, type StepDispatcher, type StepDispatcherContext, } from "./dispatchers.js";
|
|
3
4
|
export { createDurableObjectRunStore } from "./do-store.js";
|
|
4
5
|
export { type DurableObjectDeps, handleDurableObjectAlarm, handleDurableObjectRequest, } from "./durable-object.js";
|
|
6
|
+
export { type CfManifestEnvelope, type CfManifestStore, type CreateKvManifestStoreOptions, createInMemoryKv, createKvManifestStore, type KvNamespaceLike, } from "./manifest-kv-store.js";
|
|
5
7
|
export { createR2Presigner, type PresignArgs, type R2PresignerOptions, } from "./r2-sign.js";
|
|
6
8
|
export * from "./types.js";
|
|
7
9
|
export { type DurableObjectNamespaceLike, handleWorkerRequest, type WorkerFetchDeps, } from "./worker.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkCA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,2BAA2B,GAC5B,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,KAAK,2BAA2B,EAChC,0BAA0B,GAC3B,MAAM,6BAA6B,CAAA;AACpC,OAAO,EACL,oBAAoB,EACpB,sBAAsB,EACtB,8BAA8B,EAC9B,KAAK,qBAAqB,EAC1B,KAAK,+BAA+B,EACpC,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,qBAAqB,GAC3B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,2BAA2B,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,EACL,KAAK,iBAAiB,EACtB,wBAAwB,EACxB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,eAAe,EACpB,KAAK,4BAA4B,EACjC,gBAAgB,EAChB,qBAAqB,EACrB,KAAK,eAAe,GACrB,MAAM,wBAAwB,CAAA;AAC/B,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
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// @voyantjs/workflows-orchestrator-cloudflare
|
|
2
2
|
//
|
|
3
3
|
// Cloudflare Worker + Durable Object adapter for @voyantjs/workflows-orchestrator.
|
|
4
|
-
// Composes the protocol-agnostic state machine
|
|
5
|
-
//
|
|
4
|
+
// Composes the protocol-agnostic state machine with a DO-backed run
|
|
5
|
+
// store and a pluggable step dispatcher (`StepDispatcher`).
|
|
6
6
|
//
|
|
7
7
|
// Typical wrangler.jsonc layout:
|
|
8
8
|
//
|
|
@@ -15,20 +15,28 @@
|
|
|
15
15
|
// { "name": "WORKFLOW_RUN_DO", "class_name": "WorkflowRunDO" }
|
|
16
16
|
// ]
|
|
17
17
|
// },
|
|
18
|
-
// "dispatch_namespaces": [
|
|
19
|
-
// { "binding": "DISPATCHER", "namespace": "voyant-tenants" }
|
|
20
|
-
// ],
|
|
21
18
|
// "migrations": [
|
|
22
19
|
// { "tag": "v1", "new_sqlite_classes": ["WorkflowRunDO"] }
|
|
23
20
|
// ]
|
|
24
21
|
// }
|
|
25
22
|
//
|
|
23
|
+
// Pick a dispatcher in your DO's `deps()` based on where workflow
|
|
24
|
+
// code lives:
|
|
25
|
+
// * createInlineDispatcher — same Worker as the orchestrator
|
|
26
|
+
// * createServiceBindingDispatcher — sibling Worker via service binding
|
|
27
|
+
// * createHttpDispatcher — arbitrary HTTP endpoint
|
|
28
|
+
//
|
|
29
|
+
// Hosted multi-tenant providers implement custom dispatchers in their
|
|
30
|
+
// own deployment code.
|
|
31
|
+
//
|
|
26
32
|
// See docs/runtime-protocol.md §2 and docs/design.md §6 for the
|
|
27
33
|
// design this adapter implements.
|
|
28
34
|
export { createCfContainerStepRunner, } from "./cf-container-runner.js";
|
|
29
|
-
export {
|
|
35
|
+
export { createCloudflareEdgeDriver, } from "./cloudflare-edge-driver.js";
|
|
36
|
+
export { createHttpDispatcher, createInlineDispatcher, createServiceBindingDispatcher, } from "./dispatchers.js";
|
|
30
37
|
export { createDurableObjectRunStore } from "./do-store.js";
|
|
31
38
|
export { handleDurableObjectAlarm, handleDurableObjectRequest, } from "./durable-object.js";
|
|
39
|
+
export { createInMemoryKv, createKvManifestStore, } from "./manifest-kv-store.js";
|
|
32
40
|
export { createR2Presigner, } from "./r2-sign.js";
|
|
33
41
|
export * from "./types.js";
|
|
34
42
|
export { handleWorkerRequest, } from "./worker.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CfManifestStore } from "./manifest-kv-store.js";
|
|
2
|
+
export interface ManifestHandlerDeps {
|
|
3
|
+
manifestStore: CfManifestStore;
|
|
4
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Handle `POST /api/manifests`. Body: `{ environment, manifest }`.
|
|
8
|
+
* `manifest.versionId` is the registered key. Returns `{ ok: true, versionId }`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function handleRegisterManifest(req: Request, deps: ManifestHandlerDeps): Promise<Response>;
|
|
11
|
+
/**
|
|
12
|
+
* Handle `GET /api/manifests/:env`. Returns the current manifest envelope
|
|
13
|
+
* or 404 if no manifest is registered.
|
|
14
|
+
*/
|
|
15
|
+
export declare function handleGetManifest(environment: string, deps: ManifestHandlerDeps): Promise<Response>;
|
|
16
|
+
//# sourceMappingURL=manifest-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-handler.d.ts","sourceRoot":"","sources":["../src/manifest-handler.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAI7D,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,eAAe,CAAA;IAC9B,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChF;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAqDnB;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAYnB"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// HTTP handlers for `/api/manifests*`. Mounted by the worker before the
|
|
2
|
+
// existing `/api/runs/*` routes; both share the same auth.
|
|
3
|
+
//
|
|
4
|
+
// POST /api/manifests { environment, manifest } → { ok, versionId }
|
|
5
|
+
// GET /api/manifests/:env → manifest | 404
|
|
6
|
+
//
|
|
7
|
+
// Architecture: docs/architecture/workflows-runtime-architecture.md §8.2.
|
|
8
|
+
const ALLOWED_ENVS = new Set(["production", "preview", "development"]);
|
|
9
|
+
/**
|
|
10
|
+
* Handle `POST /api/manifests`. Body: `{ environment, manifest }`.
|
|
11
|
+
* `manifest.versionId` is the registered key. Returns `{ ok: true, versionId }`.
|
|
12
|
+
*/
|
|
13
|
+
export async function handleRegisterManifest(req, deps) {
|
|
14
|
+
let body;
|
|
15
|
+
try {
|
|
16
|
+
body = await req.json();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return json(400, {
|
|
20
|
+
error: "invalid_json",
|
|
21
|
+
message: err instanceof Error ? err.message : String(err),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
if (typeof body !== "object" || body === null) {
|
|
25
|
+
return json(400, { error: "invalid_body", message: "expected JSON object" });
|
|
26
|
+
}
|
|
27
|
+
const { environment, manifest } = body;
|
|
28
|
+
if (typeof environment !== "string" || !ALLOWED_ENVS.has(environment)) {
|
|
29
|
+
return json(400, {
|
|
30
|
+
error: "invalid_body",
|
|
31
|
+
message: `"environment" must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (typeof manifest !== "object" || manifest === null) {
|
|
35
|
+
return json(400, { error: "invalid_body", message: '"manifest" must be an object' });
|
|
36
|
+
}
|
|
37
|
+
const versionId = manifest.versionId;
|
|
38
|
+
if (typeof versionId !== "string" || versionId.length === 0) {
|
|
39
|
+
return json(400, {
|
|
40
|
+
error: "invalid_body",
|
|
41
|
+
message: '"manifest.versionId" must be a non-empty string',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const result = await deps.manifestStore.registerManifest({
|
|
46
|
+
environment,
|
|
47
|
+
versionId,
|
|
48
|
+
manifest: manifest,
|
|
49
|
+
});
|
|
50
|
+
return json(200, { ok: true, versionId: result.versionId });
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
deps.logger?.("error", "manifest registration failed", {
|
|
54
|
+
environment,
|
|
55
|
+
versionId,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err),
|
|
57
|
+
});
|
|
58
|
+
return json(500, {
|
|
59
|
+
error: "register_failed",
|
|
60
|
+
message: err instanceof Error ? err.message : String(err),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Handle `GET /api/manifests/:env`. Returns the current manifest envelope
|
|
66
|
+
* or 404 if no manifest is registered.
|
|
67
|
+
*/
|
|
68
|
+
export async function handleGetManifest(environment, deps) {
|
|
69
|
+
if (!ALLOWED_ENVS.has(environment)) {
|
|
70
|
+
return json(400, {
|
|
71
|
+
error: "invalid_environment",
|
|
72
|
+
message: `environment must be one of ${[...ALLOWED_ENVS].join(", ")}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const envelope = await deps.manifestStore.getCurrent(environment);
|
|
76
|
+
if (!envelope) {
|
|
77
|
+
return json(404, { error: "not_found", environment });
|
|
78
|
+
}
|
|
79
|
+
return json(200, envelope);
|
|
80
|
+
}
|
|
81
|
+
// ---- Internal ----
|
|
82
|
+
function json(status, body) {
|
|
83
|
+
return new Response(JSON.stringify(body), {
|
|
84
|
+
status,
|
|
85
|
+
headers: {
|
|
86
|
+
"content-type": "application/json; charset=utf-8",
|
|
87
|
+
"access-control-allow-origin": "*",
|
|
88
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
89
|
+
"access-control-allow-headers": "content-type, x-voyant-protocol",
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural view of a `WorkflowManifest` envelope. Mirrors the shape
|
|
3
|
+
* `@voyantjs/workflows-orchestrator-node`'s manifest store uses, declared
|
|
4
|
+
* locally so this package stays free of the Mode 2 dep.
|
|
5
|
+
*/
|
|
6
|
+
export interface CfManifestEnvelope {
|
|
7
|
+
environment: string;
|
|
8
|
+
versionId: string;
|
|
9
|
+
manifest: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
export interface CfManifestStore {
|
|
12
|
+
registerManifest(envelope: CfManifestEnvelope): Promise<{
|
|
13
|
+
versionId: string;
|
|
14
|
+
}>;
|
|
15
|
+
getCurrent(environment: string): Promise<CfManifestEnvelope | null>;
|
|
16
|
+
pruneToVersions(environment: string, keep: number): Promise<{
|
|
17
|
+
deleted: number;
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Subset of the CF KV namespace API we need. Declared structurally so
|
|
22
|
+
* tests can pass an in-memory fake without depending on
|
|
23
|
+
* `@cloudflare/workers-types`.
|
|
24
|
+
*/
|
|
25
|
+
export interface KvNamespaceLike {
|
|
26
|
+
get(key: string): Promise<string | null>;
|
|
27
|
+
put(key: string, value: string, opts?: {
|
|
28
|
+
metadata?: unknown;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
delete(key: string): Promise<void>;
|
|
31
|
+
list(opts?: {
|
|
32
|
+
prefix?: string;
|
|
33
|
+
limit?: number;
|
|
34
|
+
cursor?: string;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
keys: Array<{
|
|
37
|
+
name: string;
|
|
38
|
+
metadata?: unknown;
|
|
39
|
+
}>;
|
|
40
|
+
list_complete?: boolean;
|
|
41
|
+
cursor?: string;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
export interface CreateKvManifestStoreOptions {
|
|
45
|
+
/** KV namespace binding from the worker's env. */
|
|
46
|
+
kv: KvNamespaceLike;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build a KV-backed `CfManifestStore`. Stateless — every call hits KV.
|
|
50
|
+
*/
|
|
51
|
+
export declare function createKvManifestStore(opts: CreateKvManifestStoreOptions): CfManifestStore;
|
|
52
|
+
/**
|
|
53
|
+
* Tiny in-memory implementation of `KvNamespaceLike` for tests + the CF
|
|
54
|
+
* compliance suite run that doesn't go through wrangler. Mirrors the
|
|
55
|
+
* subset of CF KV semantics we use (list returns matching prefix in
|
|
56
|
+
* lexicographic order; get returns null for missing keys).
|
|
57
|
+
*/
|
|
58
|
+
export declare function createInMemoryKv(): KvNamespaceLike;
|
|
59
|
+
//# sourceMappingURL=manifest-kv-store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest-kv-store.d.ts","sourceRoot":"","sources":["../src/manifest-kv-store.ts"],"names":[],"mappings":"AAqBA;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,gBAAgB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAC9E,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAA;IACnE,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACjF;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IACxC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC7E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,IAAI,CAAC,IAAI,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QACzE,IAAI,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;SAAE,CAAC,CAAA;QACjD,aAAa,CAAC,EAAE,OAAO,CAAA;QACvB,MAAM,CAAC,EAAE,MAAM,CAAA;KAChB,CAAC,CAAA;CACH;AAED,MAAM,WAAW,4BAA4B;IAC3C,kDAAkD;IAClD,EAAE,EAAE,eAAe,CAAA;CACpB;AAID;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,4BAA4B,GAAG,eAAe,CA4EzF;AAcD;;;;;GAKG;AACH,wBAAgB,gBAAgB,IAAI,eAAe,CAyBlD"}
|