@voyantjs/workflows-cloud-adapter 0.66.0 → 0.68.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.
@@ -0,0 +1,45 @@
1
+ import { buildManifest } from "@voyantjs/workflows/events";
2
+ import type { WorkflowManifest } from "@voyantjs/workflows/protocol";
3
+ import type { CfManifestStore } from "@voyantjs/workflows-orchestrator-cloudflare";
4
+ export type WorkflowEnvironment = "production" | "preview" | "development";
5
+ export interface AutoPublishContext {
6
+ manifestStore: CfManifestStore;
7
+ environment?: WorkflowEnvironment | string;
8
+ projectId?: string;
9
+ /**
10
+ * Hook to schedule the publish so it doesn't block the hot path.
11
+ * In a CF Worker, pass `ctx.waitUntil`. Defaults to fire-and-forget
12
+ * (the returned promise is unhandled).
13
+ */
14
+ waitUntil?: (promise: Promise<unknown>) => void;
15
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
16
+ /**
17
+ * Internal seam — defaults to the global `__listRegisteredWorkflows()`.
18
+ * Tests inject a fixture registry without polluting the process-wide
19
+ * registry.
20
+ */
21
+ listWorkflows?: () => ReadonlyArray<{
22
+ id: string;
23
+ config?: Parameters<typeof buildManifest>[0]["workflows"][number]["config"];
24
+ }>;
25
+ /**
26
+ * Internal seam — defaults to the global event-filter registry.
27
+ */
28
+ listEventFilters?: () => Parameters<typeof buildManifest>[0]["eventFilters"];
29
+ }
30
+ /**
31
+ * Schedule an auto-publish of the in-process registry. Idempotent —
32
+ * the latch prevents repeated KV reads per isolate, and the publish
33
+ * itself short-circuits when the current envelope already matches the
34
+ * registry's versionId.
35
+ */
36
+ export declare function scheduleAutoPublishManifest(ctx: AutoPublishContext): void;
37
+ /**
38
+ * Build the registry-derived manifest and write it to KV when needed.
39
+ * Exported so tests (and the rare caller that wants synchronous
40
+ * semantics) can await the result. Returns the published manifest, or
41
+ * `null` when the registry is empty or KV already has a matching
42
+ * envelope.
43
+ */
44
+ export declare function publishManifest(ctx: AutoPublishContext): Promise<WorkflowManifest | null>;
45
+ //# sourceMappingURL=auto-publish.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-publish.d.ts","sourceRoot":"","sources":["../src/auto-publish.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,aAAa,EAA0B,MAAM,4BAA4B,CAAA;AAClF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AACpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6CAA6C,CAAA;AAIlF,MAAM,MAAM,mBAAmB,GAAG,YAAY,GAAG,SAAS,GAAG,aAAa,CAAA;AAE1E,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,eAAe,CAAA;IAC9B,WAAW,CAAC,EAAE,mBAAmB,GAAG,MAAM,CAAA;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IAC/C,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,aAAa,CAAC,EAAE,MAAM,aAAa,CAAC;QAClC,EAAE,EAAE,MAAM,CAAA;QACV,MAAM,CAAC,EAAE,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAA;KAC5E,CAAC,CAAA;IACF;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAA;CAC7E;AASD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,kBAAkB,GAAG,IAAI,CA0BzE;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAqC/F"}
@@ -0,0 +1,107 @@
1
+ // Cold-start auto-publish of the in-process workflow registry to the
2
+ // `WORKFLOW_MANIFESTS` KV namespace.
3
+ //
4
+ // Background (issue #1070): the KV manifest is only populated when a
5
+ // tenant explicitly calls `registerManifest` (driver path) or POSTs
6
+ // `/api/manifests` (HTTP path). Tenants that compose workflows from
7
+ // many packages and never wire either path end up with an empty KV,
8
+ // which means voyant-cloud's scheduler can't pull the runtime manifest
9
+ // to seed `workflow_schedules`. The cron tick then has nothing to fire.
10
+ //
11
+ // This module bridges the gap: when the cloud adapter sees its first
12
+ // request, it builds the manifest from the in-process registry and
13
+ // writes it to KV if the current envelope is missing or its versionId
14
+ // differs. The manifest is content-addressed, so concurrent cold
15
+ // starts converge on the same versionId — repeated publishes are
16
+ // no-ops after the first.
17
+ import { __listRegisteredWorkflows } from "@voyantjs/workflows";
18
+ import { buildManifest, getEventFilterRegistry } from "@voyantjs/workflows/events";
19
+ const ALLOWED_ENVS = new Set(["production", "preview", "development"]);
20
+ /**
21
+ * Per-store latch so we only check KV once per cold start. The store
22
+ * object is created from the KV binding, which is stable across
23
+ * requests on the same isolate.
24
+ */
25
+ const PUBLISHED = new WeakSet();
26
+ /**
27
+ * Schedule an auto-publish of the in-process registry. Idempotent —
28
+ * the latch prevents repeated KV reads per isolate, and the publish
29
+ * itself short-circuits when the current envelope already matches the
30
+ * registry's versionId.
31
+ */
32
+ export function scheduleAutoPublishManifest(ctx) {
33
+ if (PUBLISHED.has(ctx.manifestStore))
34
+ return;
35
+ PUBLISHED.add(ctx.manifestStore);
36
+ const env = normalizeEnvironment(ctx.environment);
37
+ const work = (async () => {
38
+ try {
39
+ await publishManifest({ ...ctx, environment: env });
40
+ }
41
+ catch (err) {
42
+ // Cold-start publish is best-effort — never let a KV hiccup take
43
+ // down the request that triggered it. Clear the latch so a
44
+ // subsequent request re-tries.
45
+ PUBLISHED.delete(ctx.manifestStore);
46
+ ctx.logger?.("warn", "workflows: auto-publish manifest failed", {
47
+ error: err instanceof Error ? err.message : String(err),
48
+ environment: env,
49
+ });
50
+ }
51
+ })();
52
+ if (ctx.waitUntil) {
53
+ ctx.waitUntil(work);
54
+ }
55
+ else {
56
+ // No waitUntil — let it run; we already swallowed errors inside.
57
+ void work;
58
+ }
59
+ }
60
+ /**
61
+ * Build the registry-derived manifest and write it to KV when needed.
62
+ * Exported so tests (and the rare caller that wants synchronous
63
+ * semantics) can await the result. Returns the published manifest, or
64
+ * `null` when the registry is empty or KV already has a matching
65
+ * envelope.
66
+ */
67
+ export async function publishManifest(ctx) {
68
+ const workflows = (ctx.listWorkflows ?? __listRegisteredWorkflows)();
69
+ if (workflows.length === 0)
70
+ return null;
71
+ const eventFilters = ctx.listEventFilters
72
+ ? ctx.listEventFilters()
73
+ : getEventFilterRegistry().list();
74
+ const environment = normalizeEnvironment(ctx.environment);
75
+ const manifest = await buildManifest({
76
+ projectId: ctx.projectId,
77
+ environment,
78
+ workflows: workflows.map((wf) => ({ id: wf.id, config: wf.config })),
79
+ eventFilters,
80
+ });
81
+ const current = await ctx.manifestStore.getCurrent(environment);
82
+ if (current && current.versionId === manifest.versionId) {
83
+ ctx.logger?.("info", "workflows: auto-publish manifest is a no-op", {
84
+ environment,
85
+ versionId: manifest.versionId,
86
+ });
87
+ return null;
88
+ }
89
+ await ctx.manifestStore.registerManifest({
90
+ environment,
91
+ versionId: manifest.versionId,
92
+ manifest: manifest,
93
+ });
94
+ ctx.logger?.("info", "workflows: auto-published manifest", {
95
+ environment,
96
+ versionId: manifest.versionId,
97
+ workflowCount: workflows.length,
98
+ eventFilterCount: eventFilters.length,
99
+ });
100
+ return manifest;
101
+ }
102
+ function normalizeEnvironment(value) {
103
+ if (value && ALLOWED_ENVS.has(value)) {
104
+ return value;
105
+ }
106
+ return "production";
107
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type ContainerNamespaceLike, type DurableObjectNamespaceLike, type DurableObjectStorageLike, type KvNamespaceLike, type StepDispatcher, type WorkerFetchDeps } from "@voyantjs/workflows-orchestrator-cloudflare";
2
+ export { type AutoPublishContext, publishManifest, scheduleAutoPublishManifest, type WorkflowEnvironment, } from "./auto-publish.js";
2
3
  export interface CloudWorkflowsEnv {
3
4
  /** Per-run Durable Object namespace declared by the tenant Worker. */
4
5
  WORKFLOW_RUN_DO: DurableObjectNamespaceLike;
@@ -18,6 +19,12 @@ export interface CloudWorkflowsEnv {
18
19
  * development only.
19
20
  */
20
21
  VOYANT_API_TOKENS?: string;
22
+ /**
23
+ * Deployment environment label used when the cloud adapter auto-publishes
24
+ * the in-process workflow registry to `WORKFLOW_MANIFESTS`. Voyant Cloud
25
+ * injects this on tenant workers; defaults to `"production"` when unset.
26
+ */
27
+ VOYANT_WORKFLOWS_ENVIRONMENT?: "production" | "preview" | "development" | string;
21
28
  /**
22
29
  * Prefix for the R2 S3 API URL that hosts the container bundle.
23
30
  * Expected form: `https://<account>.r2.cloudflarestorage.com/<bucket>`.
@@ -53,9 +60,33 @@ export interface CloudOrchestratorOptions<Env extends CloudWorkflowsEnv = CloudW
53
60
  tenantMeta?: WorkerFetchDeps["tenantMeta"];
54
61
  services?: import("@voyantjs/workflows/driver").ServiceResolver;
55
62
  resolveEnv?: (env: Env) => Env;
63
+ /**
64
+ * Opt out of the cold-start auto-publish of the in-process workflow
65
+ * registry to `WORKFLOW_MANIFESTS` KV. Default: auto-publish is on
66
+ * whenever the KV binding is present. Set to `false` if your deploy
67
+ * pipeline already POSTs `/api/manifests` and you want a single
68
+ * source of truth.
69
+ */
70
+ autoPublishManifest?: boolean;
71
+ /**
72
+ * Cloudflare `ctx.waitUntil` — when provided, the auto-publish
73
+ * background task is registered with it so the response isn't held
74
+ * back by the KV write. Pass `(p) => ctx.waitUntil(p)` from the
75
+ * outer `fetch(request, env, ctx)` handler.
76
+ */
77
+ waitUntil?: (promise: Promise<unknown>) => void;
78
+ }
79
+ /**
80
+ * Cloudflare ExecutionContext-like shape consumed by the cloud
81
+ * orchestrator. Declared structurally so this package doesn't pull in
82
+ * `@cloudflare/workers-types` — pass the real `ctx` from your worker's
83
+ * `fetch(request, env, ctx)` and the structural shape will match.
84
+ */
85
+ export interface CloudExecutionCtx {
86
+ waitUntil?: (promise: Promise<unknown>) => void;
56
87
  }
57
88
  export interface CloudOrchestrator<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> {
58
- fetch: (request: Request, env?: Env) => Promise<Response>;
89
+ fetch: (request: Request, env?: Env, ctx?: CloudExecutionCtx) => Promise<Response>;
59
90
  WorkflowRunDO: WorkflowRunDOClass<Env>;
60
91
  }
61
92
  export type WorkflowRunDOClass<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> = new (state: DurableObjectStateLike, env: Env) => WorkflowRunDO<Env>;
@@ -77,5 +108,4 @@ export declare class WorkflowRunDO<Env extends CloudWorkflowsEnv = CloudWorkflow
77
108
  protected executionOptions(): CloudExecutionOptions<Env>;
78
109
  }
