@temelj/workflow 0.0.0-alpha.1 → 0.0.0-alpha.2

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.
Files changed (64) hide show
  1. package/dist/bundle.d.mts +29 -0
  2. package/dist/bundle.mjs +28 -0
  3. package/dist/client/create.d.mts +10 -2
  4. package/dist/client/create.mjs +5 -3
  5. package/dist/client/facades.mjs +6 -3
  6. package/dist/client/run-handle.d.mts +1 -0
  7. package/dist/client/run-handle.mjs +3 -1
  8. package/dist/cloudflare/cloudflare.d.mts +12 -0
  9. package/dist/cloudflare/codec.d.mts +16 -0
  10. package/dist/cloudflare/codec.mjs +25 -0
  11. package/dist/cloudflare/entrypoint.d.mts +12 -0
  12. package/dist/cloudflare/entrypoint.mjs +18 -0
  13. package/dist/cloudflare/execution.d.mts +13 -0
  14. package/dist/cloudflare/execution.mjs +28 -0
  15. package/dist/cloudflare/host.d.mts +19 -0
  16. package/dist/cloudflare/host.mjs +24 -0
  17. package/dist/cloudflare/mod.d.mts +8 -0
  18. package/dist/cloudflare/mod.mjs +7 -0
  19. package/dist/cloudflare/name.d.mts +5 -0
  20. package/dist/cloudflare/name.mjs +22 -0
  21. package/dist/cloudflare/registry.d.mts +24 -0
  22. package/dist/cloudflare/registry.mjs +26 -0
  23. package/dist/cloudflare/runtime.d.mts +30 -0
  24. package/dist/cloudflare/runtime.mjs +226 -0
  25. package/dist/cloudflare/step.d.mts +22 -0
  26. package/dist/cloudflare/step.mjs +160 -0
  27. package/dist/cloudflare/types.d.mts +87 -0
  28. package/dist/cloudflare/types.mjs +1 -0
  29. package/dist/engine/inspection-api.mjs +2 -1
  30. package/dist/engine/inspection.mjs +7 -5
  31. package/dist/engine/message.mjs +16 -13
  32. package/dist/engine/observation.mjs +1 -3
  33. package/dist/engine/run-data.mjs +2 -1
  34. package/dist/engine/run-service.mjs +2 -1
  35. package/dist/engine/start-api.mjs +2 -1
  36. package/dist/engine/state.mjs +2 -1
  37. package/dist/engine/stream.d.mts +1 -0
  38. package/dist/engine/stream.mjs +1 -0
  39. package/dist/engine/task.mjs +2 -1
  40. package/dist/errors/definition.d.mts +1 -0
  41. package/dist/errors/definition.mjs +3 -0
  42. package/dist/errors/details.mjs +2 -1
  43. package/dist/history/replay-projection.mjs +1 -3
  44. package/dist/mod.d.mts +2 -1
  45. package/dist/mod.mjs +2 -1
  46. package/dist/otel.mjs +4 -2
  47. package/dist/runtime.d.mts +1 -0
  48. package/dist/runtime.mjs +7 -3
  49. package/dist/store/cleanup.mjs +3 -26
  50. package/dist/store/message-index.mjs +2 -1
  51. package/dist/store/query.mjs +5 -3
  52. package/dist/store/run.mjs +9 -6
  53. package/dist/store/schedule.mjs +5 -3
  54. package/dist/store/step-attempt.mjs +5 -3
  55. package/dist/store-keys.d.mts +2 -1
  56. package/dist/store-keys.mjs +24 -1
  57. package/dist/types/common.d.mts +1 -0
  58. package/dist/types/common.mjs +1 -0
  59. package/dist/types/runtime.d.mts +10 -2
  60. package/dist/worker/error.d.mts +1 -0
  61. package/dist/worker/error.mjs +1 -0
  62. package/dist/worker.d.mts +1 -0
  63. package/dist/worker.mjs +1 -0
  64. package/package.json +15 -10
