@voyantjs/workflows-orchestrator-cloudflare 0.66.6 → 0.69.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/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { createDurableObjectRunStore } from "./do-store.js";
6
6
  export { type DurableObjectDeps, handleDurableObjectAlarm, handleDurableObjectRequest, } from "./durable-object.js";
7
7
  export { type CfManifestEnvelope, type CfManifestStore, type CreateKvManifestStoreOptions, createInMemoryKv, createKvManifestStore, type KvNamespaceLike, } from "./manifest-kv-store.js";
8
8
  export { createR2Presigner, type PresignArgs, type R2PresignerOptions, } from "./r2-sign.js";
9
+ export { handleGetSchedules, type ScheduleHandlerDeps, type ScheduleListResponse, type ScheduleSummary, } from "./schedule-handler.js";
9
10
  export * from "./types.js";
10
11
  export { type DurableObjectNamespaceLike, handleWorkerRequest, type WorkerFetchDeps, } from "./worker.js";
11
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
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,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAC/B,4BAA4B,EAC5B,mCAAmC,GACpC,MAAM,8BAA8B,CAAA;AACrC,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"}
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,KAAK,sBAAsB,EAC3B,KAAK,0BAA0B,EAC/B,4BAA4B,EAC5B,mCAAmC,GACpC,MAAM,8BAA8B,CAAA;AACrC,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,OAAO,EACL,kBAAkB,EAClB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,eAAe,GACrB,MAAM,uBAAuB,CAAA;AAC9B,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,KAAK,0BAA0B,EAC/B,mBAAmB,EACnB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAA"}
package/dist/index.js CHANGED
@@ -39,5 +39,6 @@ export { createDurableObjectRunStore } from "./do-store.js";
39
39
  export { handleDurableObjectAlarm, handleDurableObjectRequest, } from "./durable-object.js";
40
40
  export { createInMemoryKv, createKvManifestStore, } from "./manifest-kv-store.js";
41
41
  export { createR2Presigner, } from "./r2-sign.js";
42
+ export { handleGetSchedules, } from "./schedule-handler.js";
42
43
  export * from "./types.js";
43
44
  export { handleWorkerRequest, } from "./worker.js";
