@lunora/workflow 1.0.0-alpha.7 → 1.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/do/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { WorkflowEntrypoint } from 'cloudflare:workers';
2
2
  import { NonRetryableError } from 'cloudflare:workflows';
3
3
  import { convertNonRetryableError } from '../packem_shared/NonRetryableError-Dn2dTyBS.mjs';
4
- import { stripBranchMarker, signalBranchParent, extractBranchMarker, errorOutcome, okOutcome } from '../packem_shared/MAX_BRANCHES-D6cQabJ1.mjs';
5
- import { createWorkflowRunContext } from '../packem_shared/createWorkflowRunContext-C09mSbgk.mjs';
4
+ import { extractBranchMarker, stripBranchMarker, signalBranchParentSafe, errorOutcome, okOutcome } from '../packem_shared/MAX_BRANCHES-EO5xYS_3.mjs';
5
+ import { createWorkflowRunContext } from '../packem_shared/createWorkflowRunContext-BDSHE8iO.mjs';
6
6
 
7
7
  class LunoraWorkflow extends WorkflowEntrypoint {
8
8
  /** The `lunora/workflows.ts` export name, for log correlation. */
@@ -30,12 +30,12 @@ class LunoraWorkflow extends WorkflowEntrypoint {
30
30
  output = await this.#definition.handler(context);
31
31
  } catch (error) {
32
32
  if (marker) {
33
- await signalBranchParent({ env: this.env, step: nativeStep }, marker, errorOutcome(error));
33
+ await signalBranchParentSafe({ env: this.env, log: context.log, step: nativeStep }, marker, errorOutcome(error));
34
34
  }
35
35
  return convertNonRetryableError(error, NonRetryableError);
36
36
  }
37
37
  if (marker) {
38
- await signalBranchParent({ env: this.env, step: nativeStep }, marker, okOutcome(output));
38
+ await signalBranchParentSafe({ env: this.env, log: context.log, step: nativeStep }, marker, okOutcome(output));
39
39
  }
40
40
  return output;
41
41
  }
package/dist/index.mjs CHANGED
@@ -1,9 +1,9 @@
1
- export { createWorkflowContext } from './packem_shared/createWorkflowContext-DfSgBq5I.mjs';
2
- export { default as createWorkflows } from './packem_shared/createWorkflows-BC1Mwjwe.mjs';
1
+ export { createWorkflowContext } from './packem_shared/createWorkflowContext-oxZ5zj3x.mjs';
2
+ export { default as createWorkflows } from './packem_shared/createWorkflows-wq4fs72q.mjs';
3
3
  export { defineStep, isStepDefinition } from './packem_shared/defineStep-DJQtLw7g.mjs';
4
4
  export { defineWorkflow, isWorkflowDefinition, workflowBindingName, workflowClassName, workflowDefaultName } from './packem_shared/defineWorkflow-DbUC-oCN.mjs';
5
5
  export { NonRetryableError, convertNonRetryableError, isNonRetryableError, toNativeNonRetryableError } from './packem_shared/NonRetryableError-Dn2dTyBS.mjs';
6
- export { MAX_BRANCHES, branch } from './packem_shared/MAX_BRANCHES-D6cQabJ1.mjs';
6
+ export { MAX_BRANCHES, branch } from './packem_shared/MAX_BRANCHES-EO5xYS_3.mjs';
7
7
  export { WorkflowsRestError, createWorkflowsRestClient } from './packem_shared/WorkflowsRestError-zmjOxTR1.mjs';
8
- export { createWorkflowRunContext } from './packem_shared/createWorkflowRunContext-C09mSbgk.mjs';
8
+ export { createWorkflowRunContext } from './packem_shared/createWorkflowRunContext-BDSHE8iO.mjs';
9
9
  export { createRunStep, validateStepArgs } from './packem_shared/createRunStep-8jOXxP2o.mjs';
@@ -1,3 +1,4 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import { NonRetryableError } from './NonRetryableError-Dn2dTyBS.mjs';
2
3
 
3
4
  const MAX_BRANCHES = 100;
@@ -29,17 +30,25 @@ const compensateCompleted = async (deps, completed, error) => {
29
30
  if (done === void 0 || compensateWith === void 0) {
30
31
  continue;
31
32
  }
32
- await deps.step.do(`${COMPENSATE_STEP_PREFIX}${done.plan.childId}`, async () => {
33
- const compensateId = `${done.plan.childId}:compensate`;
34
- const compensationParams = {
35
- branch: done.plan.item.workflow,
36
- error,
37
- index: done.plan.index,
38
- output: done.output
39
- };
40
- await deps.resolveBinding(compensateWith).create({ id: compensateId, params: compensationParams });
41
- return compensateId;
42
- });
33
+ try {
34
+ const compensation = deps.resolveBinding(compensateWith);
35
+ await deps.step.do(`${COMPENSATE_STEP_PREFIX}${done.plan.childId}`, async () => {
36
+ const compensateId = `${done.plan.childId}:compensate`;
37
+ const compensationParams = {
38
+ branch: done.plan.item.workflow,
39
+ error,
40
+ index: done.plan.index,
41
+ output: done.output
42
+ };
43
+ await compensation.create({ id: compensateId, params: compensationParams });
44
+ return compensateId;
45
+ });
46
+ } catch (compensationError) {
47
+ deps.log?.error(
48
+ `ctx.parallel: group-saga compensation "${compensateWith}" for branch "${done.plan.item.workflow}" (#${String(done.plan.index)}) failed`,
49
+ compensationError
50
+ );
51
+ }
43
52
  }