@@ -0,0 +1,226 @@
1
+ import { WorkflowResultTimeoutError, WorkflowRunCanceledError } from "../errors/run.mjs";
2
+ import { durationTotal } from "../utility.mjs";
3
+ import { WorkflowCapabilityError } from "../errors/capability.mjs";
4
+ import { WorkflowDefinitionError } from "../errors/definition.mjs";
5
+ import "../errors/mod.mjs";
6
+ import { getCompiledWorkflowDefinition } from "../compiled-function.mjs";
7
+ import { Registry } from "../registry.mjs";
8
+ import { parseWorkflowInput } from "../definition.mjs";
9
+ import { resolveSendMessageInput } from "../message-options.mjs";
10
+ import { decodeCloudflareWorkflowValue, encodeCloudflareWorkflowValue } from "./codec.mjs";
11
+ import { resolveCloudflareWorkflowImplementation } from "./registry.mjs";
12
+ import { makeCloudflareWorkflowEventType } from "./name.mjs";
13
+ //#region src/cloudflare/runtime.ts
14
+ const DEFAULT_RESULT_POLL_INTERVAL = 100;
15
+ const CLOUDFLARE_INSTANCE_ID_PATTERN = /^[a-zA-Z0-9_][a-zA-Z0-9-_]*$/;
16
+ /** Creates a runtime facade over a generic Cloudflare Workflow binding. */
17
+ function createCloudflareWorkflowRuntime(options) {
18
+ return new CloudflareWorkflowRuntimeImpl(options);
19
+ }
20
+ var CloudflareWorkflowRuntimeImpl = class {
21
+ runs;
22
+ messages;
23
+ schedules = unsupportedApi("Cloudflare workflow schedules");
24
+ locks = unsupportedApi("Cloudflare workflow locks");
25
+ workers = unsupportedApi("Cloudflare workflow workers");
26
+ #binding;
27
+ #registry;
28
+ #registration;
29
+ #resultPollInterval;
30
+ constructor(options) {
31
+ this.#binding = options.binding;
32
+ this.#registry = options.registry ?? new Registry();
33
+ this.#registration = options.registration ?? "dynamic";
34
+ this.#resultPollInterval = options.resultPollInterval === void 0 ? DEFAULT_RESULT_POLL_INTERVAL : durationMilliseconds(options.resultPollInterval, "Cloudflare result poll interval");
35
+ this.runs = {
36
+ getHandle: async () => WorkflowCapabilityError.unsupported("Cloudflare run lookup without a workflow target; use runtime.runs.get(workflow, runId)"),
37
+ list: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow run listing"),
38
+ get: async (workflow, runId) => this.#createHandle(workflow, runId)
39
+ };
40
+ this.messages = { send: async (runId, input, channelOptions) => {
41
+ const message = await resolveSendMessageInput(input, channelOptions);
42
+ await (await this.#binding.get(runId)).sendEvent({
43
+ type: makeCloudflareWorkflowEventType(message.messageId),
44
+ payload: encodeCloudflareWorkflowValue(message.payload)
45
+ });
46
+ } };
47
+ }
48
+ async close() {}
49
+ implementWorkflow(definition, handler) {
50
+ if (this.#registration === "static") {
51
+ const implementation = this.#registry.get(definition);
52
+ if (implementation === void 0 || implementation.handler !== handler) WorkflowDefinitionError.implementationNotBundled(workflowImplementationKey(definition.name, definition.version));
53
+ return implementation;
54
+ }
55
+ return this.#registry.implementWorkflow(definition, handler);
56
+ }
57
+ register(implementation) {
58
+ if (this.#registry.get(implementation.definition) !== implementation) {
59
+ if (this.#registration === "static") WorkflowDefinitionError.implementationNotBundled(workflowImplementationKey(implementation.name, implementation.version));
60
+ this.#registry.register(implementation);
61
+ }
62
+ }
63
+ async start(workflow, ...args) {
64
+ if (typeof workflow === "function") {
65
+ const implementation = getCompiledWorkflowDefinition(workflow);
66
+ if (implementation === void 0) WorkflowDefinitionError.compiledMetadataMissing();
67
+ this.register(implementation);
68
+ return await this.#startTarget(implementation, args, void 0);
69
+ }
70
+ const target = workflow;
71
+ if (target.kind === "implementation") this.register(target);
72
+ return await this.#startTarget(target, args[0], args[1]);
73
+ }
74
+ async #startTarget(target, rawInput, startOptions) {
75
+ rejectUnsupportedStartOptions(startOptions);
76
+ const requestedVersion = startOptions?.workflowVersion ?? target.version;
77
+ const implementation = resolveCloudflareWorkflowImplementation(this.#registry, target.name, requestedVersion);
78
+ if (implementation.config.retry !== void 0) WorkflowCapabilityError.unsupported("Cloudflare workflow-level retry configuration");
79
+ const input = await parseWorkflowInput(implementation.definition, rawInput);
80
+ const envelope = {
81
+ kind: "temelj-workflow",
82
+ version: 1,
83
+ workflowName: implementation.name,
84
+ ...implementation.version === void 0 ? {} : { workflowVersion: implementation.version },
85
+ input: encodeCloudflareWorkflowValue(input)
86
+ };
87
+ const id = await resolveInstanceId(implementation.name, implementation.version, startOptions);
88
+ const createdAt = Temporal.Now.instant();
89
+ let instance;
90
+ if (startOptions?.idempotencyKey === void 0) instance = await this.#binding.create({
91
+ ...id === void 0 ? {} : { id },
92
+ params: envelope
93
+ });
94
+ else {
95
+ if (id === void 0) throw new TypeError("Cloudflare idempotent workflow start requires a resolved instance id");
96
+ instance = (await this.#binding.createBatch([{
97
+ id,
98
+ params: envelope
99
+ }]))[0] ?? await this.#binding.get(id);
100
+ }
101
+ return this.#handle(implementation.definition, instance, createdAt);
102
+ }
103
+ async #createHandle(workflow, runId) {
104
+ const implementation = resolveCloudflareWorkflowImplementation(this.#registry, workflow.name, workflow.version);
105
+ const instance = await this.#binding.get(runId);
106
+ return this.#handle(implementation.definition, instance, Temporal.Now.instant());
107
+ }
108
+ #handle(definition, instance, createdAt) {
109
+ const status = async () => await instance.status();
110
+ const record = async () => workflowRunRecord(definition, instance.id, createdAt, await status());
111
+ return {
112
+ runId: instance.id,
113
+ workflowName: definition.name,
114
+ ...definition.version === void 0 ? {} : { workflowVersion: definition.version },
115
+ getRun: record,
116
+ status: async () => cloudflareRunStatus((await status()).status),
117
+ events: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow event history"),
118
+ timeline: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow timeline"),
119
+ result: async (options) => await cloudflareWorkflowResult(instance, options, this.#resultPollInterval),
120
+ getStreamInfo: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow streams"),
121
+ followStream: () => WorkflowCapabilityError.unsupported("Cloudflare workflow streams"),
122
+ waitForStream: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow streams"),
123
+ waitForStreamChunk: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow streams"),
124
+ waitForStreamTerminal: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow streams"),
125
+ readStream: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow streams"),
126
+ readStreamPage: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow streams"),
127
+ reschedule: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow rescheduling"),
128
+ releaseStaleLease: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow worker leases"),
129
+ retryFailed: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow manual retries"),
130
+ markPermanentlyFailed: async () => WorkflowCapabilityError.unsupported("Cloudflare permanent workflow failure"),
131
+ setMetadata: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow metadata"),
132
+ setAttributes: async () => WorkflowCapabilityError.unsupported("Cloudflare workflow attributes"),
133
+ async cancel() {
134
+ if (!isTerminalCloudflareStatus((await instance.status()).status)) await instance.terminate();
135
+ return await record();
136
+ }
137
+ };
138
+ }
139
+ };
140
+ function workflowImplementationKey(name, version) {
141
+ return version === void 0 ? name : `${name}@${version}`;
142
+ }
143
+ function isTerminalCloudflareStatus(status) {
144
+ return status === "complete" || status === "errored" || status === "terminated";
145
+ }
146
+ async function cloudflareWorkflowResult(instance, options, defaultPollInterval) {
147
+ const pollInterval = options?.pollInterval === void 0 ? defaultPollInterval : durationMilliseconds(options.pollInterval, "Cloudflare result poll interval");
148
+ const timeout = options?.timeout === void 0 ? void 0 : durationMilliseconds(options.timeout, "Cloudflare result timeout");
149
+ const startedAt = Date.now();
150
+ while (true) {
151
+ const state = await instance.status();
152
+ if (state.status === "complete") return decodeCloudflareWorkflowValue(state.output, `Cloudflare Workflow ${instance.id} output`);
153
+ if (state.status === "errored") {
154
+ const error = new Error(state.error?.message ?? "Cloudflare Workflow failed");
155
+ error.name = state.error?.name ?? "Error";
156
+ throw error;
157
+ }
158
+ if (state.status === "terminated") WorkflowRunCanceledError.canceled(instance.id);
159
+ if (state.status === "unknown") throw new TypeError("Cloudflare Workflow returned an unknown instance status");
160
+ if (timeout !== void 0 && Date.now() - startedAt >= timeout) WorkflowResultTimeoutError.timedOut(instance.id, Temporal.Duration.from({ milliseconds: timeout }));
161
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
162
+ }
163
+ }
164
+ function workflowRunRecord(definition, runId, createdAt, state) {
165
+ const now = Temporal.Now.instant();
166
+ const status = cloudflareRunStatus(state.status);
167
+ return {
168
+ id: runId,
169
+ namespace: "cloudflare",
170
+ workflowName: definition.name,
171
+ ...definition.version === void 0 ? {} : { workflowVersion: definition.version },
172
+ status,
173
+ ...state.status === "complete" ? { output: decodeCloudflareWorkflowValue(state.output, `Cloudflare Workflow ${runId} output`) } : {},
174
+ ...state.error === void 0 ? {} : { error: {
175
+ name: state.error.name,
176
+ message: state.error.message
177
+ } },
178
+ createdAt,
179
+ updatedAt: now,
180
+ lastTransitionAt: now,
181
+ lastTransitionReason: status === "completed" ? "completed" : status === "failed" ? "failed" : status === "canceled" ? "canceled" : status === "waiting" ? "waiting" : status === "running" ? "started" : "created",
182
+ ...status === "completed" || status === "failed" || status === "canceled" ? { finishedAt: now } : {}
183
+ };
184
+ }
185
+ function cloudflareRunStatus(status) {
186
+ switch (status) {
187
+ case "queued": return "pending";
188
+ case "running": return "running";
189
+ case "waiting":
190
+ case "paused":
191
+ case "waitingForPause": return "waiting";
192
+ case "complete": return "completed";
193
+ case "errored": return "failed";
194
+ case "terminated": return "canceled";
195
+ case "unknown": throw new TypeError("Cloudflare Workflow returned an unknown instance status");
196
+ }
197
+ }
198
+ async function resolveInstanceId(workflowName, workflowVersion, options) {
199
+ if (options?.id !== void 0) {
200
+ validateInstanceId(options.id);
201
+ return options.id;
202
+ }
203
+ if (options?.idempotencyKey === void 0) return;
204
+ const input = new TextEncoder().encode(`${workflowName}\0${workflowVersion ?? ""}\0${options.idempotencyKey}`);
205
+ const id = `temelj-${[...new Uint8Array(await crypto.subtle.digest("SHA-256", input))].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
206
+ validateInstanceId(id);
207
+ return id;
208
+ }
209
+ function validateInstanceId(id) {
210
+ if (id.length > 100 || !CLOUDFLARE_INSTANCE_ID_PATTERN.test(id)) throw new TypeError("Cloudflare Workflow instance id must be at most 100 characters and contain only letters, digits, underscores, and hyphens");
211
+ }
212
+ function rejectUnsupportedStartOptions(options) {
213
+ if (options?.availableAt !== void 0 || options?.deadlineAt !== void 0 || options?.context !== void 0 || options?.parent !== void 0 || options?.workflowVersionResolver !== void 0) WorkflowCapabilityError.unsupported("Cloudflare start context, parent, delayed availability, deadline, or version resolver");
214
+ }
215
+ function durationMilliseconds(duration, label) {
216
+ const milliseconds = durationTotal(duration);
217
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) throw new RangeError(`${label} must resolve to non-negative whole milliseconds`);
218
+ return milliseconds;
219
+ }
220
+ function unsupportedApi(capability) {
221
+ return new Proxy({}, { get() {
222
+ return () => WorkflowCapabilityError.unsupported(capability);
223
+ } });
224
+ }
225
+ //#endregion
226
+ export { createCloudflareWorkflowRuntime };
@@ -0,0 +1,22 @@
1
+ import { WorkflowContext } from "../types/definition.mjs";
2
+ import { WorkflowStepApi } from "../types/step.mjs";
3
+ import { CloudflareNonRetryableErrorConstructor, CloudflareWorkflowStep } from "./types.mjs";
4
+ import { Logger } from "@temelj/log";
5
+ //#region src/cloudflare/step.d.ts
6
+ interface CreateCloudflareWorkflowStepApiOptions {
7
+ readonly step: CloudflareWorkflowStep;
8
+ readonly logger?: Logger;
9
+ readonly nonRetryableError?: CloudflareNonRetryableErrorConstructor;
10
+ }
11
+ /** Creates the Temelj durable command API backed by one Cloudflare Workflow step object. */
12
+ declare function createCloudflareWorkflowStepApi(options: CreateCloudflareWorkflowStepApiOptions): WorkflowStepApi;
13
+ interface CreateCloudflareWorkflowContextOptions<TInput> extends CreateCloudflareWorkflowStepApiOptions {
14
+ readonly input: TInput;
15
+ readonly runId: string;
16
+ readonly workflowName: string;
17
+ readonly workflowVersion?: string;
18
+ readonly createdAt: Temporal.Instant;
19
+ }
20
+ declare function createCloudflareWorkflowContext<TInput>(options: CreateCloudflareWorkflowContextOptions<TInput>): WorkflowContext<TInput>;
21
+ //#endregion
22
+ export { CreateCloudflareWorkflowContextOptions, CreateCloudflareWorkflowStepApiOptions, createCloudflareWorkflowContext, createCloudflareWorkflowStepApi };
@@ -0,0 +1,160 @@
1
+ import { WorkflowNonRetryableError } from "../errors/failures.mjs";
2
+ import { durationTotal } from "../utility.mjs";
3
+ import { WorkflowCapabilityError } from "../errors/capability.mjs";
4
+ import "../errors/mod.mjs";
5
+ import { resolveWaitForMessageInput } from "../message-options.mjs";
6
+ import { resolveWorkflowRetryConfig } from "../engine/retry.mjs";
7
+ import { decodeCloudflareWorkflowValue, encodeCloudflareWorkflowValue } from "./codec.mjs";
8
+ import { makeCloudflareWorkflowEventType, makeCloudflareWorkflowStepName } from "./name.mjs";
9
+ import { validateStandardSchema } from "@temelj/standard-schema";
10
+ import { createNoopLogger } from "@temelj/log";
11
+ //#region src/cloudflare/step.ts
12
+ const DEFAULT_STEP_RETRY = {
13
+ maximumAttempts: 10,
14
+ backoffCoefficient: 2
15
+ };
16
+ const MAXIMUM_CLOUDFLARE_ATTEMPTS = 1e4;
17
+ const MAXIMUM_CLOUDFLARE_DELAY_MILLISECONDS = 31536e6;
18
+ const MAXIMUM_CLOUDFLARE_STEP_TIMEOUT_MILLISECONDS = 18e5;
19
+ const MAXIMUM_EVENT_WAIT_MILLISECONDS = 31536e6;
20
+ /** Creates the Temelj durable command API backed by one Cloudflare Workflow step object. */
21
+ function createCloudflareWorkflowStepApi(options) {
22
+ const logger = options.logger ?? createNoopLogger();
23
+ const runTask = async (config, callback) => {
24
+ const commandName = config.commandId ?? config.name;
25
+ if (commandName === void 0) throw new TypeError("Cloudflare durable task requires a name or commandId");
26
+ const retry = resolveWorkflowRetryConfig(config.retry, DEFAULT_STEP_RETRY, "Cloudflare Workflow step retry");
27
+ const stepName = makeCloudflareWorkflowStepName(commandName);
28
+ const encoded = await options.step.do(stepName, cloudflareStepConfig(config, retry), async (cloudflareContext) => {
29
+ const context = workflowStepContext(commandName, retry, cloudflareContext, logger);
30
+ try {
31
+ return encodeCloudflareWorkflowValue(await callback(context));
32
+ } catch (error) {
33
+ if (error instanceof WorkflowNonRetryableError && options.nonRetryableError !== void 0) throw new options.nonRetryableError(error.message, error.name);
34
+ throw error;
35
+ }
36
+ });
37
+ return decodeCloudflareWorkflowValue(encoded, `Cloudflare step ${commandName} result`);
38
+ };
39
+ const deterministic = {
40
+ now: async (commandId = "now") => await runTask({ commandId: `deterministic:${commandId}` }, () => Temporal.Now.instant()),
41
+ random: async (commandId = "random") => await runTask({ commandId: `deterministic:${commandId}` }, () => Math.random()),
42
+ recordedId: async (commandId = "id") => await runTask({ commandId: `deterministic:${commandId}` }, () => crypto.randomUUID()),
43
+ id: async (commandId = "id") => await runTask({ commandId: `deterministic:${commandId}` }, () => crypto.randomUUID()),
44
+ uuid: async (commandId = "uuid") => await runTask({ commandId: `deterministic:${commandId}` }, () => crypto.randomUUID()),
45
+ bytes: async (commandId, length) => await runTask({ commandId: `deterministic:${commandId}` }, () => {
46
+ const bytes = new Uint8Array(length);
47
+ for (let offset = 0; offset < bytes.byteLength; offset += 65536) crypto.getRandomValues(bytes.subarray(offset, offset + 65536));
48
+ return bytes;
49
+ })
50
+ };
51
+ const waitMessage = (async (messageInput, channelOptions) => {
52
+ const wait = resolveWaitForMessageInput(messageInput, channelOptions);
53
+ const timeout = wait.timeout === void 0 ? MAXIMUM_EVENT_WAIT_MILLISECONDS : durationMilliseconds(wait.timeout, "Cloudflare Workflow message timeout");
54
+ if (timeout < 1e3 || timeout > MAXIMUM_EVENT_WAIT_MILLISECONDS) throw new RangeError("Cloudflare Workflow message timeout must be between 1 second and 365 days");
55
+ const event = await options.step.waitForEvent(makeCloudflareWorkflowStepName(wait.commandId ?? wait.name ?? wait.messageId), {
56
+ type: makeCloudflareWorkflowEventType(wait.messageId),
57
+ timeout
58
+ });
59
+ const decoded = decodeCloudflareWorkflowValue(event.payload, `Cloudflare message ${wait.messageId} payload`);
60
+ if (wait.schema === void 0) return decoded;
61
+ return await validateStandardSchema(wait.schema, decoded, "Workflow message validation failed");
62
+ });
63
+ return {
64
+ log: logger,
65
+ task: {
66
+ run: runTask,
67
+ async call(definition, ...args) {
68
+ return await runTask(definition.config, async (context) => {
69
+ return await definition.handler(...args, context);
70
+ });
71
+ },
72
+ async sleep(commandId, duration) {
73
+ await options.step.sleep(makeCloudflareWorkflowStepName(commandId), durationMilliseconds(duration, "Cloudflare Workflow sleep"));
74
+ },
75
+ async sleepUntil(commandId, timestamp) {
76
+ await options.step.sleepUntil(makeCloudflareWorkflowStepName(commandId), timestamp.epochMilliseconds);
77
+ }
78
+ },
79
+ workflow: unsupportedFacet("child workflows"),
80
+ message: {
81
+ send: async () => WorkflowCapabilityError.unsupported("Cloudflare in-workflow message send"),
82
+ wait: waitMessage
83
+ },
84
+ lock: unsupportedFacet("Cloudflare workflow locks"),
85
+ runData: unsupportedFacet("Cloudflare workflow run data"),
86
+ deterministic,
87
+ stream: unsupportedFacet("Cloudflare workflow streams"),
88
+ hook: unsupportedFacet("Cloudflare workflow hooks")
89
+ };
90
+ }
91
+ function createCloudflareWorkflowContext(options) {
92
+ const logger = options.logger ?? createNoopLogger();
93
+ const step = createCloudflareWorkflowStepApi({
94
+ ...options,
95
+ logger
96
+ });
97
+ return {
98
+ input: options.input,
99
+ step,
100
+ deterministic: step.deterministic,
101
+ log: logger,
102
+ run: {
103
+ id: options.runId,
104
+ workflowName: options.workflowName,
105
+ ...options.workflowVersion === void 0 ? {} : { workflowVersion: options.workflowVersion },
106
+ createdAt: options.createdAt
107
+ },
108
+ signal: new AbortController().signal,
109
+ version: options.workflowVersion
110
+ };
111
+ }
112
+ function cloudflareStepConfig(config, retry) {
113
+ if (retry.maximumAttempts === 0 || retry.maximumAttempts > MAXIMUM_CLOUDFLARE_ATTEMPTS) throw new RangeError(`Cloudflare Workflow steps support at most ${MAXIMUM_CLOUDFLARE_ATTEMPTS} attempts`);
114
+ const initialDelay = retry.initialInterval === void 0 ? 0 : durationMilliseconds(retry.initialInterval, "Cloudflare Workflow retry interval");
115
+ const maximumDelay = retry.maximumInterval === void 0 ? void 0 : durationMilliseconds(retry.maximumInterval, "Cloudflare Workflow maximum retry interval");
116
+ assertMaximumDuration(initialDelay, MAXIMUM_CLOUDFLARE_DELAY_MILLISECONDS, "Cloudflare Workflow retry interval");
117
+ if (maximumDelay !== void 0) assertMaximumDuration(maximumDelay, MAXIMUM_CLOUDFLARE_DELAY_MILLISECONDS, "Cloudflare Workflow maximum retry interval");
118
+ const timeout = config.timeout === void 0 ? void 0 : durationMilliseconds(config.timeout, "Cloudflare Workflow step timeout");
119
+ if (timeout !== void 0) assertMaximumDuration(timeout, MAXIMUM_CLOUDFLARE_STEP_TIMEOUT_MILLISECONDS, "Cloudflare Workflow step timeout");
120
+ return {
121
+ retries: {
122
+ limit: retry.maximumAttempts,
123
+ delay: ({ ctx }) => retryDelay(initialDelay, retry, ctx.attempt, maximumDelay)
124
+ },
125
+ ...timeout === void 0 ? {} : { timeout }
126
+ };
127
+ }
128
+ function retryDelay(initialDelay, retry, attempt, maximumDelay) {
129
+ const delay = initialDelay * retry.backoffCoefficient ** Math.max(0, attempt - 1);
130
+ return Math.min(delay, maximumDelay ?? MAXIMUM_CLOUDFLARE_DELAY_MILLISECONDS);
131
+ }
132
+ function workflowStepContext(commandName, retry, context, logger) {
133
+ return {
134
+ step: {
135
+ id: `${commandName}:${context.step.count}`,
136
+ name: commandName,
137
+ count: context.step.count,
138
+ kind: "run"
139
+ },
140
+ attempt: context.attempt,
141
+ config: retry,
142
+ signal: new AbortController().signal,
143
+ log: logger
144
+ };
145
+ }
146
+ function durationMilliseconds(duration, label) {
147
+ const milliseconds = durationTotal(duration);
148
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0) throw new RangeError(`${label} must resolve to non-negative whole milliseconds`);
149
+ return milliseconds;
150
+ }
151
+ function assertMaximumDuration(value, maximum, label) {
152
+ if (value > maximum) throw new RangeError(`${label} must not exceed ${maximum} milliseconds`);
153
+ }
154
+ function unsupportedFacet(capability) {
155
+ return new Proxy({}, { get() {
156
+ return () => WorkflowCapabilityError.unsupported(capability);
157
+ } });
158
+ }
159
+ //#endregion
160
+ export { createCloudflareWorkflowContext, createCloudflareWorkflowStepApi };
@@ -0,0 +1,87 @@
1
+ //#region src/cloudflare/types.d.ts
2
+ /** Minimal Cloudflare Workflow binding used by the Temelj adapter. */
3
+ interface CloudflareWorkflowBinding {
4
+ create(options?: CloudflareWorkflowInstanceCreateOptions): Promise<CloudflareWorkflowInstance>;
5
+ createBatch(items: readonly CloudflareWorkflowInstanceCreateOptions[]): Promise<readonly CloudflareWorkflowInstance[]>;
6
+ get(id: string): Promise<CloudflareWorkflowInstance>;
7
+ }
8
+ /** Parameters accepted when creating a Cloudflare Workflow instance. */
9
+ interface CloudflareWorkflowInstanceCreateOptions {
10
+ readonly id?: string;
11
+ readonly params?: unknown;
12
+ }
13
+ /** Minimal Cloudflare Workflow instance used by the Temelj adapter. */
14
+ interface CloudflareWorkflowInstance {
15
+ readonly id: string;
16
+ status(): Promise<CloudflareWorkflowInstanceStatus>;
17
+ terminate(options?: {
18
+ readonly rollback?: boolean;
19
+ }): Promise<void>;
20
+ sendEvent(options: {
21
+ readonly type: string;
22
+ readonly payload?: unknown;
23
+ }): Promise<void>;
24
+ }
25
+ /** Status returned by a Cloudflare Workflow instance binding. */
26
+ interface CloudflareWorkflowInstanceStatus {
27
+ readonly status: "queued" | "running" | "paused" | "errored" | "terminated" | "complete" | "waiting" | "waitingForPause" | "unknown";
28
+ readonly error?: {
29
+ readonly name: string;
30
+ readonly message: string;
31
+ };
32
+ readonly output?: unknown;
33
+ }
34
+ /** Minimal Cloudflare durable step context. */
35
+ interface CloudflareWorkflowStepContext {
36
+ readonly step: {
37
+ readonly name: string;
38
+ readonly count: number;
39
+ };
40
+ readonly attempt: number;
41
+ readonly config: CloudflareWorkflowStepConfig;
42
+ }
43
+ /** Cloudflare retry and timeout configuration used by a durable step. */
44
+ interface CloudflareWorkflowStepConfig {
45
+ readonly retries?: {
46
+ readonly limit: number;
47
+ readonly delay: number | ((input: {
48
+ readonly ctx: CloudflareWorkflowStepContext;
49
+ readonly error: Error;
50
+ }) => number | Promise<number>);
51
+ readonly backoff?: "constant" | "linear" | "exponential";
52
+ };
53
+ readonly timeout?: number;
54
+ }
55
+ /** Event returned by Cloudflare after a matching `waitForEvent` call. */
56
+ interface CloudflareWorkflowStepEvent<TPayload = unknown> {
57
+ readonly payload: TPayload;
58
+ readonly timestamp: Date;
59
+ readonly type: string;
60
+ }
61
+ /** Minimal Cloudflare durable step API used by the Temelj adapter. */
62
+ interface CloudflareWorkflowStep {
63
+ do<T>(name: string, config: CloudflareWorkflowStepConfig, callback: (context: CloudflareWorkflowStepContext) => T | Promise<T>): Promise<T>;
64
+ sleep(name: string, duration: number): Promise<void>;
65
+ sleepUntil(name: string, timestamp: number): Promise<void>;
66
+ waitForEvent<T>(name: string, options: {
67
+ readonly type: string;
68
+ readonly timeout: string | number;
69
+ }): Promise<CloudflareWorkflowStepEvent<T>>;
70
+ }
71
+ /** Event delivered to the generic Temelj Cloudflare entrypoint. */
72
+ interface CloudflareWorkflowEvent<TPayload = unknown> {
73
+ readonly payload: TPayload;
74
+ readonly timestamp: Date;
75
+ readonly instanceId: string;
76
+ readonly workflowName: string;
77
+ readonly schedule?: {
78
+ readonly cron: string;
79
+ readonly scheduledTime: number;
80
+ };
81
+ }
82
+ /** Constructor used to convert Temelj non-retryable errors for Cloudflare. */
83
+ interface CloudflareNonRetryableErrorConstructor {
84
+ new (message: string, name?: string): Error;
85
+ }
86
+ //#endregion
87
+ export { CloudflareNonRetryableErrorConstructor, CloudflareWorkflowBinding, CloudflareWorkflowEvent, CloudflareWorkflowInstance, CloudflareWorkflowInstanceCreateOptions, CloudflareWorkflowInstanceStatus, CloudflareWorkflowStep, CloudflareWorkflowStepConfig, CloudflareWorkflowStepContext, CloudflareWorkflowStepEvent };
@@ -0,0 +1 @@
1
+ export {};
@@ -42,7 +42,8 @@ function createWorkflowEngineInspectionApi(store, now) {
42
42
  });
43
43
  },
44
44
  async getTimeline(runId) {
45
- return createTimeline(await getRequiredRun(store, runId), await store.getEvents(runId));
45
+ const run = await getRequiredRun(store, runId);
46
+ return createTimeline(run, await store.getEvents(runId));
46
47
  },
47
48
  async listStepAttempts(runId, options) {
48
49
  await getRequiredRun(store, runId);
@@ -81,22 +81,24 @@ async function getWorkflowRunSummary(options) {
81
81
  const { store, now, filters } = options;
82
82
  const cleanupCutoff = workflowSummaryCleanupCutoff(filters);
83
83
  const runs = await store.listRuns(filters);
84
- return summarizeWorkflowRuns({
84
+ const summaryInputs = {
85
85
  runs,
86
86
  now,
87
87
  cleanupCutoff,
88
88
  ...await getWorkflowRunSummaryActivityInputs(store, runs)
89
- });
89
+ };
90
+ return summarizeWorkflowRuns(summaryInputs);
90
91
  }
91
92
  async function getWorkflowRecoverySummary(options) {
92
93
  const { store, now, filters } = options;
93
94
  const runs = await store.listRuns(filters);
95
+ const activeRuns = runs.filter((run) => {
96
+ return workflowRunCanHaveActiveWork(run);
97
+ });
94
98
  return summarizeWorkflowRecovery({
95
99
  runs,
96
100
  now,
97
- inputs: await getWorkflowRecoverySummaryInputs(store, runs.filter((run) => {
98
- return workflowRunCanHaveActiveWork(run);
99
- }))
101
+ inputs: await getWorkflowRecoverySummaryInputs(store, activeRuns)
100
102
  });
101
103
  }
102
104
  async function getWorkflowRecoverySummaryInputs(store, activeRuns) {
@@ -60,18 +60,20 @@ function createWorkflowStepMessageApi(environment) {
60
60
  payload: toOptionalPersistedValue(options.payload, "message payload", limits),
61
61
  idempotencyKey
62
62
  };
63
- if (started === void 0) await appendDurableStepStartedEvent(environment, {
64
- kind: "message_send_started",
65
- timestamp: now(),
66
- stepId: identity.id,
67
- stepName: identity.name,
68
- count: identity.count,
69
- targetRunId: nextMessageSend.targetRunId,
70
- messageId: nextMessageSend.messageId,
71
- payload: nextMessageSend.payload,
72
- ...nextMessageSend.idempotencyKey === void 0 ? {} : { idempotencyKey: nextMessageSend.idempotencyKey }
73
- });
74
- else requireMessageSendReplayMatches(identity, started, started, nextMessageSend);
63
+ if (started === void 0) {
64
+ const timestamp = now();
65
+ await appendDurableStepStartedEvent(environment, {
66
+ kind: "message_send_started",
67
+ timestamp,
68
+ stepId: identity.id,
69
+ stepName: identity.name,
70
+ count: identity.count,
71
+ targetRunId: nextMessageSend.targetRunId,
72
+ messageId: nextMessageSend.messageId,
73
+ payload: nextMessageSend.payload,
74
+ ...nextMessageSend.idempotencyKey === void 0 ? {} : { idempotencyKey: nextMessageSend.idempotencyKey }
75
+ });
76
+ } else requireMessageSendReplayMatches(identity, started, started, nextMessageSend);
75
77
  try {
76
78
  await sendWorkflowMessage(store, targetRunId, {
77
79
  ...options,
@@ -338,7 +340,8 @@ function messageWaitAvailableAt(now, timeoutAt, deadlineAt) {
338
340
  async function wakeRunForMessageEvent(store, runId, messageEvent) {
339
341
  const run = await store.getRun(runId);
340
342
  if (run === void 0 || run.status !== "waiting") return;
341
- const activeWaits = new WorkflowHistory(await store.getEvents(runId)).activeMessageWaitStepIds(messageEvent.messageId);
343
+ const events = await store.getEvents(runId);
344
+ const activeWaits = new WorkflowHistory(events).activeMessageWaitStepIds(messageEvent.messageId);
342
345
  if (!(messageEvent.waiterStepIds === void 0 ? activeWaits.length > 0 : activeWaits.some((stepId) => messageEvent.waiterStepIds?.includes(stepId)))) return;
343
346
  await store.updateRunIfCurrent(run, {
344
347
  ...run,
@@ -181,9 +181,7 @@ function emitRunEvent(emit, run, previousRun) {
181
181
  case "deadline":
182
182
  emit("workflow:run-failed", payload);
183
183
  break;
184
- case "canceled":
185
- emit("workflow:run-canceled", payload);
186
- break;
184
+ case "canceled": emit("workflow:run-canceled", payload);
187
185
  }
188
186
  }
189
187
  function makeRunObservationEvent(run, previousRun) {
@@ -52,7 +52,8 @@ async function setWorkflowRunAttributes(environment, commandId, attributes) {
52
52
  const recorded = history.attributesSet(identity.id);
53
53
  if (recorded !== void 0) {
54
54
  requireAttributesReplayMatches(identity, recorded, normalized);
55
- const updated = await patchCurrentExecutionRunAttributes(store, executionOwner, workflowAttributePatchFromEvent(recorded), recorded.timestamp);
55
+ const recordedPatch = workflowAttributePatchFromEvent(recorded);
56
+ const updated = await patchCurrentExecutionRunAttributes(store, executionOwner, recordedPatch, recorded.timestamp);
56
57
  if (updated === void 0) WorkflowStaleExecutionError.stale(executionOwner.id);
57
58
  Object.assign(executionOwner, updated);
58
59
  return;
@@ -20,7 +20,8 @@ async function rerunWorkflowRun(service, startWorkflow, runId, rerunOptions) {
20
20
  }
21
21
  async function resolveRerunWorkflowDefinition(sourceRun, options) {
22
22
  const workflowName = options?.workflowName ?? sourceRun.workflowName;
23
- return defineWorkflowFromName(workflowName, await resolveWorkflowVersion(workflowName, options !== void 0 && "workflowVersion" in options ? options.workflowVersion : sourceRun.workflowVersion, options?.workflowVersionResolver));
23
+ const resolvedWorkflowVersion = await resolveWorkflowVersion(workflowName, options !== void 0 && "workflowVersion" in options ? options.workflowVersion : sourceRun.workflowVersion, options?.workflowVersionResolver);
24
+ return defineWorkflowFromName(workflowName, resolvedWorkflowVersion);
24
25
  }
25
26
  async function rescheduleWorkflowRun(service, lifecycle, runId, rescheduleOptions) {
26
27
  const options = rescheduleOptions;
@@ -28,7 +28,8 @@ function resolveScheduleNextOptions(options, now) {
28
28
  function createWorkflowEngineStartApi(service) {
29
29
  return {
30
30
  async startWorkflow(workflow, input, startOptions) {
31
- const resolvedDefinition = await resolveWorkflowStartSpec(defineWorkflowFromTarget(workflow), startOptions);
31
+ const definition = defineWorkflowFromTarget(workflow);
32
+ const resolvedDefinition = await resolveWorkflowStartSpec(definition, startOptions);
32
33
  const parsedInput = await parseWorkflowInput(resolvedDefinition, input);
33
34
  const idempotencyKey = startOptions?.idempotencyKey;
34
35
  const explicitRunId = startOptions?.id;
@@ -376,7 +376,8 @@ async function parentHasActiveChildWorkflowAttemptForRun(store, parentRunId, chi
376
376
  }
377
377
  async function parentHasActiveChildWorkflowEventForAttempt(store, parentRunId, attempt) {
378
378
  if (attempt.childRunId === void 0) return false;
379
- const lifecycle = new WorkflowHistory(await store.getEvents(parentRunId)).childWorkflowLifecycleForAttempt(attempt);
379
+ const events = await store.getEvents(parentRunId);
380
+ const lifecycle = new WorkflowHistory(events).childWorkflowLifecycleForAttempt(attempt);
380
381
  if (lifecycle.started === void 0) return false;
381
382
  if (!isPositiveSafeInteger(lifecycle.started.attempt)) return false;
382
383
  return !lifecycle.hasTerminalAfterStarted;
@@ -6,6 +6,7 @@ import { WorkflowStepIdentity } from "../types/run.mjs";
6
6
  import { EventRecord } from "../types/events.mjs";
7
7
  import { WorkflowEventHistoryWatcher, WorkflowEventReader, WorkflowRunReaderRepository } from "../types/store.mjs";
8
8
  import { WorkflowExecutionEnvironment } from "../types/engine.mjs";
9
+ import "temporal-polyfill/global";
9
10
  //#region src/engine/stream.d.ts
10
11
  interface WorkflowStreamReader<TChunk> {
11
12
  (options?: ReadStreamOptions): Promise<StreamState<TChunk>>;