@vite-hub/workflow 0.0.4 → 0.1.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 +34 -14
- package/dist/{_app-DgwN4ulW.d.ts → app-BojnC_yf.d.ts} +1 -4
- package/dist/{app-BZpq2cwC.js → app-CzPoP84f.js} +2 -1
- package/dist/client-DsGM3M4U.js +389 -0
- package/dist/cloudflare-env-BNrYo29x.js +113 -0
- package/dist/{cloudflare-D0Di-QYL.js → cloudflare-nF4abPd4.js} +20 -4
- package/dist/cloudflare-shared-CaxrMD1A.d.ts +1 -0
- package/dist/{cloudflare-vite-BkDgB6Sr.js → cloudflare-vite-B3IabBsv.js} +5 -4
- package/dist/{config-BHknO_rT.js → config-IJMb0eeZ.js} +38 -30
- package/dist/error-diagnostics-A_8Kp-48.js +116 -0
- package/dist/error-diagnostics-pwNFvAHP.js +67 -0
- package/dist/index.d.ts +20 -21
- package/dist/index.js +25 -369
- package/dist/inspect-B72iFyj-.js +70 -0
- package/dist/operations-B_rYG71W.js +230 -0
- package/dist/operations-tNUdj3G4.d.ts +212 -0
- package/dist/operations.d.ts +37 -0
- package/dist/operations.js +887 -0
- package/dist/out-CsaJxg8m.js +21755 -0
- package/dist/rolldown-runtime-CMFfr-1z.js +26 -0
- package/dist/runtime/cloudflare-runner.d.ts +5 -15
- package/dist/runtime/cloudflare-runner.js +40 -5
- package/dist/runtime/cloudflare-shared.d.ts +1 -0
- package/dist/runtime/cloudflare-shared.js +1 -1
- package/dist/runtime/cloudflare-vite.d.ts +8 -10
- package/dist/runtime/cloudflare-vite.js +1 -1
- package/dist/runtime/execute.d.ts +8 -7
- package/dist/runtime/execute.js +28 -5
- package/dist/runtime/openworkflow-worker.d.ts +6 -8
- package/dist/runtime/openworkflow-worker.js +5 -4
- package/dist/runtime/openworkflow.d.ts +8 -10
- package/dist/runtime/openworkflow.js +23 -17
- package/dist/runtime/state.d.ts +20 -21
- package/dist/runtime/state.js +90 -20
- package/dist/runtime/vercel-vite.d.ts +10 -12
- package/dist/runtime/vercel-vite.js +2 -2
- package/dist/runtime-type-DBCvIh5x.js +7 -0
- package/dist/{types-B9RdRkKh.d.ts → types-D82WmfZa.d.ts} +23 -12
- package/dist/{vercel-B0MuIVgJ.js → vercel-D-ZNtB_u.js} +34 -9
- package/dist/vite.d.ts +325 -11
- package/dist/vite.js +1267 -691
- package/package.json +79 -29
- package/dist/cloudflare-env-DjxQNbrx.js +0 -58
- package/dist/cloudflare-shared-BVVgDSdq.d.ts +0 -1
- package/dist/runtime-type-DGUn8Ple.js +0 -37
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# @vite-hub/workflow
|
|
2
2
|
|
|
3
3
|
<p>
|
|
4
|
-
<a href="https://vitehub.dev"><img alt="ViteHub" src="https://
|
|
5
|
-
<img alt="TypeScript" src="https://
|
|
6
|
-
<img alt="Vite" src="https://
|
|
7
|
-
<img alt="Workflow" src="https://
|
|
4
|
+
<a href="https://vitehub.dev"><img alt="ViteHub" src="https://comark-ui.onmax.me/badge.svg?label=ViteHub&value=vitehub.dev"></a>
|
|
5
|
+
<img alt="TypeScript" src="https://comark-ui.onmax.me/badge.svg?label=TypeScript&value=ready">
|
|
6
|
+
<img alt="Vite" src="https://comark-ui.onmax.me/badge.svg?label=Vite&value=discovery">
|
|
7
|
+
<img alt="Workflow" src="https://comark-ui.onmax.me/badge.svg?label=Workflow&value=durable+steps">
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
`@vite-hub/workflow` discovers named long-running work and exposes one provider-neutral API for starting and inspecting runs.
|
|
11
11
|
|
|
12
|
-
Use a Workflow when the application needs a run id, durable state, retries, cancellation, or resumable work. Use [Queue](https://vitehub.dev/docs/
|
|
12
|
+
Use a Workflow when the application needs a run id, durable state, retries, cancellation, or resumable work. Use [Queue](https://vitehub.dev/docs/queue) when background delivery is enough and the application does not need to inspect a run.
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -23,10 +23,14 @@ Then install the dependency required by the selected provider:
|
|
|
23
23
|
| -------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------ |
|
|
24
24
|
| Cloudflare Workflows | None in application code | Cloudflare owns run state. |
|
|
25
25
|
| Vercel Workflow | `workflow` and `@workflow/builders` for native durable definitions | Native definitions survive function restarts; plain handlers run inline. |
|
|
26
|
-
| OpenWorkflow | `openworkflow`; add `postgres` when using Postgres |
|
|
26
|
+
| OpenWorkflow | `openworkflow`; add `postgres` when using Postgres | Uses local SQLite by default. Configure durable storage for deployment. |
|
|
27
27
|
|
|
28
28
|
Importing the provider-neutral package root does not load OpenWorkflow types. Worker lifecycle helpers live at `@vite-hub/workflow/runtime/openworkflow-worker`.
|
|
29
29
|
|
|
30
|
+
Set `workflow.sqlite.path` or `workflow.postgres.url` to select OpenWorkflow storage. An explicit value takes precedence over ambient storage environment variables. A configured environment declaration must resolve to a value; missing configuration rejects before opening a backend. With no storage configuration or storage environment variables, OpenWorkflow uses `.vitehub/data/openworkflow.sqlite.db`.
|
|
31
|
+
|
|
32
|
+
The former `workflow.database` option was never connected to Named Databases. It now rejects instead of silently selecting fallback storage. Replace it with `workflow.sqlite.path` or `workflow.postgres.url`.
|
|
33
|
+
|
|
30
34
|
## Define a workflow
|
|
31
35
|
|
|
32
36
|
```ts
|
|
@@ -52,7 +56,6 @@ ViteHub discovers `server/workflows/<name>.ts`, folder workflows such as `server
|
|
|
52
56
|
// server/workflow-runtime.ts
|
|
53
57
|
import type {
|
|
54
58
|
ResolvedWorkflowOptions,
|
|
55
|
-
WorkflowDefinition,
|
|
56
59
|
WorkflowDefinitionRegistry,
|
|
57
60
|
} from "@vite-hub/workflow";
|
|
58
61
|
import {
|
|
@@ -70,9 +73,7 @@ export const workflowConfig = {
|
|
|
70
73
|
} satisfies ResolvedWorkflowOptions;
|
|
71
74
|
|
|
72
75
|
const workflowRegistry = {
|
|
73
|
-
"onboard-user":
|
|
74
|
-
default: (await import("./workflows/onboard-user.ts")).default as WorkflowDefinition,
|
|
75
|
-
}),
|
|
76
|
+
"onboard-user": () => import("./workflows/onboard-user.ts"),
|
|
76
77
|
} satisfies WorkflowDefinitionRegistry;
|
|
77
78
|
|
|
78
79
|
export function installWorkflowRuntime() {
|
|
@@ -147,6 +148,10 @@ The other runtime helpers are:
|
|
|
147
148
|
|
|
148
149
|
Unsupported provider operations fail with `WORKFLOW_OPERATION_UNSUPPORTED`; ViteHub does not pretend that an inline run was cancelled or resumed.
|
|
149
150
|
|
|
151
|
+
Inline runs buffer the response body before completion. Each inspection and duplicate start returns a fresh response, so reading or cancelling one result does not consume later results. A body read failure marks the run as failed.
|
|
152
|
+
|
|
153
|
+
Inline run inspection uses weak references while execution is active. A reachable execution remains inspectable; an abandoned execution with no remaining owner can be garbage-collected and then reports `unknown`. Completed inline runs are retained for five minutes, up to 1,024 entries per runtime. Completed history does not evict active executions. If either `WeakRef` or `FinalizationRegistry` is unavailable, active inspection uses strong references capped at 1,024 entries. Starting another run evicts the oldest active inspection entry, even if execution is still reachable; it reports `unknown` and completion does not restore it. Use a durable provider when inspection must survive abandonment or a process restart.
|
|
154
|
+
|
|
150
155
|
## Make a Vercel workflow durable
|
|
151
156
|
|
|
152
157
|
A plain Vercel definition executes inline and does not survive a function restart. For durable execution, keep the same context-shaped handler and register a native Workflow DevKit entry:
|
|
@@ -156,7 +161,7 @@ pnpm add workflow @workflow/builders
|
|
|
156
161
|
```
|
|
157
162
|
|
|
158
163
|
```ts
|
|
159
|
-
import { defineWorkflow, type WorkflowExecutionContext } from "@vite-hub/workflow";
|
|
164
|
+
import { defineWorkflow, type WorkflowExecutionContext, type WorkflowProviderContext } from "@vite-hub/workflow";
|
|
160
165
|
|
|
161
166
|
interface OnboardPayload {
|
|
162
167
|
email: string;
|
|
@@ -168,7 +173,7 @@ async function upsertUserStep(email: string) {
|
|
|
168
173
|
return await upsertUser(email, { idempotencyKey: `onboard-user:${email}` });
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
async function durableOnboard({ payload }:
|
|
176
|
+
async function durableOnboard({ payload }: WorkflowProviderContext<OnboardPayload>) {
|
|
172
177
|
"use workflow";
|
|
173
178
|
|
|
174
179
|
const user = await upsertUserStep(payload.email);
|
|
@@ -183,7 +188,7 @@ async function inlineOnboard({ payload }: WorkflowExecutionContext<OnboardPayloa
|
|
|
183
188
|
export default defineWorkflow(inlineOnboard, { native: durableOnboard });
|
|
184
189
|
```
|
|
185
190
|
|
|
186
|
-
ViteHub transforms `native` when it generates Vercel output. Other providers keep using the normal handler. Put external side effects in idempotent `use step` functions because a durable step may be retried.
|
|
191
|
+
ViteHub transforms `native` when it generates Vercel output. Other providers keep using the normal handler. Vercel calls `native` with a `WorkflowProviderContext`, which has no `steps` runner. Put external side effects in idempotent `use step` functions because a durable step may be retried.
|
|
187
192
|
|
|
188
193
|
## Handle stable failures
|
|
189
194
|
|
|
@@ -216,8 +221,23 @@ async function transcribe(recordingId: string) {
|
|
|
216
221
|
|
|
217
222
|
## Documentation
|
|
218
223
|
|
|
219
|
-
- [Workflow guide](https://vitehub.dev/docs/
|
|
224
|
+
- [Workflow guide](https://vitehub.dev/docs/workflows)
|
|
220
225
|
- [Runtime and host support](https://vitehub.dev/docs/frameworks-hosts/support-matrix)
|
|
221
226
|
- [OpenWorkflow](https://openworkflow.dev/docs/overview)
|
|
222
227
|
- [Cloudflare Workflows](https://developers.cloudflare.com/workflows/)
|
|
223
228
|
- [Vercel Workflow](https://vercel.com/docs/workflow)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
## Required handle input
|
|
232
|
+
|
|
233
|
+
A typed Workflow handle requires a payload when its handler requires one. Both `run()` and `defer()` use this rule. A handle with an optional payload or `void` input can still run with no argument. To set start options for a no-input Workflow, pass `undefined` first.
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const welcome = createWorkflow<{ email: string }>("welcome", async ({ payload }) => {
|
|
237
|
+
await sendWelcomeEmail(payload.email)
|
|
238
|
+
})
|
|
239
|
+
await welcome.run({ email: "ada@example.com" })
|
|
240
|
+
// welcome.run() is a type error.
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
This is a breaking type correction. Supply the required payload at each handle call. Named operational functions do not infer a handler from a string, and persisted run results remain unknown.
|
|
@@ -5,7 +5,4 @@ type VitehubApp = AppHandler | {
|
|
|
5
5
|
request?: (request: Request, options?: RequestInit, context?: Record<string, unknown>) => Response | Promise<Response>;
|
|
6
6
|
};
|
|
7
7
|
//#endregion
|
|
8
|
-
|
|
9
|
-
type WorkflowApp = VitehubApp;
|
|
10
|
-
//#endregion
|
|
11
|
-
export { WorkflowApp as t };
|
|
8
|
+
export { VitehubApp as t };
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { t as internalErrorDiagnostics } from "./error-diagnostics-A_8Kp-48.js";
|
|
1
2
|
//#region ../internal/src/runtime/app.ts
|
|
2
3
|
function resolveAppFetch(label, app) {
|
|
3
4
|
if (!app) return;
|
|
4
5
|
if (typeof app === "function") return app;
|
|
5
6
|
if (typeof app.request === "function") return (request, context) => app.request(request, void 0, context);
|
|
6
7
|
if (typeof app.fetch === "function") return app.fetch.bind(app);
|
|
7
|
-
throw
|
|
8
|
+
throw internalErrorDiagnostics.INTERNAL_R0011({ message: `Invalid ${label} app. Expected an h3 app or a fetch-compatible handler.` });
|
|
8
9
|
}
|
|
9
10
|
//#endregion
|
|
10
11
|
export { resolveAppFetch as t };
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { t as normalizeWorkflowOptions } from "./config-IJMb0eeZ.js";
|
|
2
|
+
import { t as workflowErrorDiagnostics } from "./error-diagnostics-pwNFvAHP.js";
|
|
3
|
+
import { n as getCloudflareWorkflowBindingName, o as encodeNameHex } from "./cloudflare-nF4abPd4.js";
|
|
4
|
+
import { t as hasRuntimeType } from "./runtime-type-DBCvIh5x.js";
|
|
5
|
+
import { a as createWorkflowError, i as safeWorkflowName, r as runWorkflowProviderOperation } from "./provider-operation-CW2cYfVy.js";
|
|
6
|
+
import { n as getCloudflareEnv, r as resolveWaitUntil } from "./cloudflare-env-BNrYo29x.js";
|
|
7
|
+
import { runSerializedWorkflowHandler } from "./runtime/execute.js";
|
|
8
|
+
import { getOpenWorkflowRun, runOpenWorkflow } from "./runtime/openworkflow.js";
|
|
9
|
+
import { getWorkflowRunState, getWorkflowRuntimeConfig, getWorkflowRuntimeEvent, getWorkflowRuntimeRegistry, loadWorkflowDefinition, registerInlineWorkflowDefinition, runWithWorkflowRuntimeEvent, setWorkflowRun } from "./runtime/state.js";
|
|
10
|
+
import { a as startVercelWorkflow, n as inspectVercelWorkflowRun, r as resumeVercelWorkflowSignal, t as cancelVercelWorkflow } from "./vercel-D-ZNtB_u.js";
|
|
11
|
+
import { deserializeResponse, isSerializedResponse } from "@vite-hub/runtime";
|
|
12
|
+
//#region src/integrations/vercel.ts
|
|
13
|
+
function getVercelWorkflowName(name) {
|
|
14
|
+
return `workflow--${encodeNameHex(name)}`;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region ../internal/src/runtime/random.ts
|
|
18
|
+
function randomId(prefix) {
|
|
19
|
+
const body = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
20
|
+
return prefix ? `${prefix}_${body}` : body;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/runtime/adapters.ts
|
|
24
|
+
function resolveDispatchObserver(event) {
|
|
25
|
+
if (!event || !hasRuntimeType(event, "object")) return void 0;
|
|
26
|
+
const candidate = Object.hasOwn(event, "onDispatch") ? Reflect.get(event, "onDispatch") : void 0;
|
|
27
|
+
return hasRuntimeType(candidate, "function") ? candidate : void 0;
|
|
28
|
+
}
|
|
29
|
+
function resolveSettlementObserver(event) {
|
|
30
|
+
if (!event || !hasRuntimeType(event, "object")) return void 0;
|
|
31
|
+
const candidate = Object.hasOwn(event, "settled") ? Reflect.get(event, "settled") : void 0;
|
|
32
|
+
if (hasRuntimeType(candidate, "function")) return candidate;
|
|
33
|
+
const context = Object.hasOwn(event, "context") ? Reflect.get(event, "context") : void 0;
|
|
34
|
+
if (!context || !hasRuntimeType(context, "object")) return void 0;
|
|
35
|
+
const contextObject = Object(context);
|
|
36
|
+
const nested = Object.hasOwn(contextObject, "settled") ? Reflect.get(contextObject, "settled") : void 0;
|
|
37
|
+
return hasRuntimeType(nested, "function") ? nested : void 0;
|
|
38
|
+
}
|
|
39
|
+
function unsupportedOperation(provider, operation) {
|
|
40
|
+
throw createWorkflowError({
|
|
41
|
+
code: "WORKFLOW_OPERATION_UNSUPPORTED",
|
|
42
|
+
details: {
|
|
43
|
+
operation,
|
|
44
|
+
provider
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function resolveCloudflareBinding(event, binding, name, definition) {
|
|
49
|
+
const env = getCloudflareEnv(event);
|
|
50
|
+
const bindingName = definition?.internalAgentInvocationRecovery ? getCloudflareWorkflowBindingName(name) : binding || getCloudflareWorkflowBindingName(name);
|
|
51
|
+
return env && Object.hasOwn(env, bindingName) ? env[bindingName] : void 0;
|
|
52
|
+
}
|
|
53
|
+
function resolveCloudflareInspectionBinding(event, binding, name) {
|
|
54
|
+
return resolveCloudflareBinding(event, binding, name, getWorkflowRuntimeRegistry()?.[name]);
|
|
55
|
+
}
|
|
56
|
+
function normalizeCloudflareWorkflowStart(value, expectedId) {
|
|
57
|
+
if (!hasRuntimeType(value, "object") || value === null) throw new Error("Cloudflare Workflow provider returned an invalid workflow instance.");
|
|
58
|
+
let id;
|
|
59
|
+
try {
|
|
60
|
+
id = Reflect.get(value, "id");
|
|
61
|
+
} catch (cause) {
|
|
62
|
+
throw new Error("Cloudflare Workflow provider returned an invalid workflow instance.", { cause });
|
|
63
|
+
}
|
|
64
|
+
if (!hasRuntimeType(id, "string") || !id || id !== expectedId) throw new Error("Cloudflare Workflow provider returned an invalid workflow instance.");
|
|
65
|
+
return { id };
|
|
66
|
+
}
|
|
67
|
+
const cloudflareStatusMap = {
|
|
68
|
+
cancelled: "cancelled",
|
|
69
|
+
complete: "completed",
|
|
70
|
+
completed: "completed",
|
|
71
|
+
errored: "failed",
|
|
72
|
+
failed: "failed",
|
|
73
|
+
queued: "queued",
|
|
74
|
+
running: "running",
|
|
75
|
+
success: "completed",
|
|
76
|
+
terminated: "failed"
|
|
77
|
+
};
|
|
78
|
+
function normalizeCloudflareStatus(status) {
|
|
79
|
+
const value = hasRuntimeType(status, "object") && status && Object.hasOwn(status, "status") ? status.status : status;
|
|
80
|
+
const normalized = String(value || "").toLowerCase();
|
|
81
|
+
return Object.hasOwn(cloudflareStatusMap, normalized) ? cloudflareStatusMap[normalized] : "unknown";
|
|
82
|
+
}
|
|
83
|
+
function ownSerializedOutput(metadata) {
|
|
84
|
+
if (!hasRuntimeType(metadata, "object") || metadata === null || !Object.hasOwn(metadata, "output")) return void 0;
|
|
85
|
+
const output = metadata.output;
|
|
86
|
+
return isSerializedResponse(output) ? output : void 0;
|
|
87
|
+
}
|
|
88
|
+
function hasUnknownWorkflowAcknowledgement(error) {
|
|
89
|
+
if (!error || !hasRuntimeType(error, "object")) return false;
|
|
90
|
+
const details = error.details;
|
|
91
|
+
return Boolean(details && hasRuntimeType(details, "object") && details.acknowledgement === "unknown");
|
|
92
|
+
}
|
|
93
|
+
function createCloudflareAdapter(config) {
|
|
94
|
+
return {
|
|
95
|
+
cancel: () => unsupportedOperation("cloudflare", "cancellation"),
|
|
96
|
+
async get({ event, id, name }) {
|
|
97
|
+
const binding = resolveCloudflareInspectionBinding(event, config.binding, name);
|
|
98
|
+
if (binding) {
|
|
99
|
+
const instance = await runWorkflowProviderOperation("cloudflare", "get", () => binding.get(id));
|
|
100
|
+
const metadata = await runWorkflowProviderOperation("cloudflare", "status", () => instance.status());
|
|
101
|
+
const output = ownSerializedOutput(metadata);
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
metadata,
|
|
105
|
+
provider: "cloudflare",
|
|
106
|
+
result: output ? deserializeResponse(output) : void 0,
|
|
107
|
+
status: normalizeCloudflareStatus(metadata)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return await inlineAdapter(config).get({
|
|
111
|
+
event,
|
|
112
|
+
id,
|
|
113
|
+
name
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
async run({ definition, event, id, name, options, payload }) {
|
|
117
|
+
const binding = resolveCloudflareBinding(event, config.binding, name, definition);
|
|
118
|
+
if (binding) {
|
|
119
|
+
resolveDispatchObserver(event)?.();
|
|
120
|
+
const start = () => runWorkflowProviderOperation("cloudflare", "create", async () => normalizeCloudflareWorkflowStart((await binding.createBatch([{
|
|
121
|
+
id,
|
|
122
|
+
params: payload
|
|
123
|
+
}]))[0] || await binding.get(id), id), { acknowledgementUnknown: (_error, status) => status === void 0 });
|
|
124
|
+
const creation = start().catch(async (firstError) => {
|
|
125
|
+
try {
|
|
126
|
+
return await start();
|
|
127
|
+
} catch (retryError) {
|
|
128
|
+
if (hasUnknownWorkflowAcknowledgement(firstError)) throw firstError;
|
|
129
|
+
throw retryError;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
const waitUntil = options.deferred ? resolveWaitUntil(event) : void 0;
|
|
133
|
+
if (waitUntil) waitUntil(creation);
|
|
134
|
+
return {
|
|
135
|
+
id: (await creation).id,
|
|
136
|
+
payload,
|
|
137
|
+
provider: "cloudflare",
|
|
138
|
+
status: "queued"
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return await inlineAdapter(config).run({
|
|
142
|
+
definition,
|
|
143
|
+
event,
|
|
144
|
+
id,
|
|
145
|
+
name,
|
|
146
|
+
options,
|
|
147
|
+
payload
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
resume: () => unsupportedOperation("cloudflare", "signals")
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function createOpenWorkflowAdapter(config) {
|
|
154
|
+
return {
|
|
155
|
+
cancel: () => unsupportedOperation("openworkflow", "cancellation"),
|
|
156
|
+
get: async ({ id, name }) => await getOpenWorkflowRun(config, name, id),
|
|
157
|
+
resume: () => unsupportedOperation("openworkflow", "signals"),
|
|
158
|
+
run: async ({ definition, event, name, options, payload }) => await runOpenWorkflow(config, name, payload, definition, options, resolveDispatchObserver(event))
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function inlineAdapter(config) {
|
|
162
|
+
return {
|
|
163
|
+
cancel: () => unsupportedOperation(config.provider, "cancellation"),
|
|
164
|
+
async get({ id, name }) {
|
|
165
|
+
const run = getWorkflowRunState(name, id);
|
|
166
|
+
if (run) {
|
|
167
|
+
if (run.status === "running") return {
|
|
168
|
+
id,
|
|
169
|
+
provider: config.provider,
|
|
170
|
+
status: "running"
|
|
171
|
+
};
|
|
172
|
+
return {
|
|
173
|
+
id,
|
|
174
|
+
metadata: run.error,
|
|
175
|
+
provider: config.provider,
|
|
176
|
+
result: isSerializedResponse(run.result) ? deserializeResponse(run.result) : run.result,
|
|
177
|
+
status: run.status
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
id,
|
|
182
|
+
provider: config.provider,
|
|
183
|
+
status: "unknown"
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
async run({ definition, event, id, name, payload }) {
|
|
187
|
+
const existing = getWorkflowRunState(name, id);
|
|
188
|
+
if (existing) return {
|
|
189
|
+
id,
|
|
190
|
+
metadata: existing.error ?? (config.provider === "vercel" ? {
|
|
191
|
+
mode: "inline",
|
|
192
|
+
workflow: getVercelWorkflowName(name)
|
|
193
|
+
} : void 0),
|
|
194
|
+
payload: existing.payload,
|
|
195
|
+
provider: config.provider,
|
|
196
|
+
result: isSerializedResponse(existing.result) ? deserializeResponse(existing.result) : existing.result,
|
|
197
|
+
status: existing.status === "running" ? "queued" : existing.status
|
|
198
|
+
};
|
|
199
|
+
resolveDispatchObserver(event)?.();
|
|
200
|
+
const run = Promise.resolve().then(() => runSerializedWorkflowHandler({
|
|
201
|
+
id,
|
|
202
|
+
name,
|
|
203
|
+
payload,
|
|
204
|
+
provider: config.provider
|
|
205
|
+
}, definition)).then((result) => ({
|
|
206
|
+
result,
|
|
207
|
+
status: "completed"
|
|
208
|
+
})).catch((error) => ({
|
|
209
|
+
error,
|
|
210
|
+
status: "failed"
|
|
211
|
+
}));
|
|
212
|
+
const runState = setWorkflowRun(name, id, run, payload, true);
|
|
213
|
+
const waitUntil = resolveWaitUntil(event);
|
|
214
|
+
if (waitUntil) waitUntil(runState.promise);
|
|
215
|
+
return {
|
|
216
|
+
id,
|
|
217
|
+
metadata: config.provider === "vercel" ? {
|
|
218
|
+
mode: "inline",
|
|
219
|
+
workflow: getVercelWorkflowName(name)
|
|
220
|
+
} : void 0,
|
|
221
|
+
payload,
|
|
222
|
+
provider: config.provider,
|
|
223
|
+
status: "queued"
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
resume: () => unsupportedOperation(config.provider, "signals")
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function createVercelAdapter(config) {
|
|
230
|
+
const fallback = inlineAdapter(config);
|
|
231
|
+
return {
|
|
232
|
+
async cancel({ id, name }) {
|
|
233
|
+
const definition = await loadWorkflowDefinition(name);
|
|
234
|
+
if (!definition?.options?.native) return await fallback.cancel({
|
|
235
|
+
event: void 0,
|
|
236
|
+
id,
|
|
237
|
+
name
|
|
238
|
+
});
|
|
239
|
+
return await cancelVercelWorkflow(name, definition, id);
|
|
240
|
+
},
|
|
241
|
+
async get({ id, name }) {
|
|
242
|
+
const definition = await loadWorkflowDefinition(name);
|
|
243
|
+
return definition?.options?.native ? await inspectVercelWorkflowRun(name, definition, id) : await fallback.get({
|
|
244
|
+
event: void 0,
|
|
245
|
+
id,
|
|
246
|
+
name
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
resume: async (token, payload) => await resumeVercelWorkflowSignal(token, payload),
|
|
250
|
+
async run({ definition, event, id, name, options, payload }) {
|
|
251
|
+
if (!definition.options?.native) return await fallback.run({
|
|
252
|
+
definition,
|
|
253
|
+
event,
|
|
254
|
+
id,
|
|
255
|
+
name,
|
|
256
|
+
options,
|
|
257
|
+
payload
|
|
258
|
+
});
|
|
259
|
+
if (options.id) throw createWorkflowError({
|
|
260
|
+
code: "WORKFLOW_RUN_ID_UNSUPPORTED",
|
|
261
|
+
details: {
|
|
262
|
+
...safeWorkflowName(name) ? { name } : {},
|
|
263
|
+
provider: "vercel"
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
return await startVercelWorkflow(name, definition, payload, resolveSettlementObserver(event), resolveDispatchObserver(event));
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function getWorkflowRuntimeAdapter(config) {
|
|
271
|
+
if (config.provider === "cloudflare") return createCloudflareAdapter(config);
|
|
272
|
+
if (config.provider === "openworkflow") return createOpenWorkflowAdapter(config);
|
|
273
|
+
return createVercelAdapter(config);
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/runtime/client.ts
|
|
277
|
+
function getActiveWorkflowConfig() {
|
|
278
|
+
const config = getWorkflowRuntimeConfig();
|
|
279
|
+
if (config === false) throw createWorkflowError({ code: "WORKFLOW_DISABLED" });
|
|
280
|
+
return config || normalizeWorkflowOptions(void 0, { hosting: "vercel" });
|
|
281
|
+
}
|
|
282
|
+
async function loadRequiredWorkflowDefinition(name) {
|
|
283
|
+
const definition = await loadWorkflowDefinition(name);
|
|
284
|
+
if (!definition) throw createWorkflowError({
|
|
285
|
+
code: "WORKFLOW_DEFINITION_NOT_FOUND",
|
|
286
|
+
details: safeWorkflowName(name) ? { name } : void 0
|
|
287
|
+
});
|
|
288
|
+
return definition;
|
|
289
|
+
}
|
|
290
|
+
function normalizeWorkflowRunIdValue(value) {
|
|
291
|
+
if (value === void 0) return null;
|
|
292
|
+
if (value === null || !hasRuntimeType(value, "object")) return value;
|
|
293
|
+
if (Array.isArray(value)) return value.map((item) => normalizeWorkflowRunIdValue(item));
|
|
294
|
+
const record = value;
|
|
295
|
+
return Object.fromEntries(Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => [key, normalizeWorkflowRunIdValue(record[key])]));
|
|
296
|
+
}
|
|
297
|
+
async function sha256Hex(value) {
|
|
298
|
+
const data = new TextEncoder().encode(value);
|
|
299
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
300
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
301
|
+
}
|
|
302
|
+
async function createStableWorkflowRunId(name, value) {
|
|
303
|
+
const hash = await sha256Hex(`${name}:${JSON.stringify(normalizeWorkflowRunIdValue(value))}`);
|
|
304
|
+
return `${name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "workflow"}-${hash.slice(0, 32)}`;
|
|
305
|
+
}
|
|
306
|
+
async function resolveWorkflowStartOptions(name, payload, createOptions, startOptions) {
|
|
307
|
+
if (startOptions.id || !createOptions?.id) return startOptions;
|
|
308
|
+
return {
|
|
309
|
+
...startOptions,
|
|
310
|
+
id: await createStableWorkflowRunId(name, await createOptions.id({
|
|
311
|
+
name,
|
|
312
|
+
payload
|
|
313
|
+
}))
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function createWorkflow(nameOrOptions, handlerOrOptions, options) {
|
|
317
|
+
let name;
|
|
318
|
+
let handler;
|
|
319
|
+
let createOptions;
|
|
320
|
+
if (hasRuntimeType(nameOrOptions, "object") && nameOrOptions !== null) {
|
|
321
|
+
createOptions = nameOrOptions;
|
|
322
|
+
if (!createOptions.name || !hasRuntimeType(createOptions.name, "string")) throw workflowErrorDiagnostics.WORKFLOW_R0009({ message: "`createWorkflow()` requires a workflow name." });
|
|
323
|
+
if (!hasRuntimeType(createOptions.handler, "function")) throw workflowErrorDiagnostics.WORKFLOW_R0010({ message: "`createWorkflow()` requires a workflow handler." });
|
|
324
|
+
name = createOptions.name;
|
|
325
|
+
handler = createOptions.handler;
|
|
326
|
+
} else {
|
|
327
|
+
name = nameOrOptions;
|
|
328
|
+
if (!name || !hasRuntimeType(name, "string")) throw workflowErrorDiagnostics.WORKFLOW_R0011({ message: "`createWorkflow()` requires a workflow name." });
|
|
329
|
+
handler = hasRuntimeType(handlerOrOptions, "function") ? handlerOrOptions : hasRuntimeType(handlerOrOptions, "object") && handlerOrOptions !== null ? handlerOrOptions.handler : void 0;
|
|
330
|
+
createOptions = hasRuntimeType(handlerOrOptions, "function") ? options : handlerOrOptions;
|
|
331
|
+
if (handler !== void 0 && !hasRuntimeType(handler, "function")) throw workflowErrorDiagnostics.WORKFLOW_R0012({ message: "`createWorkflow()` requires a workflow handler." });
|
|
332
|
+
if (handler === void 0 && handlerOrOptions !== void 0 && (!hasRuntimeType(handlerOrOptions, "object") || handlerOrOptions === null)) throw workflowErrorDiagnostics.WORKFLOW_R0013({ message: "`createWorkflow()` options must be an object." });
|
|
333
|
+
}
|
|
334
|
+
if (handler !== void 0) registerInlineWorkflowDefinition(name, {
|
|
335
|
+
handler,
|
|
336
|
+
...createOptions?.rootStep === void 0 ? {} : { options: { rootStep: createOptions.rootStep } }
|
|
337
|
+
});
|
|
338
|
+
return {
|
|
339
|
+
cancel: (id) => cancelWorkflow(name, id),
|
|
340
|
+
name,
|
|
341
|
+
defer: async (payload, options = {}) => deferWorkflow(name, payload, await resolveWorkflowStartOptions(name, payload, createOptions, options)),
|
|
342
|
+
getRun: (id) => getWorkflowRun(name, id),
|
|
343
|
+
async run(payload, options = {}) {
|
|
344
|
+
return await runWorkflow(name, payload, await resolveWorkflowStartOptions(name, payload, createOptions, options));
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async function runWorkflow(name, payload, options = {}) {
|
|
349
|
+
const config = getActiveWorkflowConfig();
|
|
350
|
+
const id = options.id || randomId("wrun");
|
|
351
|
+
const definition = await loadRequiredWorkflowDefinition(name);
|
|
352
|
+
return await getWorkflowRuntimeAdapter(config).run({
|
|
353
|
+
definition,
|
|
354
|
+
event: getWorkflowRuntimeEvent(),
|
|
355
|
+
id,
|
|
356
|
+
name,
|
|
357
|
+
options,
|
|
358
|
+
payload
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
async function getWorkflowRun(name, id) {
|
|
362
|
+
return await getWorkflowRuntimeAdapter(getActiveWorkflowConfig()).get({
|
|
363
|
+
event: getWorkflowRuntimeEvent(),
|
|
364
|
+
id,
|
|
365
|
+
name
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
async function cancelWorkflow(name, id) {
|
|
369
|
+
const config = getActiveWorkflowConfig();
|
|
370
|
+
await loadRequiredWorkflowDefinition(name);
|
|
371
|
+
return await getWorkflowRuntimeAdapter(config).cancel({
|
|
372
|
+
event: getWorkflowRuntimeEvent(),
|
|
373
|
+
id,
|
|
374
|
+
name
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
async function resumeWorkflowSignal(token, payload) {
|
|
378
|
+
if (!token || !hasRuntimeType(token, "string")) throw workflowErrorDiagnostics.WORKFLOW_R0014({ message: "`resumeWorkflowSignal()` requires an opaque signal token." });
|
|
379
|
+
return await getWorkflowRuntimeAdapter(getActiveWorkflowConfig()).resume(token, payload);
|
|
380
|
+
}
|
|
381
|
+
function deferWorkflow(name, payload, options = {}) {
|
|
382
|
+
const request = getWorkflowRuntimeEvent();
|
|
383
|
+
return runWithWorkflowRuntimeEvent(request, () => runWorkflow(name, payload, {
|
|
384
|
+
...options,
|
|
385
|
+
deferred: true
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
export { resumeWorkflowSignal as a, getWorkflowRun as i, createWorkflow as n, runWorkflow as o, deferWorkflow as r, getVercelWorkflowName as s, cancelWorkflow as t };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
//#region ../internal/src/runtime/cloudflare-env.ts
|
|
3
|
+
let activeEnv;
|
|
4
|
+
const activeEnvStorageKey = Symbol.for("vitehub.cloudflare.env-storage");
|
|
5
|
+
const runtimeGlobal = globalThis;
|
|
6
|
+
const activeEnvStorage = runtimeGlobal[activeEnvStorageKey] ||= new AsyncLocalStorage();
|
|
7
|
+
const globalEnvProbe = Symbol("vitehub.cloudflare.global-env-probe");
|
|
8
|
+
/**
|
|
9
|
+
* Under the Nitro Cloudflare dev server, `cloudflare:workers` exports unenv's env proxy, which reads every key from
|
|
10
|
+
* `globalThis.__env__`. Storing that proxy in `__env__` makes it read itself, and the next read overflows the stack.
|
|
11
|
+
*/
|
|
12
|
+
function readsGlobalEnv(env) {
|
|
13
|
+
const scope = globalThis;
|
|
14
|
+
const owned = Object.hasOwn(scope, "__env__");
|
|
15
|
+
const previous = scope.__env__;
|
|
16
|
+
scope.__env__ = { [globalEnvProbe]: true };
|
|
17
|
+
try {
|
|
18
|
+
return Reflect.get(env, globalEnvProbe) === true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
} finally {
|
|
22
|
+
if (owned) scope.__env__ = previous;
|
|
23
|
+
else delete scope.__env__;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function setActiveCloudflareEnv(env) {
|
|
27
|
+
activeEnv = env;
|
|
28
|
+
if (!env || !readsGlobalEnv(env)) globalThis.__env__ = env;
|
|
29
|
+
try {
|
|
30
|
+
activeEnvStorage.enterWith({ env });
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
function runWithActiveCloudflareEnv(env, callback) {
|
|
34
|
+
return activeEnvStorage.run({ env }, callback);
|
|
35
|
+
}
|
|
36
|
+
function getActiveCloudflareEnv() {
|
|
37
|
+
const scoped = activeEnvStorage.getStore();
|
|
38
|
+
if (scoped) return scoped.env;
|
|
39
|
+
const host = globalThis;
|
|
40
|
+
return Object.hasOwn(host, "__env__") && host.__env__ !== void 0 ? host.__env__ : activeEnv;
|
|
41
|
+
}
|
|
42
|
+
function ownValue(value, key) {
|
|
43
|
+
if (value === null || typeof value !== "object" && typeof value !== "function" || !Object.hasOwn(value, key)) return void 0;
|
|
44
|
+
return Reflect.get(value, key);
|
|
45
|
+
}
|
|
46
|
+
function ownObject(value) {
|
|
47
|
+
return value !== null && (typeof value === "object" || typeof value === "function") ? value : void 0;
|
|
48
|
+
}
|
|
49
|
+
/** The objects where H3 1, H3 2, and Nitro hosts put Cloudflare `env` and `context`, in lookup order. */
|
|
50
|
+
function cloudflareOwners(target) {
|
|
51
|
+
const context = ownObject(ownValue(target, "context"));
|
|
52
|
+
const platform = ownObject(ownValue(context, "_platform"));
|
|
53
|
+
const req = ownObject(ownValue(target, "req"));
|
|
54
|
+
const nodeReq = ownObject(ownValue(ownObject(ownValue(target, "node")), "req"));
|
|
55
|
+
return [
|
|
56
|
+
ownObject(ownValue(context, "cloudflare")),
|
|
57
|
+
ownObject(ownValue(platform, "cloudflare")),
|
|
58
|
+
ownObject(ownValue(ownObject(ownValue(req, "runtime")), "cloudflare")),
|
|
59
|
+
ownObject(ownValue(ownObject(ownValue(nodeReq, "runtime")), "cloudflare"))
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function getCloudflareEnv(event, options = {}) {
|
|
63
|
+
const target = ownObject(event);
|
|
64
|
+
for (const candidate of [ownValue(target, "env"), ...cloudflareOwners(target).map((owner) => ownValue(owner, "env"))]) if (candidate) return candidate;
|
|
65
|
+
return options.fallback === false ? void 0 : getActiveCloudflareEnv();
|
|
66
|
+
}
|
|
67
|
+
function resolveWaitUntil(event, options = {}) {
|
|
68
|
+
const target = ownObject(event);
|
|
69
|
+
const context = ownObject(ownValue(target, "context"));
|
|
70
|
+
const cloudflare = ownObject(ownValue(context, "cloudflare"));
|
|
71
|
+
const cloudflareContext = ownObject(ownValue(cloudflare, "context"));
|
|
72
|
+
const platformCloudflare = ownObject(ownValue(ownObject(ownValue(context, "_platform")), "cloudflare"));
|
|
73
|
+
const platformCloudflareContext = ownObject(ownValue(platformCloudflare, "context"));
|
|
74
|
+
const req = ownObject(ownValue(target, "req"));
|
|
75
|
+
const reqCloudflare = ownObject(ownValue(ownObject(ownValue(req, "runtime")), "cloudflare"));
|
|
76
|
+
const reqCloudflareContext = ownObject(ownValue(reqCloudflare, "context"));
|
|
77
|
+
const nodeReq = ownObject(ownValue(ownObject(ownValue(target, "node")), "req"));
|
|
78
|
+
const nodeCloudflare = ownObject(ownValue(ownObject(ownValue(nodeReq, "runtime")), "cloudflare"));
|
|
79
|
+
const hosts = [
|
|
80
|
+
context,
|
|
81
|
+
cloudflare,
|
|
82
|
+
cloudflareContext,
|
|
83
|
+
platformCloudflare,
|
|
84
|
+
platformCloudflareContext,
|
|
85
|
+
req,
|
|
86
|
+
reqCloudflare,
|
|
87
|
+
reqCloudflareContext,
|
|
88
|
+
nodeReq,
|
|
89
|
+
nodeCloudflare,
|
|
90
|
+
ownObject(ownValue(nodeCloudflare, "context"))
|
|
91
|
+
];
|
|
92
|
+
const owners = options.preferHost ? [...hosts, target] : [target, ...hosts];
|
|
93
|
+
for (const owner of owners) {
|
|
94
|
+
const waitUntil = ownValue(owner, "waitUntil");
|
|
95
|
+
if (typeof waitUntil === "function") return waitUntil.bind(owner);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function createCloudflareRuntimeEvent(env, context) {
|
|
99
|
+
const waitUntil = typeof context?.waitUntil === "function" ? context.waitUntil.bind(context) : void 0;
|
|
100
|
+
return {
|
|
101
|
+
context: {
|
|
102
|
+
cloudflare: {
|
|
103
|
+
env,
|
|
104
|
+
context
|
|
105
|
+
},
|
|
106
|
+
waitUntil
|
|
107
|
+
},
|
|
108
|
+
env,
|
|
109
|
+
waitUntil
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
113
|
+
export { setActiveCloudflareEnv as a, runWithActiveCloudflareEnv as i, getCloudflareEnv as n, resolveWaitUntil as r, createCloudflareRuntimeEvent as t };
|