@blokjs/shared 0.2.2 → 0.4.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.
@@ -1,10 +1,12 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import type Context from "../../../src/types/Context";
3
3
  import mapper from "../../../src/utils/Mapper";
4
+ import { MapperResolutionError } from "../../../src/utils/MapperResolutionError";
4
5
 
5
6
  function createMockContext(overrides: Partial<Context> = {}): Context {
6
7
  return {
7
8
  id: "test-id",
9
+ workflow_name: "test-workflow",
8
10
  request: { body: {}, headers: {}, query: {}, params: {} },
9
11
  response: { data: null, error: null, success: true },
10
12
  error: { message: "" },
@@ -18,107 +20,373 @@ function createMockContext(overrides: Partial<Context> = {}): Context {
18
20
  } as Context;
19
21
  }
20
22
 
23
+ /**
24
+ * Test helper — set the resolution mode for the duration of one test.
25
+ * Resets to the v0.3.x default (`"warn"`) after each case.
26
+ */
27
+ function setMode(mode: "warn" | "strict" | "silent"): void {
28
+ process.env.BLOK_MAPPER_MODE = mode;
29
+ }
30
+
21
31
  describe("Mapper", () => {
22
32
  beforeEach(() => {
23
33
  vi.restoreAllMocks();
34
+ // Reset to the v0.3.x default ("warn"). Assignment to undefined
35
+ // keeps biome's `noDelete` rule happy without changing semantics —
36
+ // `process.env.X = undefined` makes `process.env.X` evaluate to
37
+ // the string `"undefined"` in Node, but Mapper's `readMode()`
38
+ // treats anything that isn't "strict" or "silent" as warn, so
39
+ // the default is preserved.
40
+ process.env.BLOK_MAPPER_MODE = undefined;
24
41
  });
25
42
 
26
- describe("replaceString()", () => {
27
- it("should replace ${key} with data value", () => {
43
+ afterEach(() => {
44
+ process.env.BLOK_MAPPER_MODE = undefined;
45
+ });
46
+
47
+ // =========================================================================
48
+ // Pre-existing behavior — replaceString happy paths (preserved across rewrite)
49
+ // =========================================================================
50
+
51
+ describe("replaceString() — happy paths", () => {
52
+ it("replaces ${key} with data value", () => {
28
53
  const ctx = createMockContext();
29
54
  const data = { name: "John" };
30
55
  const result = mapper.replaceString("Hello ${name}", ctx, data);
31
56
  expect(result).toBe("Hello John");
32
57
  });
33
58
 
34
- it("should replace multiple placeholders", () => {
59
+ it("replaces multiple placeholders", () => {
35
60
  const ctx = createMockContext();
36
61
  const data = { first: "John", last: "Doe" };
37
62
  const result = mapper.replaceString("${first} ${last}", ctx, data);
38
63
  expect(result).toBe("John Doe");
39
64
  });
40
65
 
41
- it("should handle nested data access via lodash.get", () => {
66
+ it("handles nested data access via lodash.get", () => {
42
67
  const ctx = createMockContext();
43
68
  const data = { user: { name: "Alice" } };
44
69
  const result = mapper.replaceString("Hi ${user.name}", ctx, data);
45
70
  expect(result).toBe("Hi Alice");
46
71
  });
47
72
 
48
- it("should handle no matches (no ${})", () => {
73
+ it("handles no matches (no ${})", () => {
49
74
  const ctx = createMockContext();
50
75
  const result = mapper.replaceString("plain text", ctx, {});
51
76
  expect(result).toBe("plain text");
52
77
  });
53
78
 
54
- it("should execute js/ prefix expressions", () => {
79
+ it("executes js/ prefix expressions", () => {
55
80
  const ctx = createMockContext();
56
81
  const result = mapper.replaceString("js/1 + 2", ctx, {});
57
82
  expect(result).toBe(3);
58
83
  });
59
84
 
60
- it("should pass through non-js strings", () => {
85
+ it("passes through non-js strings", () => {
61
86
  const ctx = createMockContext();
62
87
  const result = mapper.replaceString("hello world", ctx, {});
63
88
  expect(result).toBe("hello world");
64
89
  });
90
+ });
91
+
92
+ // =========================================================================
93
+ // Bug fixes shipped with the rewrite (v0.3.x)
94
+ // =========================================================================
95
+
96
+ describe("replaceString() — bug fixes (v0.3.x)", () => {
97
+ it("preserves falsy-but-valid lookup values (was: || fell through to runJs)", () => {
98
+ const ctx = createMockContext();
99
+ // Pre-v0.3.x: `_.get(data, key) || runJs(key)` — when lookup
100
+ // returned 0, the `||` fell through to runJs (which would throw
101
+ // for "count" not being in scope). Now `=== undefined` check
102
+ // preserves the 0.
103
+ expect(mapper.replaceString("${count}", ctx, { count: 0 })).toBe("0");
104
+ expect(mapper.replaceString("${flag}", ctx, { flag: false })).toBe("false");
105
+ expect(mapper.replaceString("${empty}", ctx, { empty: "" })).toBe("");
106
+ });
65
107
 
66
- it("should handle errors silently in placeholder replacement", () => {
67
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
108
+ it("JSON-encodes object values in interpolation (was: '[object Object]')", () => {
68
109
  const ctx = createMockContext();
69
- // An expression that throws inside Function()
70
- const result = mapper.replaceString("${undefinedVar.deep.access}", ctx, {});
71
- // Should not throw, returns string with attempted replacement
72
- expect(result).toBeTypeOf("string");
73
- consoleSpy.mockRestore();
110
+ const data = { user: { id: 1, name: "Alice" } };
111
+ const result = mapper.replaceString("payload=${user}", ctx, data);
112
+ // Pre-v0.3.x: `value as string` "[object Object]". Now JSON.
113
+ expect(result).toBe('payload={"id":1,"name":"Alice"}');
114
+ });
115
+
116
+ it("renders null/undefined interpolation values as empty string", () => {
117
+ const ctx = createMockContext();
118
+ const result = mapper.replaceString("v=${x}", ctx, { x: null });
119
+ expect(result).toBe("v=");
120
+ });
121
+
122
+ it("strips only the `js/` prefix (slice(3) vs replace('js/', ''))", () => {
123
+ const ctx = createMockContext();
124
+ // A fabricated edge case — an expression that contains the
125
+ // substring "js/" later. Pre-v0.3.x's `replace("js/", "")`
126
+ // would strip the wrong occurrence and break the eval.
127
+ // (The expression here evaluates safely; we just check
128
+ // the prefix-stripping doesn't double-strip.)
129
+ const result = mapper.replaceString('js/"prefix:" + "js/inside"', ctx, {});
130
+ expect(result).toBe("prefix:js/inside");
131
+ });
132
+
133
+ it("provides symmetric scope (func + vars) inside ${...} expressions", () => {
134
+ // Pre-v0.3.x: `${func.X}` threw because runJs was called with
135
+ // only 3 args. Now func/vars are bound in both syntaxes for
136
+ // consistency with `js/...`.
137
+ const ctx = createMockContext({ vars: { count: 7 } });
138
+ const result = mapper.replaceString("${vars.count}", ctx, {});
139
+ expect(result).toBe("7");
74
140
  });
75
141
  });
76
142
 
143
+ // =========================================================================
144
+ // Failure modes (BLOK_MAPPER_MODE)
145
+ // =========================================================================
146
+
147
+ describe('mode = "warn" (default) — log + pass-through', () => {
148
+ it("logs an actionable warning via ctx.logger.logLevel", () => {
149
+ const logLevel = vi.fn();
150
+ const ctx = createMockContext({
151
+ logger: { log: vi.fn(), logLevel, error: vi.fn() } as unknown as Context["logger"],
152
+ workflow_name: "wf-X",
153
+ });
154
+ (ctx as Record<string, unknown>)._stepInfo = { name: "step-Y" };
155
+
156
+ const result = mapper.replaceString("js/ctx.req.body.bad.path", ctx, {});
157
+
158
+ // Original literal passes through (back-compat).
159
+ expect(result).toBe("js/ctx.req.body.bad.path");
160
+ // Single warn call with the structured message.
161
+ expect(logLevel).toHaveBeenCalledTimes(1);
162
+ expect(logLevel.mock.calls[0][0]).toBe("warn");
163
+ const message = logLevel.mock.calls[0][1] as string;
164
+ expect(message).toContain('step "step-Y"');
165
+ expect(message).toContain('workflow "wf-X"');
166
+ expect(message).toContain("ctx.req.body.bad.path");
167
+ expect(message).toContain("hint:");
168
+ expect(message).toContain("BLOK_MAPPER_MODE=strict");
169
+ });
170
+
171
+ it("falls back to console.warn when ctx.logger has neither logLevel nor log", () => {
172
+ const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
173
+ // Logger object lacking logLevel + log methods.
174
+ const ctx = createMockContext({
175
+ logger: { error: vi.fn() } as unknown as Context["logger"],
176
+ });
177
+ mapper.replaceString("js/ctx.bad.access", ctx, {});
178
+ expect(consoleWarn).toHaveBeenCalledTimes(1);
179
+ expect(consoleWarn.mock.calls[0][0] as string).toContain("[blok][mapper]");
180
+ });
181
+
182
+ it("returns the literal placeholder for failed ${...} interpolation", () => {
183
+ const ctx = createMockContext({
184
+ logger: { log: vi.fn(), logLevel: vi.fn(), error: vi.fn() } as unknown as Context["logger"],
185
+ });
186
+ const result = mapper.replaceString("hi ${ctx.req.body.bad.path}", ctx, {});
187
+ // Pre-v0.3.x also did this — we preserve the back-compat.
188
+ expect(result).toBe("hi ${ctx.req.body.bad.path}");
189
+ });
190
+ });
191
+
192
+ describe('mode = "strict" — throws MapperResolutionError', () => {
193
+ it("throws on failed js/ expression with full context", () => {
194
+ setMode("strict");
195
+ const ctx = createMockContext({ workflow_name: "wf-strict" });
196
+ (ctx as Record<string, unknown>)._stepInfo = { name: "step-A" };
197
+
198
+ let thrown: unknown = null;
199
+ try {
200
+ mapper.replaceString("js/ctx.req.body.bad.path", ctx, {});
201
+ } catch (e) {
202
+ thrown = e;
203
+ }
204
+ expect(thrown).toBeInstanceOf(MapperResolutionError);
205
+ const err = thrown as MapperResolutionError;
206
+ expect(err.context.expression).toBe("ctx.req.body.bad.path");
207
+ expect(err.context.syntax).toBe("js");
208
+ expect(err.context.workflowName).toBe("wf-strict");
209
+ expect(err.context.stepName).toBe("step-A");
210
+ expect(err.context.cause).toBeInstanceOf(TypeError);
211
+ });
212
+
213
+ it("throws on failed ${...} expression", () => {
214
+ setMode("strict");
215
+ const ctx = createMockContext();
216
+ expect(() => mapper.replaceString("${ctx.req.body.bad.path}", ctx, {})).toThrow(MapperResolutionError);
217
+ });
218
+
219
+ it("does NOT throw when expression resolves successfully", () => {
220
+ setMode("strict");
221
+ const ctx = createMockContext();
222
+ expect(mapper.replaceString("js/1 + 2", ctx, {})).toBe(3);
223
+ expect(mapper.replaceString("${name}", ctx, { name: "ok" })).toBe("ok");
224
+ });
225
+ });
226
+
227
+ describe('mode = "silent" — full suppression (pre-v0.3.x behavior)', () => {
228
+ it("does not log via ctx.logger", () => {
229
+ setMode("silent");
230
+ const logLevel = vi.fn();
231
+ const log = vi.fn();
232
+ const ctx = createMockContext({
233
+ logger: { log, logLevel, error: vi.fn() } as unknown as Context["logger"],
234
+ });
235
+ const result = mapper.replaceString("js/ctx.req.body.bad.path", ctx, {});
236
+ expect(result).toBe("js/ctx.req.body.bad.path");
237
+ expect(logLevel).not.toHaveBeenCalled();
238
+ expect(log).not.toHaveBeenCalled();
239
+ });
240
+
241
+ it("does not log via console.warn", () => {
242
+ setMode("silent");
243
+ const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {});
244
+ const ctx = createMockContext({ logger: undefined as unknown as Context["logger"] });
245
+ mapper.replaceString("js/ctx.bad", ctx, {});
246
+ expect(consoleWarn).not.toHaveBeenCalled();
247
+ });
248
+ });
249
+
250
+ // =========================================================================
251
+ // MapperResolutionError diagnostic content
252
+ // =========================================================================
253
+
254
+ describe("MapperResolutionError — diagnostic message quality", () => {
255
+ it("includes a hint for 'Cannot read properties of undefined' errors", () => {
256
+ setMode("strict");
257
+ const ctx = createMockContext();
258
+ let thrown: MapperResolutionError | null = null;
259
+ try {
260
+ mapper.replaceString("js/ctx.req.body.deeply.nested.value", ctx, {});
261
+ } catch (e) {
262
+ thrown = e as MapperResolutionError;
263
+ }
264
+ expect(thrown?.message).toMatch(/hint: the path/);
265
+ expect(thrown?.message).toMatch(/check the trigger payload/i);
266
+ });
267
+
268
+ it("includes a hint for ReferenceError ('not defined')", () => {
269
+ setMode("strict");
270
+ const ctx = createMockContext();
271
+ let thrown: MapperResolutionError | null = null;
272
+ try {
273
+ mapper.replaceString("js/unknownIdentifier.foo", ctx, {});
274
+ } catch (e) {
275
+ thrown = e as MapperResolutionError;
276
+ }
277
+ expect(thrown?.message).toMatch(/`unknownIdentifier` is not in scope/);
278
+ expect(thrown?.message).toMatch(/ctx, data, func, vars/);
279
+ });
280
+
281
+ it("includes a hint for syntax errors", () => {
282
+ setMode("strict");
283
+ const ctx = createMockContext();
284
+ let thrown: MapperResolutionError | null = null;
285
+ try {
286
+ mapper.replaceString("js/ctx.req.body.+", ctx, {});
287
+ } catch (e) {
288
+ thrown = e as MapperResolutionError;
289
+ }
290
+ expect(thrown?.message).toMatch(/not valid JavaScript/);
291
+ });
292
+
293
+ it("works with `instanceof` after JSON round-trip preservation (Object.setPrototypeOf)", () => {
294
+ setMode("strict");
295
+ const ctx = createMockContext();
296
+ let thrown: unknown = null;
297
+ try {
298
+ mapper.replaceString("js/ctx.req.body.x.y", ctx, {});
299
+ } catch (e) {
300
+ thrown = e;
301
+ }
302
+ expect(thrown instanceof MapperResolutionError).toBe(true);
303
+ expect(thrown instanceof Error).toBe(true);
304
+ });
305
+
306
+ it("attaches Error.cause for native cause-chain support", () => {
307
+ setMode("strict");
308
+ const ctx = createMockContext();
309
+ let thrown: MapperResolutionError | null = null;
310
+ try {
311
+ mapper.replaceString("js/ctx.req.body.x.y", ctx, {});
312
+ } catch (e) {
313
+ thrown = e as MapperResolutionError;
314
+ }
315
+ const e = thrown as Error & { cause?: unknown };
316
+ expect(e.cause).toBeDefined();
317
+ expect(e.cause).toBe(thrown?.context.cause);
318
+ });
319
+ });
320
+
321
+ // =========================================================================
322
+ // replaceObjectStrings — recursion + mutation contract
323
+ // =========================================================================
324
+
77
325
  describe("replaceObjectStrings()", () => {
78
- it("should replace string values in flat object", () => {
326
+ it("replaces string values in flat object", () => {
79
327
  const ctx = createMockContext();
80
328
  const data = { greeting: "World" };
81
329
  const obj: Record<string, unknown> = { msg: "Hello ${greeting}" };
82
- mapper.replaceObjectStrings(obj, ctx, data);
330
+ mapper.replaceObjectStrings(obj as Record<string, string>, ctx, data);
83
331
  expect(obj.msg).toBe("Hello World");
84
332
  });
85
333
 
86
- it("should recursively replace nested objects", () => {
334
+ it("recursively replaces nested objects", () => {
87
335
  const ctx = createMockContext();
88
336
  const data = { val: "replaced" };
89
337
  const obj: Record<string, unknown> = {
90
- level1: {
91
- level2: "value is ${val}",
92
- },
338
+ level1: { level2: "value is ${val}" },
93
339
  };
94
- mapper.replaceObjectStrings(obj, ctx, data);
340
+ mapper.replaceObjectStrings(obj as Record<string, string>, ctx, data);
95
341
  expect((obj.level1 as Record<string, unknown>).level2).toBe("value is replaced");
96
342
  });
97
343
 
98
- it("should skip non-string, non-object values", () => {
344
+ it("skips non-string, non-object values (null, primitives untouched)", () => {
99
345
  const ctx = createMockContext();
100
- const obj: Record<string, unknown> = { num: 42, bool: true, str: "keep" };
101
- mapper.replaceObjectStrings(obj, ctx, {});
346
+ const obj: Record<string, unknown> = { num: 42, bool: true, str: "keep", nullish: null };
347
+ mapper.replaceObjectStrings(obj as Record<string, string>, ctx, {});
102
348
  expect(obj.num).toBe(42);
103
349
  expect(obj.bool).toBe(true);
104
350
  expect(obj.str).toBe("keep");
351
+ expect(obj.nullish).toBe(null);
352
+ });
353
+
354
+ it("preserves the actual resolved type when assigning back to the dictionary slot", () => {
355
+ // `obj.count` ends up as the NUMBER 5, not the string "5",
356
+ // because js/ expressions return their actual evaluated type.
357
+ const ctx = createMockContext({ vars: { count: 5 } });
358
+ const obj: Record<string, unknown> = { count: "js/ctx.vars.count" };
359
+ mapper.replaceObjectStrings(obj as Record<string, string>, ctx, {});
360
+ expect(obj.count).toBe(5);
361
+ expect(typeof obj.count).toBe("number");
105
362
  });
106
363
  });
107
364
 
365
+ // =========================================================================
366
+ // jsMapper via replaceString
367
+ // =========================================================================
368
+
108
369
  describe("jsMapper via replaceString", () => {
109
- it("should access ctx in js/ expressions", () => {
370
+ it("accesses ctx in js/ expressions", () => {
110
371
  const ctx = createMockContext({ vars: { count: 5 } });
111
372
  const result = mapper.replaceString("js/ctx.vars.count", ctx, {});
112
373
  expect(result).toBe(5);
113
374
  });
114
375
 
115
- it("should handle js/ errors silently", () => {
116
- const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
117
- const ctx = createMockContext();
376
+ it("handles js/ errors in default mode (warn) by passing through the literal", () => {
377
+ const ctx = createMockContext({
378
+ logger: { log: vi.fn(), logLevel: vi.fn(), error: vi.fn() } as unknown as Context["logger"],
379
+ });
118
380
  const result = mapper.replaceString('js/throw new Error("fail")', ctx, {});
119
- // Should return the original string on error
381
+ // Original literal passes through (back-compat).
120
382
  expect(result).toBe('js/throw new Error("fail")');
121
- consoleSpy.mockRestore();
383
+ });
384
+
385
+ it("returns the actual evaluated type (number, object, array) — not a string", () => {
386
+ const ctx = createMockContext();
387
+ expect(mapper.replaceString("js/[1, 2, 3]", ctx, {})).toEqual([1, 2, 3]);
388
+ expect(mapper.replaceString('js/({hello: "world"})', ctx, {})).toEqual({ hello: "world" });
389
+ expect(mapper.replaceString("js/true", ctx, {})).toBe(true);
122
390
  });
123
391
  });
124
392
  });
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { MapperResolutionError } from "../../../src/utils/MapperResolutionError";
3
+
4
+ describe("MapperResolutionError", () => {
5
+ it("constructs with name 'MapperResolutionError'", () => {
6
+ const e = new MapperResolutionError("msg", { expression: "x", syntax: "js" });
7
+ expect(e.name).toBe("MapperResolutionError");
8
+ });
9
+
10
+ it("preserves the prototype chain across `instanceof` checks", () => {
11
+ const e = new MapperResolutionError("msg", { expression: "x", syntax: "js" });
12
+ expect(e instanceof MapperResolutionError).toBe(true);
13
+ expect(e instanceof Error).toBe(true);
14
+ });
15
+
16
+ it("carries the structured context object verbatim", () => {
17
+ const cause = new TypeError("boom");
18
+ const e = new MapperResolutionError("msg", {
19
+ expression: "ctx.req.body.id",
20
+ syntax: "js",
21
+ workflowName: "wf-1",
22
+ stepName: "step-2",
23
+ cause,
24
+ });
25
+ expect(e.context.expression).toBe("ctx.req.body.id");
26
+ expect(e.context.syntax).toBe("js");
27
+ expect(e.context.workflowName).toBe("wf-1");
28
+ expect(e.context.stepName).toBe("step-2");
29
+ expect(e.context.cause).toBe(cause);
30
+ });
31
+
32
+ it("attaches Error.cause when context.cause is provided (ES2022 cause-chain)", () => {
33
+ const cause = new Error("underlying");
34
+ const e = new MapperResolutionError("msg", { expression: "x", syntax: "js", cause });
35
+ // `cause` is set on the Error instance per spec.
36
+ expect((e as Error & { cause?: unknown }).cause).toBe(cause);
37
+ });
38
+
39
+ it("does NOT set Error.cause when context.cause is omitted", () => {
40
+ const e = new MapperResolutionError("msg", { expression: "x", syntax: "js" });
41
+ expect((e as Error & { cause?: unknown }).cause).toBeUndefined();
42
+ });
43
+
44
+ it("supports both syntax discriminators (js + template)", () => {
45
+ const a = new MapperResolutionError("msg", { expression: "ctx.x", syntax: "js" });
46
+ const b = new MapperResolutionError("msg", { expression: "ctx.x", syntax: "template" });
47
+ expect(a.context.syntax).toBe("js");
48
+ expect(b.context.syntax).toBe("template");
49
+ });
50
+
51
+ it("captures the original message verbatim (multi-line OK)", () => {
52
+ const msg = "[blok][mapper] Failed to resolve `js/x`\n underlying: bad\n hint: try this";
53
+ const e = new MapperResolutionError(msg, { expression: "x", syntax: "js" });
54
+ expect(e.message).toBe(msg);
55
+ });
56
+
57
+ it("makes context fields readonly at the type level (compile-time check)", () => {
58
+ // This test exists for documentation — TS rejects mutation on
59
+ // `readonly` fields at compile time. At runtime, the object
60
+ // is plain. We assert the shape, not enforcement.
61
+ const e = new MapperResolutionError("msg", { expression: "x", syntax: "js" });
62
+ expect(Object.isFrozen(e.context)).toBe(false); // readonly is type-level only
63
+ });
64
+ });
@@ -40,6 +40,64 @@ export default abstract class NodeBase {
40
40
  * Only `ctx.prev` carries the result to the immediately next step.
41
41
  */
42
42
  ephemeral: boolean;
43
+ /**
44
+ * Optional cache key for this step's result. When set, the runner consults
45
+ * the idempotency cache before executing — a hit returns the cached result
46
+ * (and emits a NODE_CACHED event); a miss runs the step and caches its
47
+ * result on success. Cache namespace is (workflowName, name, idempotencyKey).
48
+ *
49
+ * Author-facing values may be a literal string ("user-123") or a $ proxy
50
+ * expression compiled to `js/ctx....`. The runner resolves the expression
51
+ * against the live ctx at run time before consulting the cache.
52
+ */
53
+ idempotencyKey?: string;
54
+ /**
55
+ * Optional cache lifetime in milliseconds. Defaults to 24 hours
56
+ * (86_400_000) when undefined. Pass 0 to mark a stored result as
57
+ * immediately expired (effectively disables caching for this step).
58
+ */
59
+ idempotencyKeyTTL?: number;
60
+ /**
61
+ * Optional retry configuration with capped exponential backoff. When
62
+ * undefined, the step runs at most once (matches pre-v0.3.x behaviour).
63
+ * Per-attempt failures emit `NODE_ATTEMPT_FAILED` trace events.
64
+ */
65
+ retry?: {
66
+ maxAttempts: number;
67
+ minTimeoutInMs?: number;
68
+ maxTimeoutInMs?: number;
69
+ factor?: number;
70
+ };
71
+ /**
72
+ * Tier 2 quick-wins — per-attempt execution timeout in milliseconds.
73
+ * When set, `RunnerSteps` wraps each `step.process()` in a setTimeout-
74
+ * based Promise.race. On timeout, throws `StepTimeoutError` (which the
75
+ * retry loop treats as any other error). On final-attempt timeout,
76
+ * the run auto-flips to `"timedOut"` status. When undefined, the
77
+ * step runs without a per-attempt cap (matches pre-quick-wins
78
+ * behaviour).
79
+ *
80
+ * Originally set as a duration string or number on the step schema;
81
+ * `Configuration.getSteps` converts to milliseconds via
82
+ * `parseDuration` before assigning here.
83
+ */
84
+ maxDurationMs?: number;
85
+ /**
86
+ * Name of the workflow to invoke when this step runs. When set, the
87
+ * step's `node` ref is `"@blokjs/subworkflow"` (a sentinel) and the
88
+ * runner resolves it to a `SubworkflowNode` that looks up the child
89
+ * by this name in the `WorkflowRegistry` singleton.
90
+ */
91
+ subworkflow?: string;
92
+ /**
93
+ * If true (default), the parent step blocks until the child workflow
94
+ * completes. The child's `ctx.response` becomes the parent step's
95
+ * output (lands on `state[<id>]` like any other step).
96
+ *
97
+ * `wait: false` (fire-and-forget) is rejected at workflow load time
98
+ * in v0.3.x — the schema includes a deferred-feature error message.
99
+ */
100
+ wait?: boolean;
43
101
  process(ctx: Context, step?: Step): Promise<ResponseContext>;
44
102
  processFlow(ctx: Context): Promise<ResponseContext>;
45
103
  abstract run(ctx: Context): Promise<ResponseContext>;
package/dist/NodeBase.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import _ from "lodash";
2
2
  import GlobalError from "./GlobalError";
3
3
  import mapper from "./utils/Mapper";
4
+ import { MapperResolutionError } from "./utils/MapperResolutionError";
4
5
  export default class NodeBase {
5
6
  flow = false;
6
7
  name = "";
@@ -39,6 +40,75 @@ export default class NodeBase {
39
40
  * Only `ctx.prev` carries the result to the immediately next step.
40
41
  */
41
42
  ephemeral = false;
43
+ // =========================================================================
44
+ // V2 idempotency cache + retry knobs — populated by Configuration.getSteps
45
+ // from the step definition. Read by RunnerSteps before delegating to
46
+ // `step.process()`. Caching layers ABOVE PersistenceHelper.applyStepOutput;
47
+ // retry wraps the same call site.
48
+ //
49
+ // Mirrors the Zod schema in `@blokjs/helper/src/types/StepOpts.ts`. Kept
50
+ // as a structural interface here to avoid a runtime dep from shared on
51
+ // helper.
52
+ // =========================================================================
53
+ /**
54
+ * Optional cache key for this step's result. When set, the runner consults
55
+ * the idempotency cache before executing — a hit returns the cached result
56
+ * (and emits a NODE_CACHED event); a miss runs the step and caches its
57
+ * result on success. Cache namespace is (workflowName, name, idempotencyKey).
58
+ *
59
+ * Author-facing values may be a literal string ("user-123") or a $ proxy
60
+ * expression compiled to `js/ctx....`. The runner resolves the expression
61
+ * against the live ctx at run time before consulting the cache.
62
+ */
63
+ idempotencyKey;
64
+ /**
65
+ * Optional cache lifetime in milliseconds. Defaults to 24 hours
66
+ * (86_400_000) when undefined. Pass 0 to mark a stored result as
67
+ * immediately expired (effectively disables caching for this step).
68
+ */
69
+ idempotencyKeyTTL;
70
+ /**
71
+ * Optional retry configuration with capped exponential backoff. When
72
+ * undefined, the step runs at most once (matches pre-v0.3.x behaviour).
73
+ * Per-attempt failures emit `NODE_ATTEMPT_FAILED` trace events.
74
+ */
75
+ retry;
76
+ /**
77
+ * Tier 2 quick-wins — per-attempt execution timeout in milliseconds.
78
+ * When set, `RunnerSteps` wraps each `step.process()` in a setTimeout-
79
+ * based Promise.race. On timeout, throws `StepTimeoutError` (which the
80
+ * retry loop treats as any other error). On final-attempt timeout,
81
+ * the run auto-flips to `"timedOut"` status. When undefined, the
82
+ * step runs without a per-attempt cap (matches pre-quick-wins
83
+ * behaviour).
84
+ *
85
+ * Originally set as a duration string or number on the step schema;
86
+ * `Configuration.getSteps` converts to milliseconds via
87
+ * `parseDuration` before assigning here.
88
+ */
89
+ maxDurationMs;
90
+ // =========================================================================
91
+ // V2 sub-workflow knobs — populated by Configuration.getSteps for steps
92
+ // that invoke another workflow (`subworkflow: "<name>"` shape). Read by
93
+ // `SubworkflowNode.run()` to look up the child workflow in the
94
+ // WorkflowRegistry. Mirrors the Zod schema in `@blokjs/helper`.
95
+ // =========================================================================
96
+ /**
97
+ * Name of the workflow to invoke when this step runs. When set, the
98
+ * step's `node` ref is `"@blokjs/subworkflow"` (a sentinel) and the
99
+ * runner resolves it to a `SubworkflowNode` that looks up the child
100
+ * by this name in the `WorkflowRegistry` singleton.
101
+ */
102
+ subworkflow;
103
+ /**
104
+ * If true (default), the parent step blocks until the child workflow
105
+ * completes. The child's `ctx.response` becomes the parent step's
106
+ * output (lands on `state[<id>]` like any other step).
107
+ *
108
+ * `wait: false` (fire-and-forget) is rejected at workflow load time
109
+ * in v0.3.x — the schema includes a deferred-feature error message.
110
+ */
111
+ wait;
42
112
  async process(ctx, step) {
43
113
  let response = {
44
114
  success: true,
@@ -106,7 +176,15 @@ export default class NodeBase {
106
176
  mapper.replaceObjectStrings(newObj, ctx, data);
107
177
  }
108
178
  catch (e) {
109
- console.log("MAPPER ERROR", e);
179
+ // `MapperResolutionError` (strict mode) carries full diagnostic
180
+ // context — let it escape so the step's error envelope surfaces
181
+ // the workflow / step / expression that failed.
182
+ if (e instanceof MapperResolutionError)
183
+ throw e;
184
+ // Anything else here is an UNEXPECTED bug in the mapper itself
185
+ // (recursion fault, OOM, logger crash). Surface loudly via
186
+ // stderr WITH the stack trace — never silently swallow.
187
+ console.error("[blok][mapper] unexpected error during input resolution:", e);
110
188
  }
111
189
  return newObj;
112
190
  };
@@ -1 +1 @@
1
- {"version":3,"file":"NodeBase.js","sourceRoot":"","sources":["../src/NodeBase.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"NodeBase.js","sourceRoot":"","sources":["../src/NodeBase.ts"],"names":[],"mappings":"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"}