@lunora/workflow 0.0.0 → 1.0.0-alpha.1
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/LICENSE.md +105 -0
- package/README.md +260 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/do/index.d.mts +25 -0
- package/dist/do/index.d.ts +25 -0
- package/dist/do/index.mjs +32 -0
- package/dist/index.d.mts +269 -0
- package/dist/index.d.ts +269 -0
- package/dist/index.mjs +8 -0
- package/dist/packem_shared/WorkflowsRestError-b06i7K5j.mjs +118 -0
- package/dist/packem_shared/convertNonRetryableError-Dn2dTyBS.mjs +27 -0
- package/dist/packem_shared/createRunStep-BsK4LsUX.mjs +54 -0
- package/dist/packem_shared/createWorkflowContext-D6thzmlF.mjs +14 -0
- package/dist/packem_shared/createWorkflowLogger-FktqxNLe.mjs +76 -0
- package/dist/packem_shared/createWorkflows-BoSYVIXg.mjs +23 -0
- package/dist/packem_shared/defineStep-DJQtLw7g.mjs +28 -0
- package/dist/packem_shared/defineWorkflow-DbUC-oCN.mjs +15 -0
- package/dist/packem_shared/types.d-CQO_koGe.d.mts +344 -0
- package/dist/packem_shared/types.d-CQO_koGe.d.ts +344 -0
- package/package.json +42 -15
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const API_BASE = "https://api.cloudflare.com/client/v4/accounts";
|
|
2
|
+
const KNOWN_STATUSES = {
|
|
3
|
+
complete: true,
|
|
4
|
+
errored: true,
|
|
5
|
+
paused: true,
|
|
6
|
+
queued: true,
|
|
7
|
+
running: true,
|
|
8
|
+
terminated: true,
|
|
9
|
+
unknown: true,
|
|
10
|
+
waiting: true,
|
|
11
|
+
waitingForPause: true
|
|
12
|
+
};
|
|
13
|
+
const toStatus = (value) => typeof value === "string" && Object.hasOwn(KNOWN_STATUSES, value) ? value : "unknown";
|
|
14
|
+
const asString = (value) => typeof value === "string" && value !== "" ? value : void 0;
|
|
15
|
+
const stringOr = (value, fallback) => typeof value === "string" ? value : fallback;
|
|
16
|
+
const asBoolean = (value) => typeof value === "boolean" ? value : void 0;
|
|
17
|
+
const countAttempts = (value) => {
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
return value.length;
|
|
20
|
+
}
|
|
21
|
+
return typeof value === "number" ? value : void 0;
|
|
22
|
+
};
|
|
23
|
+
const toSummary = (raw) => {
|
|
24
|
+
return {
|
|
25
|
+
createdOn: asString(raw["created_on"]),
|
|
26
|
+
endedOn: asString(raw["ended_on"]),
|
|
27
|
+
id: stringOr(raw["id"], ""),
|
|
28
|
+
startedOn: asString(raw["started_on"]),
|
|
29
|
+
status: toStatus(raw["status"])
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const toStep = (raw) => {
|
|
33
|
+
return {
|
|
34
|
+
attempts: countAttempts(raw["attempts"]),
|
|
35
|
+
end: asString(raw["end"]),
|
|
36
|
+
error: raw["error"],
|
|
37
|
+
name: stringOr(raw["name"], ""),
|
|
38
|
+
output: raw["output"],
|
|
39
|
+
start: asString(raw["start"]),
|
|
40
|
+
success: asBoolean(raw["success"]),
|
|
41
|
+
type: asString(raw["type"])
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
class WorkflowsRestError extends Error {
|
|
45
|
+
status;
|
|
46
|
+
constructor(status, body) {
|
|
47
|
+
super(`Cloudflare Workflows REST API returned ${String(status)}: ${body}`);
|
|
48
|
+
this.name = "WorkflowsRestError";
|
|
49
|
+
this.status = status;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const createWorkflowsRestClient = (config) => {
|
|
53
|
+
const fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
54
|
+
const base = `${API_BASE}/${config.accountId}/workflows`;
|
|
55
|
+
const request = async (path, init) => {
|
|
56
|
+
const response = await fetchImpl(`${base}${path}`, {
|
|
57
|
+
...init,
|
|
58
|
+
headers: { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" }
|
|
59
|
+
});
|
|
60
|
+
const text = await response.text();
|
|
61
|
+
let body;
|
|
62
|
+
try {
|
|
63
|
+
body = JSON.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
throw new WorkflowsRestError(response.status, text);
|
|
66
|
+
}
|
|
67
|
+
if (!response.ok || body["success"] === false) {
|
|
68
|
+
throw new WorkflowsRestError(response.status, text);
|
|
69
|
+
}
|
|
70
|
+
return body;
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
getInstance: async ({ instanceId, workflowName }) => {
|
|
74
|
+
const body = await request(`/${encodeURIComponent(workflowName)}/instances/${encodeURIComponent(instanceId)}`);
|
|
75
|
+
const result = body["result"] ?? {};
|
|
76
|
+
const steps = Array.isArray(result["steps"]) ? result["steps"] : [];
|
|
77
|
+
return {
|
|
78
|
+
...toSummary(result),
|
|
79
|
+
error: result["error"],
|
|
80
|
+
output: result["output"],
|
|
81
|
+
params: result["params"],
|
|
82
|
+
steps: steps.map((step) => toStep(step))
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
listInstances: async ({ page, perPage, status, workflowName }) => {
|
|
86
|
+
const query = new URLSearchParams();
|
|
87
|
+
if (status !== void 0) {
|
|
88
|
+
query.set("status", status);
|
|
89
|
+
}
|
|
90
|
+
if (page !== void 0) {
|
|
91
|
+
query.set("page", String(page));
|
|
92
|
+
}
|
|
93
|
+
if (perPage !== void 0) {
|
|
94
|
+
query.set("per_page", String(perPage));
|
|
95
|
+
}
|
|
96
|
+
const suffix = query.toString() === "" ? "" : `?${query.toString()}`;
|
|
97
|
+
const body = await request(`/${encodeURIComponent(workflowName)}/instances${suffix}`);
|
|
98
|
+
const result = Array.isArray(body["result"]) ? body["result"] : [];
|
|
99
|
+
const info = body["result_info"] ?? {};
|
|
100
|
+
return {
|
|
101
|
+
instances: result.map((instance) => toSummary(instance)),
|
|
102
|
+
page: typeof info["page"] === "number" ? info["page"] : page ?? 1,
|
|
103
|
+
perPage: typeof info["per_page"] === "number" ? info["per_page"] : perPage ?? result.length,
|
|
104
|
+
totalCount: typeof info["total_count"] === "number" ? info["total_count"] : void 0
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
setInstanceStatus: async ({ action, instanceId, workflowName }) => {
|
|
108
|
+
const body = await request(`/${encodeURIComponent(workflowName)}/instances/${encodeURIComponent(instanceId)}`, {
|
|
109
|
+
body: JSON.stringify({ status: action }),
|
|
110
|
+
method: "PATCH"
|
|
111
|
+
});
|
|
112
|
+
const result = body["result"] ?? {};
|
|
113
|
+
return { status: toStatus(result["status"]) };
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export { WorkflowsRestError, createWorkflowsRestClient };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const NON_RETRYABLE_BRAND = "__lunoraNonRetryable";
|
|
2
|
+
class NonRetryableError extends Error {
|
|
3
|
+
constructor(message, name = "NonRetryableError") {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = name;
|
|
6
|
+
this[NON_RETRYABLE_BRAND] = true;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const isNonRetryableError = (value) => value instanceof Error && value[NON_RETRYABLE_BRAND] === true;
|
|
10
|
+
const toNativeNonRetryableError = (error, NativeNonRetryableError) => {
|
|
11
|
+
const native = new NativeNonRetryableError(error.message, error.name);
|
|
12
|
+
if (error.stack !== void 0) {
|
|
13
|
+
native.stack = error.stack;
|
|
14
|
+
}
|
|
15
|
+
if (error.cause !== void 0 && native.cause === void 0) {
|
|
16
|
+
native.cause = error.cause;
|
|
17
|
+
}
|
|
18
|
+
return native;
|
|
19
|
+
};
|
|
20
|
+
const convertNonRetryableError = (error, NativeNonRetryableError) => {
|
|
21
|
+
if (NativeNonRetryableError !== void 0 && isNonRetryableError(error)) {
|
|
22
|
+
throw toNativeNonRetryableError(error, NativeNonRetryableError);
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { NonRetryableError, convertNonRetryableError, isNonRetryableError, toNativeNonRetryableError };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { parseValidatorMap } from '@lunora/values';
|
|
2
|
+
import { convertNonRetryableError, NonRetryableError } from './convertNonRetryableError-Dn2dTyBS.mjs';
|
|
3
|
+
|
|
4
|
+
const validateStepArgs = (validators, source) => parseValidatorMap(validators, source, "step args");
|
|
5
|
+
const createRunStep = (deps) => async (step, args, options) => {
|
|
6
|
+
const config = options?.config ?? step.config;
|
|
7
|
+
const validatedArgs = validateStepArgs(step.args, args);
|
|
8
|
+
const callback = async (nativeContext) => {
|
|
9
|
+
const stepContext = {
|
|
10
|
+
attempt: nativeContext.attempt,
|
|
11
|
+
config: nativeContext.config,
|
|
12
|
+
env: deps.env,
|
|
13
|
+
log: deps.log,
|
|
14
|
+
run: deps.run,
|
|
15
|
+
step: nativeContext.step
|
|
16
|
+
};
|
|
17
|
+
let result;
|
|
18
|
+
try {
|
|
19
|
+
result = await step.handler(stepContext, validatedArgs);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return convertNonRetryableError(error, deps.nonRetryableErrorClass);
|
|
22
|
+
}
|
|
23
|
+
if (!step.returns) {
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
return step.returns.parse(result);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
30
|
+
const nonRetryable = new NonRetryableError(`step "${step.name}" returns validation failed: ${message}`);
|
|
31
|
+
if (error !== void 0) {
|
|
32
|
+
nonRetryable.cause = error;
|
|
33
|
+
}
|
|
34
|
+
return convertNonRetryableError(nonRetryable, deps.nonRetryableErrorClass);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const rollbackHandler = step.rollback;
|
|
38
|
+
const rollbackOptions = rollbackHandler ? {
|
|
39
|
+
rollback: async (rollbackContext) => {
|
|
40
|
+
await rollbackHandler({
|
|
41
|
+
args: validatedArgs,
|
|
42
|
+
env: deps.env,
|
|
43
|
+
error: rollbackContext.error,
|
|
44
|
+
log: deps.log,
|
|
45
|
+
output: rollbackContext.output,
|
|
46
|
+
run: deps.run
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
rollbackConfig: step.rollbackConfig
|
|
50
|
+
} : void 0;
|
|
51
|
+
return config === void 0 ? deps.step.do(step.name, callback, rollbackOptions) : deps.step.do(step.name, config, callback, rollbackOptions);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { createRunStep, validateStepArgs };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import createWorkflows from './createWorkflows-BoSYVIXg.mjs';
|
|
2
|
+
|
|
3
|
+
const createWorkflowContext = (env, specs) => {
|
|
4
|
+
const bindings = {};
|
|
5
|
+
for (const spec of specs) {
|
|
6
|
+
const binding = env[spec.binding];
|
|
7
|
+
if (binding && typeof binding.create === "function" && typeof binding.createBatch === "function" && typeof binding.get === "function") {
|
|
8
|
+
bindings[spec.exportName] = binding;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return createWorkflows({ bindings });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { createWorkflowContext };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createRunStep } from './createRunStep-BsK4LsUX.mjs';
|
|
2
|
+
|
|
3
|
+
const trimTrailingSlashes = (value) => {
|
|
4
|
+
let end = value.length;
|
|
5
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
6
|
+
end -= 1;
|
|
7
|
+
}
|
|
8
|
+
return value.slice(0, end);
|
|
9
|
+
};
|
|
10
|
+
const createWorkflowRunner = (options) => {
|
|
11
|
+
const globalFetch = globalThis.fetch;
|
|
12
|
+
const fetchImpl = options.fetchImpl ?? (typeof globalFetch === "function" ? globalFetch.bind(globalThis) : void 0);
|
|
13
|
+
return async (function_, args, runOptions = {}) => {
|
|
14
|
+
if (typeof fetchImpl !== "function") {
|
|
15
|
+
throw new TypeError("@lunora/workflow: no fetch implementation available — pass fetchImpl or run on a platform with global fetch");
|
|
16
|
+
}
|
|
17
|
+
const origin = options.env.LUNORA_ORIGIN_URL;
|
|
18
|
+
if (typeof origin !== "string" || origin.length === 0) {
|
|
19
|
+
throw new Error("@lunora/workflow: `LUNORA_ORIGIN_URL` must be set on the Worker env so a workflow can call back into Lunora functions");
|
|
20
|
+
}
|
|
21
|
+
const token = options.env.LUNORA_ADMIN_TOKEN;
|
|
22
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
23
|
+
throw new Error("@lunora/workflow: `LUNORA_ADMIN_TOKEN` must be set on the Worker env to authenticate workflow function dispatch");
|
|
24
|
+
}
|
|
25
|
+
const url = `${trimTrailingSlashes(origin)}/_lunora/scheduler/dispatch`;
|
|
26
|
+
const response = await fetchImpl(url, {
|
|
27
|
+
body: JSON.stringify({ args: args ?? {}, functionPath: function_.__lunoraRef, shardKey: runOptions.shardKey }),
|
|
28
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
29
|
+
method: "POST"
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
throw new Error(`@lunora/workflow: function dispatch failed (${String(response.status)}): ${await response.text()}`);
|
|
33
|
+
}
|
|
34
|
+
const text = await response.text();
|
|
35
|
+
if (text.length === 0) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(text);
|
|
40
|
+
} catch {
|
|
41
|
+
return text;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
const createWorkflowLogger = (exportName) => {
|
|
46
|
+
const prefix = `[workflow:${exportName}]`;
|
|
47
|
+
return {
|
|
48
|
+
debug: (message, ...rest) => {
|
|
49
|
+
console.debug(prefix, message, ...rest);
|
|
50
|
+
},
|
|
51
|
+
error: (message, ...rest) => {
|
|
52
|
+
console.error(prefix, message, ...rest);
|
|
53
|
+
},
|
|
54
|
+
info: (message, ...rest) => {
|
|
55
|
+
console.info(prefix, message, ...rest);
|
|
56
|
+
},
|
|
57
|
+
warn: (message, ...rest) => {
|
|
58
|
+
console.warn(prefix, message, ...rest);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const createWorkflowRunContext = (options) => {
|
|
63
|
+
const log = createWorkflowLogger(options.exportName);
|
|
64
|
+
const run = createWorkflowRunner({ env: options.env, fetchImpl: options.fetchImpl });
|
|
65
|
+
return {
|
|
66
|
+
env: options.env,
|
|
67
|
+
event: options.event,
|
|
68
|
+
log,
|
|
69
|
+
params: options.event.payload,
|
|
70
|
+
run,
|
|
71
|
+
runStep: createRunStep({ env: options.env, log, nonRetryableErrorClass: options.nonRetryableErrorClass, run, step: options.step }),
|
|
72
|
+
step: options.step
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { createWorkflowLogger, createWorkflowRunContext, createWorkflowRunner };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const handleFor = (binding) => {
|
|
2
|
+
return {
|
|
3
|
+
create: async (options) => binding.create(options),
|
|
4
|
+
createBatch: async (batch) => binding.createBatch(batch),
|
|
5
|
+
get: async (id) => binding.get(id)
|
|
6
|
+
};
|
|
7
|
+
};
|
|
8
|
+
const createWorkflows = (options) => {
|
|
9
|
+
const bindings = options.bindings ?? {};
|
|
10
|
+
return {
|
|
11
|
+
get: (name) => {
|
|
12
|
+
const binding = bindings[name];
|
|
13
|
+
if (binding === void 0) {
|
|
14
|
+
const known = Object.keys(bindings);
|
|
15
|
+
const suffix = known.length === 0 ? "no workflows are declared" : `known workflows: ${known.join(", ")}`;
|
|
16
|
+
throw new Error(`@lunora/workflow: no workflow named "${name}" (${suffix})`);
|
|
17
|
+
}
|
|
18
|
+
return handleFor(binding);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export { createWorkflows as default };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const defineStep = (name, config) => {
|
|
2
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
3
|
+
throw new TypeError("defineStep: `name` must be a non-empty string (the durable step label)");
|
|
4
|
+
}
|
|
5
|
+
const declaredArgs = config.args;
|
|
6
|
+
if (typeof declaredArgs !== "object" || declaredArgs === null) {
|
|
7
|
+
throw new TypeError("defineStep: `args` must be a validator map (e.g. `{ id: v.string() }`)");
|
|
8
|
+
}
|
|
9
|
+
if (typeof config.handler !== "function") {
|
|
10
|
+
throw new TypeError("defineStep: `handler` must be a function (the step body)");
|
|
11
|
+
}
|
|
12
|
+
if (config.rollback !== void 0 && typeof config.rollback !== "function") {
|
|
13
|
+
throw new TypeError("defineStep: `rollback` must be a function when provided");
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
args: config.args,
|
|
17
|
+
config: config.config,
|
|
18
|
+
handler: config.handler,
|
|
19
|
+
isLunoraStep: true,
|
|
20
|
+
name,
|
|
21
|
+
returns: config.returns,
|
|
22
|
+
rollback: config.rollback,
|
|
23
|
+
rollbackConfig: config.rollbackConfig
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
const isStepDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraStep === true;
|
|
27
|
+
|
|
28
|
+
export { defineStep, isStepDefinition };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const workflowClassName = (exportName) => `${exportName.charAt(0).toUpperCase()}${exportName.slice(1)}Workflow`;
|
|
2
|
+
const workflowBindingName = (exportName) => `WORKFLOW_${exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "_").toUpperCase()}`;
|
|
3
|
+
const workflowDefaultName = (exportName) => exportName.replaceAll(/(?<=[a-z0-9])(?=[A-Z])/g, "-").toLowerCase();
|
|
4
|
+
const defineWorkflow = (config) => {
|
|
5
|
+
if (typeof config.handler !== "function") {
|
|
6
|
+
throw new TypeError("defineWorkflow: `handler` must be a function (the workflow body)");
|
|
7
|
+
}
|
|
8
|
+
if (config.name !== void 0 && (typeof config.name !== "string" || config.name.length === 0)) {
|
|
9
|
+
throw new TypeError("defineWorkflow: `name` must be a non-empty string when provided");
|
|
10
|
+
}
|
|
11
|
+
return { ...config, isLunoraWorkflow: true };
|
|
12
|
+
};
|
|
13
|
+
const isWorkflowDefinition = (value) => typeof value === "object" && value !== null && value.isLunoraWorkflow === true;
|
|
14
|
+
|
|
15
|
+
export { defineWorkflow, isWorkflowDefinition, workflowBindingName, workflowClassName, workflowDefaultName };
|