79
110
  export declare function createCloudStepDispatcher<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv>(env: Env, options?: CloudExecutionOptions<Env>): StepDispatcher;
80
- export {};
81
111
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -7,15 +7,20 @@ import { createBearerVerifier, createHmacSigner } from "@voyantjs/workflows/auth
7
7
  import { handleStepRequest, } from "@voyantjs/workflows/handler";
8
8
  import { createInMemoryRateLimiter } from "@voyantjs/workflows/rate-limit";
9
9
  import { createCfContainerStepRunner, createInlineDispatcher, createKvManifestStore, createR2Presigner, handleDurableObjectAlarm, handleDurableObjectRequest, handleWorkerRequest, } from "@voyantjs/workflows-orchestrator-cloudflare";
10
+ import { scheduleAutoPublishManifest } from "./auto-publish.js";
11
+ export { publishManifest, scheduleAutoPublishManifest, } from "./auto-publish.js";
10
12
  const envCache = new WeakMap();
11
13
  const defaultExecutionOptions = {};
12
14
  export function createCloudOrchestrator(workflows, boundEnv, options = {}) {
13
15
  void workflows;
14
16
  const WorkflowRunDOWithOptions = createWorkflowRunDOClass(options);
15
17
  return {
16
- fetch(request, requestEnv) {
18
+ fetch(request, requestEnv, ctx) {
17
19
  const env = resolveBoundEnv(boundEnv, requestEnv, options);
18
- return handleCloudFetch(request, env, options);
20
+ const callOptions = ctx?.waitUntil
21
+ ? { ...options, waitUntil: options.waitUntil ?? ctx.waitUntil.bind(ctx) }
22
+ : options;
23
+ return handleCloudFetch(request, env, callOptions);
19
24
  },
20
25
  WorkflowRunDO: WorkflowRunDOWithOptions,
21
26
  };
@@ -27,7 +32,8 @@ export function mountWorkflows(app, env, options = {}) {
27
32
  app.all(`${pathPrefix}/*`, (...args) => {
28
33
  const request = extractRequest(args);
29
34
  const requestEnv = extractEnv(args, env);
30
- return orchestrator.fetch(request, requestEnv);
35
+ const ctx = extractCtx(args);
36
+ return orchestrator.fetch(request, requestEnv, ctx);
31
37
  });
32
38
  return app;
33
39
  }
@@ -35,7 +41,7 @@ export function mountWorkflows(app, env, options = {}) {
35
41
  const originalFetch = app.fetch.bind(app);
36
42
  app.fetch = (request, requestEnv, ctx) => {
37
43
  if (isMountedPath(new URL(request.url).pathname, pathPrefix)) {
38
- return orchestrator.fetch(request, requestEnv ?? env);
44
+ return orchestrator.fetch(request, requestEnv ?? env, ctx);
39
45
  }
40
46
  return originalFetch(request, requestEnv, ctx);
41
47
  };
@@ -49,6 +55,16 @@ export async function handleCloudFetch(request, env, options = {}) {
49
55
  .split(",")
50
56
  .map((s) => s.trim())
51
57
  .filter((s) => s.length > 0);
58
+ const manifestStore = resolveManifestStore(resolvedEnv);
59
+ if (manifestStore && options.autoPublishManifest !== false) {
60
+ const autoPublishCtx = {
61
+ manifestStore,
62
+ environment: resolvedEnv.VOYANT_WORKFLOWS_ENVIRONMENT,
63
+ logger: options.logger,
64
+ ...(options.waitUntil ? { waitUntil: options.waitUntil } : {}),
65
+ };
66
+ scheduleAutoPublishManifest(autoPublishCtx);
67
+ }
52
68
  return handleWorkerRequest(request, {
53
69
  runDO: resolvedEnv.WORKFLOW_RUN_DO,
54
70
  verifyRequest: options.verifyRequest ?? (tokens.length > 0 ? createBearerVerifier(tokens) : undefined),
@@ -56,9 +72,7 @@ export async function handleCloudFetch(request, env, options = {}) {
56
72
  idGenerator: options.idGenerator,
57
73
  now: options.now,
58
74
  tenantMeta: options.tenantMeta,
59
- manifestStore: resolvedEnv.WORKFLOW_MANIFESTS
60
- ? createKvManifestStore({ kv: resolvedEnv.WORKFLOW_MANIFESTS })
61
- : undefined,
75
+ manifestStore,
62
76
  });
63
77
  }
64
78
  export class WorkflowRunDO {
@@ -96,6 +110,22 @@ function createWorkflowRunDOClass(options) {
96
110
  }
97
111
  };
98
112
  }
113
+ function resolveManifestStore(env) {
114
+ const kv = env.WORKFLOW_MANIFESTS;
115
+ if (!kv)
116
+ return undefined;
117
+ const cache = cacheFor(env);
118
+ // Re-create when the KV binding identity changes (e.g. a test that
119
+ // swaps namespaces on the same env reference). In production the
120
+ // binding is stable across requests so this path is a cache hit and
121
+ // the WeakSet-based latch in scheduleAutoPublishManifest works as
122
+ // intended.
123
+ if (!cache.manifestStore || cache.manifestStoreKv !== kv) {
124
+ cache.manifestStoreKv = kv;
125
+ cache.manifestStore = createKvManifestStore({ kv });
126
+ }
127
+ return cache.manifestStore;
128
+ }
99
129
  function resolveDispatcher(env, options = defaultExecutionOptions) {
100
130
  const cache = cacheFor(env);
101
131
  if (!cache.dispatcher || cache.dispatcherOptions !== options) {
@@ -378,3 +408,15 @@ function extractEnv(args, boundEnv) {
378
408
  const firstEnv = args[0]?.env;
379
409
  return firstEnv ?? args[1];
380
410
  }
411
+ function extractCtx(args) {
412
+ // Hono passes (c, next) with executionCtx on the context; raw workers
413
+ // pass (request, env, ctx) directly. Probe both shapes so the auto-publish
414
+ // background task can register with the right waitUntil.
415
+ const honoCtx = args[0];
416
+ if (honoCtx?.executionCtx?.waitUntil)
417
+ return honoCtx.executionCtx;
418
+ const rawCtx = args[2];
419
+ if (rawCtx && typeof rawCtx.waitUntil === "function")
420
+ return rawCtx;
421
+ return undefined;
422
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/workflows-cloud-adapter",
3
- "version": "0.66.0",
3
+ "version": "0.68.0",
4
4
  "description": "Tenant Worker adapter for Voyant Cloud Workflows deployments. Wires WorkflowRunDO, inline local dispatch, and platform step-runner bindings from env.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -26,9 +26,9 @@
26
26
  "NOTICE"
27
27
  ],
28
28
  "dependencies": {
29
- "@voyantjs/workflows": "0.66.0",
30
- "@voyantjs/workflows-orchestrator": "0.66.0",
31
- "@voyantjs/workflows-orchestrator-cloudflare": "0.66.0"
29
+ "@voyantjs/workflows": "0.68.0",
30
+ "@voyantjs/workflows-orchestrator": "0.68.0",
31
+ "@voyantjs/workflows-orchestrator-cloudflare": "0.68.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.12.0",
@@ -0,0 +1,145 @@
1
+ // Cold-start auto-publish of the in-process workflow registry to the
2
+ // `WORKFLOW_MANIFESTS` KV namespace.
3
+ //
4
+ // Background (issue #1070): the KV manifest is only populated when a
5
+ // tenant explicitly calls `registerManifest` (driver path) or POSTs
6
+ // `/api/manifests` (HTTP path). Tenants that compose workflows from
7
+ // many packages and never wire either path end up with an empty KV,
8
+ // which means voyant-cloud's scheduler can't pull the runtime manifest
9
+ // to seed `workflow_schedules`. The cron tick then has nothing to fire.
10
+ //
11
+ // This module bridges the gap: when the cloud adapter sees its first
12
+ // request, it builds the manifest from the in-process registry and
13
+ // writes it to KV if the current envelope is missing or its versionId
14
+ // differs. The manifest is content-addressed, so concurrent cold
15
+ // starts converge on the same versionId — repeated publishes are
16
+ // no-ops after the first.
17
+
18
+ import { __listRegisteredWorkflows } from "@voyantjs/workflows"
19
+ import { buildManifest, getEventFilterRegistry } from "@voyantjs/workflows/events"
20
+ import type { WorkflowManifest } from "@voyantjs/workflows/protocol"
21
+ import type { CfManifestStore } from "@voyantjs/workflows-orchestrator-cloudflare"
22
+
23
+ const ALLOWED_ENVS = new Set<WorkflowEnvironment>(["production", "preview", "development"])
24
+
25
+ export type WorkflowEnvironment = "production" | "preview" | "development"
26
+
27
+ export interface AutoPublishContext {
28
+ manifestStore: CfManifestStore
29
+ environment?: WorkflowEnvironment | string
30
+ projectId?: string
31
+ /**
32
+ * Hook to schedule the publish so it doesn't block the hot path.
33
+ * In a CF Worker, pass `ctx.waitUntil`. Defaults to fire-and-forget
34
+ * (the returned promise is unhandled).
35
+ */
36
+ waitUntil?: (promise: Promise<unknown>) => void
37
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
38
+ /**
39
+ * Internal seam — defaults to the global `__listRegisteredWorkflows()`.
40
+ * Tests inject a fixture registry without polluting the process-wide
41
+ * registry.
42
+ */
43
+ listWorkflows?: () => ReadonlyArray<{
44
+ id: string
45
+ config?: Parameters<typeof buildManifest>[0]["workflows"][number]["config"]
46
+ }>
47
+ /**
48
+ * Internal seam — defaults to the global event-filter registry.
49
+ */
50
+ listEventFilters?: () => Parameters<typeof buildManifest>[0]["eventFilters"]
51
+ }
52
+
53
+ /**
54
+ * Per-store latch so we only check KV once per cold start. The store
55
+ * object is created from the KV binding, which is stable across
56
+ * requests on the same isolate.
57
+ */
58
+ const PUBLISHED = new WeakSet<CfManifestStore>()
59
+
60
+ /**
61
+ * Schedule an auto-publish of the in-process registry. Idempotent —
62
+ * the latch prevents repeated KV reads per isolate, and the publish
63
+ * itself short-circuits when the current envelope already matches the
64
+ * registry's versionId.
65
+ */
66
+ export function scheduleAutoPublishManifest(ctx: AutoPublishContext): void {
67
+ if (PUBLISHED.has(ctx.manifestStore)) return
68
+ PUBLISHED.add(ctx.manifestStore)
69
+
70
+ const env = normalizeEnvironment(ctx.environment)
71
+ const work = (async () => {
72
+ try {
73
+ await publishManifest({ ...ctx, environment: env })
74
+ } catch (err) {
75
+ // Cold-start publish is best-effort — never let a KV hiccup take
76
+ // down the request that triggered it. Clear the latch so a
77
+ // subsequent request re-tries.
78
+ PUBLISHED.delete(ctx.manifestStore)
79
+ ctx.logger?.("warn", "workflows: auto-publish manifest failed", {
80
+ error: err instanceof Error ? err.message : String(err),
81
+ environment: env,
82
+ })
83
+ }
84
+ })()
85
+
86
+ if (ctx.waitUntil) {
87
+ ctx.waitUntil(work)
88
+ } else {
89
+ // No waitUntil — let it run; we already swallowed errors inside.
90
+ void work
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Build the registry-derived manifest and write it to KV when needed.
96
+ * Exported so tests (and the rare caller that wants synchronous
97
+ * semantics) can await the result. Returns the published manifest, or
98
+ * `null` when the registry is empty or KV already has a matching
99
+ * envelope.
100
+ */
101
+ export async function publishManifest(ctx: AutoPublishContext): Promise<WorkflowManifest | null> {
102
+ const workflows = (ctx.listWorkflows ?? __listRegisteredWorkflows)()
103
+ if (workflows.length === 0) return null
104
+
105
+ const eventFilters = ctx.listEventFilters
106
+ ? ctx.listEventFilters()
107
+ : getEventFilterRegistry().list()
108
+
109
+ const environment = normalizeEnvironment(ctx.environment)
110
+ const manifest = await buildManifest({
111
+ projectId: ctx.projectId,
112
+ environment,
113
+ workflows: workflows.map((wf) => ({ id: wf.id, config: wf.config })),
114
+ eventFilters,
115
+ })
116
+
117
+ const current = await ctx.manifestStore.getCurrent(environment)
118
+ if (current && current.versionId === manifest.versionId) {
119
+ ctx.logger?.("info", "workflows: auto-publish manifest is a no-op", {
120
+ environment,
121
+ versionId: manifest.versionId,
122
+ })
123
+ return null
124
+ }
125
+
126
+ await ctx.manifestStore.registerManifest({
127
+ environment,
128
+ versionId: manifest.versionId,
129
+ manifest: manifest as unknown as Record<string, unknown>,
130
+ })
131
+ ctx.logger?.("info", "workflows: auto-published manifest", {
132
+ environment,
133
+ versionId: manifest.versionId,
134
+ workflowCount: workflows.length,
135
+ eventFilterCount: eventFilters.length,
136
+ })
137
+ return manifest
138
+ }
139
+
140
+ function normalizeEnvironment(value: string | undefined): WorkflowEnvironment {
141
+ if (value && ALLOWED_ENVS.has(value as WorkflowEnvironment)) {
142
+ return value as WorkflowEnvironment
143
+ }
144
+ return "production"
145
+ }
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  import { createInMemoryRateLimiter } from "@voyantjs/workflows/rate-limit"
14
14
  import type { StepHandler } from "@voyantjs/workflows-orchestrator"
15
15
  import {
16
+ type CfManifestStore,
16
17
  type ContainerNamespaceLike,
17
18
  createCfContainerStepRunner,
18
19
  createInlineDispatcher,
@@ -28,6 +29,15 @@ import {
28
29
  type WorkerFetchDeps,
29
30
  } from "@voyantjs/workflows-orchestrator-cloudflare"
30
31
 
32
+ import { type AutoPublishContext, scheduleAutoPublishManifest } from "./auto-publish.js"
33
+
34
+ export {
35
+ type AutoPublishContext,
36
+ publishManifest,
37
+ scheduleAutoPublishManifest,
38
+ type WorkflowEnvironment,
39
+ } from "./auto-publish.js"
40
+
31
41
  export interface CloudWorkflowsEnv {
32
42
  /** Per-run Durable Object namespace declared by the tenant Worker. */
33
43
  WORKFLOW_RUN_DO: DurableObjectNamespaceLike
@@ -47,6 +57,12 @@ export interface CloudWorkflowsEnv {
47
57
  * development only.
48
58
  */
49
59
  VOYANT_API_TOKENS?: string
60
+ /**
61
+ * Deployment environment label used when the cloud adapter auto-publishes
62
+ * the in-process workflow registry to `WORKFLOW_MANIFESTS`. Voyant Cloud
63
+ * injects this on tenant workers; defaults to `"production"` when unset.
64
+ */
65
+ VOYANT_WORKFLOWS_ENVIRONMENT?: "production" | "preview" | "development" | string
50
66
  /**
51
67
  * Prefix for the R2 S3 API URL that hosts the container bundle.
52
68
  * Expected form: `https://<account>.r2.cloudflarestorage.com/<bucket>`.
@@ -84,10 +100,35 @@ export interface CloudOrchestratorOptions<Env extends CloudWorkflowsEnv = CloudW
84
100
  tenantMeta?: WorkerFetchDeps["tenantMeta"]
85
101
  services?: import("@voyantjs/workflows/driver").ServiceResolver
86
102
  resolveEnv?: (env: Env) => Env
103
+ /**
104
+ * Opt out of the cold-start auto-publish of the in-process workflow
105
+ * registry to `WORKFLOW_MANIFESTS` KV. Default: auto-publish is on
106
+ * whenever the KV binding is present. Set to `false` if your deploy
107
+ * pipeline already POSTs `/api/manifests` and you want a single
108
+ * source of truth.
109
+ */
110
+ autoPublishManifest?: boolean
111
+ /**
112
+ * Cloudflare `ctx.waitUntil` — when provided, the auto-publish
113
+ * background task is registered with it so the response isn't held
114
+ * back by the KV write. Pass `(p) => ctx.waitUntil(p)` from the
115
+ * outer `fetch(request, env, ctx)` handler.
116
+ */
117
+ waitUntil?: (promise: Promise<unknown>) => void
118
+ }
119
+
120
+ /**
121
+ * Cloudflare ExecutionContext-like shape consumed by the cloud
122
+ * orchestrator. Declared structurally so this package doesn't pull in
123
+ * `@cloudflare/workers-types` — pass the real `ctx` from your worker's
124
+ * `fetch(request, env, ctx)` and the structural shape will match.
125
+ */
126
+ export interface CloudExecutionCtx {
127
+ waitUntil?: (promise: Promise<unknown>) => void
87
128
  }
88
129
 
89
130
  export interface CloudOrchestrator<Env extends CloudWorkflowsEnv = CloudWorkflowsEnv> {
90
- fetch: (request: Request, env?: Env) => Promise<Response>
131
+ fetch: (request: Request, env?: Env, ctx?: CloudExecutionCtx) => Promise<Response>
91
132
  WorkflowRunDO: WorkflowRunDOClass<Env>
92
133
  }
93
134
 
@@ -96,6 +137,8 @@ type EnvCache = {
96
137
  dispatcherOptions?: CloudExecutionOptions<CloudWorkflowsEnv>
97
138
  stepHandler?: StepHandler
98
139
  stepHandlerOptions?: CloudExecutionOptions<CloudWorkflowsEnv>
140
+ manifestStore?: CfManifestStore
141
+ manifestStoreKv?: KvNamespaceLike
99
142
  }
100
143
 
101
144
  const envCache = new WeakMap<object, EnvCache>()
@@ -120,9 +163,12 @@ export function createCloudOrchestrator<Env extends CloudWorkflowsEnv = CloudWor
120
163
  const WorkflowRunDOWithOptions = createWorkflowRunDOClass<Env>(options)
121
164
 
122
165
  return {
123
- fetch(request, requestEnv) {
166
+ fetch(request, requestEnv, ctx) {
124
167
  const env = resolveBoundEnv(boundEnv, requestEnv, options)
125
- return handleCloudFetch(request, env, options)
168
+ const callOptions = ctx?.waitUntil
169
+ ? { ...options, waitUntil: options.waitUntil ?? ctx.waitUntil.bind(ctx) }
170
+ : options
171
+ return handleCloudFetch(request, env, callOptions)
126
172
  },
127
173
  WorkflowRunDO: WorkflowRunDOWithOptions,
128
174
  }
@@ -142,7 +188,8 @@ export function mountWorkflows<
142
188
  app.all(`${pathPrefix}/*`, (...args) => {
143
189
  const request = extractRequest(args)
144
190
  const requestEnv = extractEnv<Env>(args, env)
145
- return orchestrator.fetch(request, requestEnv)
191
+ const ctx = extractCtx(args)
192
+ return orchestrator.fetch(request, requestEnv, ctx)
146
193
  })
147
194
  return app
148
195
  }
@@ -151,7 +198,11 @@ export function mountWorkflows<
151
198
  const originalFetch = app.fetch.bind(app)
152
199
  ;(app as { fetch: typeof app.fetch }).fetch = (request, requestEnv, ctx) => {
153
200
  if (isMountedPath(new URL(request.url).pathname, pathPrefix)) {
154
- return orchestrator.fetch(request, (requestEnv as Env | undefined) ?? env)
201
+ return orchestrator.fetch(
202
+ request,
203
+ (requestEnv as Env | undefined) ?? env,
204
+ ctx as CloudExecutionCtx | undefined,
205
+ )
155
206
  }
156
207
  return originalFetch(request, requestEnv, ctx)
157
208
  }
@@ -174,6 +225,18 @@ export async function handleCloudFetch<Env extends CloudWorkflowsEnv = CloudWork
174
225
  .map((s) => s.trim())
175
226
  .filter((s) => s.length > 0)
176
227
 
228
+ const manifestStore = resolveManifestStore(resolvedEnv)
229
+
230
+ if (manifestStore && options.autoPublishManifest !== false) {
231
+ const autoPublishCtx: AutoPublishContext = {
232
+ manifestStore,
233
+ environment: resolvedEnv.VOYANT_WORKFLOWS_ENVIRONMENT,
234
+ logger: options.logger,
235
+ ...(options.waitUntil ? { waitUntil: options.waitUntil } : {}),
236
+ }
237
+ scheduleAutoPublishManifest(autoPublishCtx)
238
+ }
239
+
177
240
  return handleWorkerRequest(request, {
178
241
  runDO: resolvedEnv.WORKFLOW_RUN_DO,
179
242
  verifyRequest:
@@ -182,9 +245,7 @@ export async function handleCloudFetch<Env extends CloudWorkflowsEnv = CloudWork
182
245
  idGenerator: options.idGenerator,
183
246
  now: options.now,
184
247
  tenantMeta: options.tenantMeta,
185
- manifestStore: resolvedEnv.WORKFLOW_MANIFESTS
186
- ? createKvManifestStore({ kv: resolvedEnv.WORKFLOW_MANIFESTS })
187
- : undefined,
248
+ manifestStore,
188
249
  })
189
250
  }
190
251
 
@@ -235,6 +296,22 @@ function createWorkflowRunDOClass<Env extends CloudWorkflowsEnv>(
235
296
  }
236
297
  }
237
298
 
299
+ function resolveManifestStore(env: CloudWorkflowsEnv): CfManifestStore | undefined {
300
+ const kv = env.WORKFLOW_MANIFESTS
301
+ if (!kv) return undefined
302
+ const cache = cacheFor(env)
303
+ // Re-create when the KV binding identity changes (e.g. a test that
304
+ // swaps namespaces on the same env reference). In production the
305
+ // binding is stable across requests so this path is a cache hit and
306
+ // the WeakSet-based latch in scheduleAutoPublishManifest works as
307
+ // intended.
308
+ if (!cache.manifestStore || cache.manifestStoreKv !== kv) {
309
+ cache.manifestStoreKv = kv
310
+ cache.manifestStore = createKvManifestStore({ kv })
311
+ }
312
+ return cache.manifestStore
313
+ }
314
+
238
315
  function resolveDispatcher<Env extends CloudWorkflowsEnv>(
239
316
  env: Env,
240
317
  options: CloudExecutionOptions<Env> = defaultExecutionOptions as CloudExecutionOptions<Env>,
@@ -608,3 +685,14 @@ function extractEnv<Env extends CloudWorkflowsEnv>(
608
685
  const firstEnv = (args[0] as { env?: unknown } | undefined)?.env
609
686
  return (firstEnv as Env | undefined) ?? (args[1] as Env | undefined)
610
687
  }
688
+
689
+ function extractCtx(args: readonly unknown[]): CloudExecutionCtx | undefined {
690
+ // Hono passes (c, next) with executionCtx on the context; raw workers
691
+ // pass (request, env, ctx) directly. Probe both shapes so the auto-publish
692
+ // background task can register with the right waitUntil.
693
+ const honoCtx = args[0] as { executionCtx?: CloudExecutionCtx } | undefined
694
+ if (honoCtx?.executionCtx?.waitUntil) return honoCtx.executionCtx
695
+ const rawCtx = args[2] as CloudExecutionCtx | undefined
696
+ if (rawCtx && typeof rawCtx.waitUntil === "function") return rawCtx
697
+ return undefined
698
+ }