44
53
  };
45
54
  const createParallel = (deps) => {
@@ -56,6 +65,15 @@ const createParallel = (deps) => {
56
65
  const childId = deps.nextChildId(item.id);
57
66
  return { childId, eventType: `${BRANCH_EVENT_PREFIX}${childId}`, index, item };
58
67
  });
68
+ const seenIds = /* @__PURE__ */ new Set();
69
+ for (const plan of planned) {
70
+ if (seenIds.has(plan.childId)) {
71
+ throw new NonRetryableError(
72
+ `ctx.parallel: duplicate branch id "${plan.childId}" — each branch in a group must resolve to a unique child instance id (check explicit \`id\` options)`
73
+ );
74
+ }
75
+ seenIds.add(plan.childId);
76
+ }
59
77
  await Promise.all(
60
78
  planned.map(
61
79
  (plan) => deps.step.do(`${SPAWN_STEP_PREFIX}${plan.childId}`, async () => {
@@ -69,11 +87,18 @@ const createParallel = (deps) => {
69
87
  const results = [];
70
88
  const completed = [];
71
89
  for (const plan of planned) {
72
- const event = await deps.step.waitForEvent(`${AWAIT_STEP_PREFIX}${plan.childId}`, {
73
- timeout: plan.item.timeout,
74
- type: plan.eventType
75
- });
76
- const outcome = event.payload;
90
+ let outcome;
91
+ try {
92
+ const event = await deps.step.waitForEvent(`${AWAIT_STEP_PREFIX}${plan.childId}`, {
93
+ timeout: plan.item.timeout,
94
+ type: plan.eventType
95
+ });
96
+ outcome = event.payload;
97
+ } catch (joinError) {
98
+ const joinFailure = serializeError(joinError);
99
+ await compensateCompleted(deps, completed, joinFailure);
100
+ throw new NonRetryableError(`ctx.parallel: branch "${plan.item.workflow}" (#${String(plan.index)}) join failed: ${joinFailure.message}`);
101
+ }
77
102
  if (outcome.status === "error") {
78
103
  await compensateCompleted(deps, completed, outcome.error);
79
104
  throw new NonRetryableError(`ctx.parallel: branch "${plan.item.workflow}" (#${String(plan.index)}) failed: ${outcome.error.message}`);
@@ -86,6 +111,9 @@ const createParallel = (deps) => {
86
111
  return run;
87
112
  };
88
113
  const createSpawn = (deps) => async (workflow, params, options) => {
114
+ if (params !== void 0 && Object.hasOwn(params, BRANCH_MARKER_KEY)) {
115
+ throw new LunoraError("BAD_REQUEST", `@lunora/workflow: params may not contain the reserved key "${BRANCH_MARKER_KEY}"`);
116
+ }
89
117
  const childId = deps.nextChildId(options?.id);
90
118
  await deps.step.do(`${SPAWN_STEP_PREFIX}${childId}`, async () => {
91
119
  const binding = deps.resolveBinding(workflow);
@@ -106,6 +134,9 @@ const extractBranchMarker = (payload) => {
106
134
  if (typeof candidate.eventType !== "string" || typeof candidate.parentBinding !== "string" || typeof candidate.parentId !== "string" || typeof candidate.index !== "number") {
107
135
  return void 0;
108
136
  }
137
+ if (!candidate.parentBinding.startsWith("WORKFLOW_") || !candidate.eventType.startsWith(BRANCH_EVENT_PREFIX)) {
138
+ return void 0;
139
+ }
109
140
  return { eventType: candidate.eventType, index: candidate.index, parentBinding: candidate.parentBinding, parentId: candidate.parentId };
110
141
  };
111
142
  const stripBranchMarker = (payload) => {
@@ -128,5 +159,12 @@ const signalBranchParent = async (deps, marker, outcome) => {
128
159
  return marker.eventType;
129
160
  });
130
161
  };
162
+ const signalBranchParentSafe = async (deps, marker, outcome) => {
163
+ try {
164
+ await signalBranchParent(deps, marker, outcome);
165
+ } catch (signalError) {
166
+ deps.log?.error(`@lunora/workflow: failed to signal branch parent "${marker.parentId}" (event "${marker.eventType}")`, signalError);
167
+ }
168
+ };
131
169
 
132
- export { BRANCH_MARKER_KEY, MAX_BRANCHES, branch, createParallel, createSpawn, errorOutcome, extractBranchMarker, okOutcome, signalBranchParent, stripBranchMarker };
170
+ export { BRANCH_MARKER_KEY, MAX_BRANCHES, branch, createParallel, createSpawn, errorOutcome, extractBranchMarker, okOutcome, signalBranchParent, signalBranchParentSafe, stripBranchMarker };
@@ -1,4 +1,4 @@
1
- import createWorkflows from './createWorkflows-BC1Mwjwe.mjs';
1
+ import createWorkflows from './createWorkflows-wq4fs72q.mjs';
2
2
 
3
3
  const createWorkflowContext = (env, specs) => {
4
4
  const bindings = {};
@@ -1,6 +1,6 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
2
  import { workflowBindingName } from './defineWorkflow-DbUC-oCN.mjs';
3
- import { createSpawn, createParallel } from './MAX_BRANCHES-D6cQabJ1.mjs';
3
+ import { createSpawn, createParallel } from './MAX_BRANCHES-EO5xYS_3.mjs';
4
4
  import { createRunStep } from './createRunStep-8jOXxP2o.mjs';
5
5
 
6
6
  const createDispatchLogger = (prefix) => {
@@ -28,6 +28,18 @@ const trimTrailingSlashes = (value) => {
28
28
  }
29
29
  return value.slice(0, end);
30
30
  };
31
+ const toDispatchError = (label, status, rawBody) => {
32
+ try {
33
+ const parsed = JSON.parse(rawBody);
34
+ const errorBody = parsed?.error;
35
+ if (typeof errorBody === "object" && errorBody !== null && typeof errorBody.code === "string") {
36
+ const { code, data, message } = errorBody;
37
+ return new LunoraError(code, typeof message === "string" ? message : void 0, { data, status });
38
+ }
39
+ } catch {
40
+ }
41
+ return new LunoraError("INTERNAL", `${label}: function dispatch failed (${String(status)}): ${rawBody}`, { status });
42
+ };
31
43
  const createDispatchRunner = (options) => {
32
44
  const { label } = options;
33
45
  const globalFetch = globalThis.fetch;
@@ -51,7 +63,7 @@ const createDispatchRunner = (options) => {
51
63
  method: "POST"
52
64
  });
53
65
  if (!response.ok) {
54
- throw new LunoraError("INTERNAL", `${label}: function dispatch failed (${String(response.status)}): ${await response.text()}`);
66
+ throw toDispatchError(label, response.status, await response.text());
55
67
  }
56
68
  const text = await response.text();
57
69
  if (text.length === 0) {
@@ -60,7 +72,9 @@ const createDispatchRunner = (options) => {
60
72
  try {
61
73
  return JSON.parse(text);
62
74
  } catch {
63
- return text;
75
+ throw new LunoraError("INTERNAL", `${label}: function dispatch returned a non-JSON body (${String(response.status)}): ${text}`, {
76
+ status: response.status
77
+ });
64
78
  }
65
79
  };
66
80
  };
@@ -91,6 +105,7 @@ const createWorkflowRunContext = (options) => {
91
105
  const fanOutDeps = {
92
106
  env: options.env,
93
107
  instanceId: options.event.instanceId,
108
+ log,
94
109
  nextChildId,
95
110
  parentBinding: workflowBindingName(options.exportName),
96
111
  resolveBinding,
@@ -1,9 +1,24 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
+ import { BRANCH_MARKER_KEY } from './MAX_BRANCHES-EO5xYS_3.mjs';
2
3
 
4
+ const rejectReservedParams = (options) => {
5
+ const { params } = options ?? {};
6
+ if (params !== void 0 && Object.hasOwn(params, BRANCH_MARKER_KEY)) {
7
+ throw new LunoraError("BAD_REQUEST", `@lunora/workflow: params may not contain the reserved key "${BRANCH_MARKER_KEY}"`);
8
+ }
9
+ };
3
10
  const handleFor = (binding) => {
4
11
  return {
5
- create: async (options) => binding.create(options),
6
- createBatch: async (batch) => binding.createBatch(batch),
12
+ create: async (options) => {
13
+ rejectReservedParams(options);
14
+ return binding.create(options);
15
+ },
16
+ createBatch: async (batch) => {
17
+ for (const options of batch) {
18
+ rejectReservedParams(options);
19
+ }
20
+ return binding.createBatch(batch);
21
+ },
7
22
  get: async (id) => binding.get(id)
8
23
  };
9
24
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/workflow",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.9",
4
4
  "description": "Durable workflows for Lunora: defineWorkflow over Cloudflare Workflows, generated WorkflowEntrypoint classes, and the ctx.workflows surface",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -48,8 +48,8 @@
48
48
  "access": "public"
49
49
  },
50
50
  "dependencies": {
51
- "@lunora/errors": "1.0.0-alpha.2",
52
- "@lunora/values": "1.0.0-alpha.5"
51
+ "@lunora/errors": "1.0.0-alpha.4",
52
+ "@lunora/values": "1.0.0-alpha.7"
53
53
  },
54
54
  "engines": {
55
55
  "node": "^22.15.0 || >=24.11.0"