@@ -0,0 +1,43 @@
1
+ import type { ManifestSchedule } from "@voyantjs/workflows/protocol";
2
+ import type { CfManifestStore } from "./manifest-kv-store.js";
3
+ export interface ScheduleHandlerDeps {
4
+ manifestStore: CfManifestStore;
5
+ /**
6
+ * Process-wide schedules toggle. The Voyant Cloud orchestrator gates
7
+ * scheduler ticks behind `VOYANT_WORKFLOWS_ENABLE_SCHEDULES`; the UI
8
+ * surfaces that flag so operators can tell at a glance whether any
9
+ * schedule will fire even if `enabled: true` on the registration.
10
+ *
11
+ * When omitted, the response omits `schedulesEnabledByEnv` so older
12
+ * UIs can ignore the field.
13
+ */
14
+ schedulesEnabledByEnv?: boolean;
15
+ now?: () => number;
16
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
17
+ }
18
+ export interface ScheduleSummary {
19
+ workflowId: string;
20
+ scheduleId: string;
21
+ schedule: ManifestSchedule;
22
+ /** Epoch millis of the next computed fire, or null when undecidable. */
23
+ nextRunAt: number | null;
24
+ /**
25
+ * False when the registration explicitly sets `enabled: false`, or
26
+ * when the registration's `environments` list excludes the requested
27
+ * environment.
28
+ */
29
+ enabled: boolean;
30
+ disabledReason?: "registration_disabled" | "env_filtered";
31
+ }
32
+ export interface ScheduleListResponse {
33
+ environment: string;
34
+ versionId: string;
35
+ schedulesEnabledByEnv?: boolean;
36
+ data: ScheduleSummary[];
37
+ }
38
+ /**
39
+ * Handle `GET /api/schedules/:env`. Returns the projected schedule list
40
+ * for the current manifest, or 404 when no manifest is registered.
41
+ */
42
+ export declare function handleGetSchedules(environment: string, deps: ScheduleHandlerDeps): Promise<Response>;
43
+ //# sourceMappingURL=schedule-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-handler.d.ts","sourceRoot":"","sources":["../src/schedule-handler.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,gBAAgB,EAAoB,MAAM,8BAA8B,CAAA;AAGtF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAI7D,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,eAAe,CAAA;IAC9B;;;;;;;;OAQG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,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,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,gBAAgB,CAAA;IAC1B,wEAAwE;IACxE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,CAAC,EAAE,uBAAuB,GAAG,cAAc,CAAA;CAC1D;AAED,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,IAAI,EAAE,eAAe,EAAE,CAAA;CACxB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,QAAQ,CAAC,CAoEnB"}
@@ -0,0 +1,90 @@
1
+ // HTTP handler for `/api/schedules/:env`. Reads the registered manifest
2
+ // for the requested environment, projects each workflow's schedule blocks
3
+ // into a flat list, and computes `nextRun` per entry via the same cron /
4
+ // every / at logic the live scheduler uses.
5
+ //
6
+ // Aggregate "lastRun" is intentionally out of scope here — runs in the
7
+ // Cloudflare orchestrator live in per-run Durable Objects, indexed by
8
+ // runId, with no list-by-workflow path. The UI is expected to fetch
9
+ // last-run state separately from the template-side `workflow-runs`
10
+ // admin API (`/v1/admin/workflow-runs?workflowName=…&limit=1`).
11
+ import { computeNextFire } from "@voyantjs/workflows-orchestrator";
12
+ const ALLOWED_ENVS = new Set(["production", "preview", "development"]);
13
+ /**
14
+ * Handle `GET /api/schedules/:env`. Returns the projected schedule list
15
+ * for the current manifest, or 404 when no manifest is registered.
16
+ */
17
+ export async function handleGetSchedules(environment, deps) {
18
+ if (!ALLOWED_ENVS.has(environment)) {
19
+ return json(400, {
20
+ error: "invalid_environment",
21
+ message: `environment must be one of ${[...ALLOWED_ENVS].join(", ")}`,
22
+ });
23
+ }
24
+ const envelope = await deps.manifestStore.getCurrent(environment);
25
+ if (!envelope) {
26
+ return json(404, { error: "not_found", environment });
27
+ }
28
+ const now = deps.now ? deps.now() : Date.now();
29
+ const manifest = envelope.manifest;
30
+ const data = [];
31
+ for (const workflow of manifest.workflows ?? []) {
32
+ const schedules = Array.isArray(workflow.schedules) ? workflow.schedules : [];
33
+ schedules.forEach((schedule, index) => {
34
+ const scheduleId = `${envelope.versionId}:${workflow.id}:${schedule.name ?? index}`;
35
+ const registrationDisabled = schedule.enabled === false;
36
+ const envFiltered = Array.isArray(schedule.environments)
37
+ ? !schedule.environments.includes(environment)
38
+ : false;
39
+ let nextRunAt = null;
40
+ if (!registrationDisabled && !envFiltered) {
41
+ try {
42
+ const fire = computeNextFire(schedule, now);
43
+ nextRunAt = Number.isFinite(fire) ? fire : null;
44
+ }
45
+ catch (err) {
46
+ deps.logger?.("warn", "schedules: cannot compute next fire", {
47
+ workflowId: workflow.id,
48
+ scheduleId,
49
+ error: err instanceof Error ? err.message : String(err),
50
+ });
51
+ nextRunAt = null;
52
+ }
53
+ }
54
+ const enabled = !registrationDisabled && !envFiltered;
55
+ const disabledReason = registrationDisabled
56
+ ? "registration_disabled"
57
+ : envFiltered
58
+ ? "env_filtered"
59
+ : undefined;
60
+ data.push({
61
+ workflowId: workflow.id,
62
+ scheduleId,
63
+ schedule,
64
+ nextRunAt,
65
+ enabled,
66
+ ...(disabledReason ? { disabledReason } : {}),
67
+ });
68
+ });
69
+ }
70
+ const response = {
71
+ environment,
72
+ versionId: envelope.versionId,
73
+ data,
74
+ ...(deps.schedulesEnabledByEnv !== undefined
75
+ ? { schedulesEnabledByEnv: deps.schedulesEnabledByEnv }
76
+ : {}),
77
+ };
78
+ return json(200, response);
79
+ }
80
+ function json(status, body) {
81
+ return new Response(JSON.stringify(body), {
82
+ status,
83
+ headers: {
84
+ "content-type": "application/json; charset=utf-8",
85
+ "access-control-allow-origin": "*",
86
+ "access-control-allow-methods": "GET,POST,OPTIONS",
87
+ "access-control-allow-headers": "content-type, x-voyant-protocol",
88
+ },
89
+ });
90
+ }
package/dist/worker.d.ts CHANGED
@@ -25,10 +25,19 @@ export interface WorkerFetchDeps<Id = unknown> {
25
25
  now?: () => number;
26
26
  /**
27
27
  * Optional KV-backed manifest store. When set, the worker also serves
28
- * `/api/manifests` and `/api/events`. When unset, those routes 404 —
29
- * useful for orchestrators that only expose the run surface.
28
+ * `/api/manifests`, `/api/schedules`, and `/api/events`. When unset,
29
+ * those routes 404 — useful for orchestrators that only expose the
30
+ * run surface.
30
31
  */
31
32
  manifestStore?: CfManifestStore;
33
+ /**
34
+ * Optional process-wide schedules toggle reported by
35
+ * `/api/schedules/:env`. Used by the workflow-schedules UI to show
36
+ * "schedules disabled by env flag" when the orchestrator is running
37
+ * with `VOYANT_WORKFLOWS_ENABLE_SCHEDULES` (or equivalent) turned off.
38
+ * When omitted, the schedules response leaves the flag out.
39
+ */
40
+ schedulesEnabledByEnv?: boolean;
32
41
  /**
33
42
  * Tenant metadata stamped on event-triggered runs. Defaults to
34
43
  * `{ tenantId: "default", projectId: "default", organizationId: "default" }`.
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAQ7D;;;;GAIG;AACH,MAAM,WAAW,0BAA0B,CAAC,EAAE,GAAG,OAAO;IACtD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAA;IAC5B,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG;QAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CACxD;AAED,MAAM,WAAW,eAAe,CAAC,EAAE,GAAG,OAAO;IAC3C,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,CAAA;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,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;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;OAKG;IACH,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;CACF;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAC1C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC,GACxB,OAAO,CAAC,QAAQ,CAAC,CAoMnB"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAS7D;;;;GAIG;AACH,MAAM,WAAW,0BAA0B,CAAC,EAAE,GAAG,OAAO;IACtD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,EAAE,CAAA;IAC5B,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG;QAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;KAAE,CAAA;CACxD;AAED,MAAM,WAAW,eAAe,CAAC,EAAE,GAAG,OAAO;IAC3C,KAAK,EAAE,0BAA0B,CAAC,EAAE,CAAC,CAAA;IACrC;;;OAGG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACtD,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;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,MAAM,CAAA;IAC1B,0CAA0C;IAC1C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,CAAA;IAC/B;;;;;;OAMG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;;;OAKG;IACH,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;CACF;AAED,wBAAsB,mBAAmB,CAAC,EAAE,EAC1C,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,eAAe,CAAC,EAAE,CAAC,GACxB,OAAO,CAAC,QAAQ,CAAC,CAmNnB"}
package/dist/worker.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import { buildResumeJournal, buildSeededResumeJournal, } from "@voyantjs/workflows-orchestrator";
15
15
  import { handleIngestEvent } from "./event-handler.js";
16
16
  import { handleGetManifest, handleRegisterManifest } from "./manifest-handler.js";
17
+ import { handleGetSchedules } from "./schedule-handler.js";
17
18
  const DEFAULT_TENANT_META = {
18
19
  tenantId: "default",
19
20
  projectId: "default",
@@ -60,6 +61,20 @@ export async function handleWorkerRequest(req, deps) {
60
61
  logger: deps.logger,
61
62
  });
62
63
  }
64
+ // GET /api/schedules/:env — aggregate scheduled-workflow view.
65
+ const schedulesMatch = url.pathname.match(/^\/api\/schedules\/([^/]+)$/);
66
+ if (schedulesMatch) {
67
+ if (!deps.manifestStore) {
68
+ return json(404, { error: "schedules_not_configured" });
69
+ }
70
+ const env = decodeURIComponent(schedulesMatch[1] ?? "");
71
+ return handleGetSchedules(env, {
72
+ manifestStore: deps.manifestStore,
73
+ schedulesEnabledByEnv: deps.schedulesEnabledByEnv,
74
+ now: deps.now,
75
+ logger: deps.logger,
76
+ });
77
+ }
63
78
  }
64
79
  // POST /api/events — synchronous event ingest. Loads the manifest,
65
80
  // routes filters, forwards each match to the run-DO trigger flow.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voyantjs/workflows-orchestrator-cloudflare",
3
- "version": "0.66.6",
3
+ "version": "0.69.0",
4
4
  "description": "Cloudflare Worker + Durable Object adapter for @voyantjs/workflows-orchestrator. Dispatches workflow-step requests to tenant Workers via a Workers-for-Platforms dispatch namespace.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -26,8 +26,8 @@
26
26
  "NOTICE"
27
27
  ],
28
28
  "dependencies": {
29
- "@voyantjs/workflows-orchestrator": "0.66.6",
30
- "@voyantjs/workflows": "0.66.6"
29
+ "@voyantjs/workflows-orchestrator": "0.69.0",
30
+ "@voyantjs/workflows": "0.69.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@cloudflare/vitest-pool-workers": "^0.15.1",
package/src/index.ts CHANGED
@@ -77,6 +77,12 @@ export {
77
77
  type PresignArgs,
78
78
  type R2PresignerOptions,
79
79
  } from "./r2-sign.js"
80
+ export {
81
+ handleGetSchedules,
82
+ type ScheduleHandlerDeps,
83
+ type ScheduleListResponse,
84
+ type ScheduleSummary,
85
+ } from "./schedule-handler.js"
80
86
  export * from "./types.js"
81
87
  export {
82
88
  type DurableObjectNamespaceLike,
@@ -0,0 +1,144 @@
1
+ // HTTP handler for `/api/schedules/:env`. Reads the registered manifest
2
+ // for the requested environment, projects each workflow's schedule blocks
3
+ // into a flat list, and computes `nextRun` per entry via the same cron /
4
+ // every / at logic the live scheduler uses.
5
+ //
6
+ // Aggregate "lastRun" is intentionally out of scope here — runs in the
7
+ // Cloudflare orchestrator live in per-run Durable Objects, indexed by
8
+ // runId, with no list-by-workflow path. The UI is expected to fetch
9
+ // last-run state separately from the template-side `workflow-runs`
10
+ // admin API (`/v1/admin/workflow-runs?workflowName=…&limit=1`).
11
+
12
+ import type { ManifestSchedule, WorkflowManifest } from "@voyantjs/workflows/protocol"
13
+ import { computeNextFire } from "@voyantjs/workflows-orchestrator"
14
+
15
+ import type { CfManifestStore } from "./manifest-kv-store.js"
16
+
17
+ const ALLOWED_ENVS = new Set(["production", "preview", "development"])
18
+
19
+ export interface ScheduleHandlerDeps {
20
+ manifestStore: CfManifestStore
21
+ /**
22
+ * Process-wide schedules toggle. The Voyant Cloud orchestrator gates
23
+ * scheduler ticks behind `VOYANT_WORKFLOWS_ENABLE_SCHEDULES`; the UI
24
+ * surfaces that flag so operators can tell at a glance whether any
25
+ * schedule will fire even if `enabled: true` on the registration.
26
+ *
27
+ * When omitted, the response omits `schedulesEnabledByEnv` so older
28
+ * UIs can ignore the field.
29
+ */
30
+ schedulesEnabledByEnv?: boolean
31
+ now?: () => number
32
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
33
+ }
34
+
35
+ export interface ScheduleSummary {
36
+ workflowId: string
37
+ scheduleId: string
38
+ schedule: ManifestSchedule
39
+ /** Epoch millis of the next computed fire, or null when undecidable. */
40
+ nextRunAt: number | null
41
+ /**
42
+ * False when the registration explicitly sets `enabled: false`, or
43
+ * when the registration's `environments` list excludes the requested
44
+ * environment.
45
+ */
46
+ enabled: boolean
47
+ disabledReason?: "registration_disabled" | "env_filtered"
48
+ }
49
+
50
+ export interface ScheduleListResponse {
51
+ environment: string
52
+ versionId: string
53
+ schedulesEnabledByEnv?: boolean
54
+ data: ScheduleSummary[]
55
+ }
56
+
57
+ /**
58
+ * Handle `GET /api/schedules/:env`. Returns the projected schedule list
59
+ * for the current manifest, or 404 when no manifest is registered.
60
+ */
61
+ export async function handleGetSchedules(
62
+ environment: string,
63
+ deps: ScheduleHandlerDeps,
64
+ ): Promise<Response> {
65
+ if (!ALLOWED_ENVS.has(environment)) {
66
+ return json(400, {
67
+ error: "invalid_environment",
68
+ message: `environment must be one of ${[...ALLOWED_ENVS].join(", ")}`,
69
+ })
70
+ }
71
+
72
+ const envelope = await deps.manifestStore.getCurrent(environment)
73
+ if (!envelope) {
74
+ return json(404, { error: "not_found", environment })
75
+ }
76
+
77
+ const now = deps.now ? deps.now() : Date.now()
78
+ const manifest = envelope.manifest as unknown as WorkflowManifest
79
+ const data: ScheduleSummary[] = []
80
+
81
+ for (const workflow of manifest.workflows ?? []) {
82
+ const schedules = Array.isArray(workflow.schedules) ? workflow.schedules : []
83
+ schedules.forEach((schedule, index) => {
84
+ const scheduleId = `${envelope.versionId}:${workflow.id}:${schedule.name ?? index}`
85
+ const registrationDisabled = schedule.enabled === false
86
+ const envFiltered = Array.isArray(schedule.environments)
87
+ ? !schedule.environments.includes(environment as never)
88
+ : false
89
+
90
+ let nextRunAt: number | null = null
91
+ if (!registrationDisabled && !envFiltered) {
92
+ try {
93
+ const fire = computeNextFire(schedule, now)
94
+ nextRunAt = Number.isFinite(fire) ? fire : null
95
+ } catch (err) {
96
+ deps.logger?.("warn", "schedules: cannot compute next fire", {
97
+ workflowId: workflow.id,
98
+ scheduleId,
99
+ error: err instanceof Error ? err.message : String(err),
100
+ })
101
+ nextRunAt = null
102
+ }
103
+ }
104
+
105
+ const enabled = !registrationDisabled && !envFiltered
106
+ const disabledReason = registrationDisabled
107
+ ? "registration_disabled"
108
+ : envFiltered
109
+ ? "env_filtered"
110
+ : undefined
111
+
112
+ data.push({
113
+ workflowId: workflow.id,
114
+ scheduleId,
115
+ schedule,
116
+ nextRunAt,
117
+ enabled,
118
+ ...(disabledReason ? { disabledReason } : {}),
119
+ })
120
+ })
121
+ }
122
+
123
+ const response: ScheduleListResponse = {
124
+ environment,
125
+ versionId: envelope.versionId,
126
+ data,
127
+ ...(deps.schedulesEnabledByEnv !== undefined
128
+ ? { schedulesEnabledByEnv: deps.schedulesEnabledByEnv }
129
+ : {}),
130
+ }
131
+ return json(200, response)
132
+ }
133
+
134
+ function json(status: number, body: unknown): Response {
135
+ return new Response(JSON.stringify(body), {
136
+ status,
137
+ headers: {
138
+ "content-type": "application/json; charset=utf-8",
139
+ "access-control-allow-origin": "*",
140
+ "access-control-allow-methods": "GET,POST,OPTIONS",
141
+ "access-control-allow-headers": "content-type, x-voyant-protocol",
142
+ },
143
+ })
144
+ }
package/src/worker.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  import { handleIngestEvent } from "./event-handler.js"
23
23
  import { handleGetManifest, handleRegisterManifest } from "./manifest-handler.js"
24
24
  import type { CfManifestStore } from "./manifest-kv-store.js"
25
+ import { handleGetSchedules } from "./schedule-handler.js"
25
26
 
26
27
  const DEFAULT_TENANT_META = {
27
28
  tenantId: "default",
@@ -54,10 +55,19 @@ export interface WorkerFetchDeps<Id = unknown> {
54
55
  now?: () => number
55
56
  /**
56
57
  * Optional KV-backed manifest store. When set, the worker also serves
57
- * `/api/manifests` and `/api/events`. When unset, those routes 404 —
58
- * useful for orchestrators that only expose the run surface.
58
+ * `/api/manifests`, `/api/schedules`, and `/api/events`. When unset,
59
+ * those routes 404 — useful for orchestrators that only expose the
60
+ * run surface.
59
61
  */
60
62
  manifestStore?: CfManifestStore
63
+ /**
64
+ * Optional process-wide schedules toggle reported by
65
+ * `/api/schedules/:env`. Used by the workflow-schedules UI to show
66
+ * "schedules disabled by env flag" when the orchestrator is running
67
+ * with `VOYANT_WORKFLOWS_ENABLE_SCHEDULES` (or equivalent) turned off.
68
+ * When omitted, the schedules response leaves the flag out.
69
+ */
70
+ schedulesEnabledByEnv?: boolean
61
71
  /**
62
72
  * Tenant metadata stamped on event-triggered runs. Defaults to
63
73
  * `{ tenantId: "default", projectId: "default", organizationId: "default" }`.
@@ -118,6 +128,21 @@ export async function handleWorkerRequest<Id>(
118
128
  logger: deps.logger,
119
129
  })
120
130
  }
131
+
132
+ // GET /api/schedules/:env — aggregate scheduled-workflow view.
133
+ const schedulesMatch = url.pathname.match(/^\/api\/schedules\/([^/]+)$/)
134
+ if (schedulesMatch) {
135
+ if (!deps.manifestStore) {
136
+ return json(404, { error: "schedules_not_configured" })
137
+ }
138
+ const env = decodeURIComponent(schedulesMatch[1] ?? "")
139
+ return handleGetSchedules(env, {
140
+ manifestStore: deps.manifestStore,
141
+ schedulesEnabledByEnv: deps.schedulesEnabledByEnv,
142
+ now: deps.now,
143
+ logger: deps.logger,
144
+ })
145
+ }
121
146
  }
122
147
 
123
148
  // POST /api/events — synchronous event ingest. Loads the manifest,