@hotmeshio/hotmesh 0.14.3 → 0.14.4

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "obfuscate": "ts-node scripts/obfuscate.ts",
15
15
  "clean-build": "npm run clean && npm run build",
16
16
  "clean-build-obfuscate": "npm run clean-build && npm run obfuscate",
17
- "docs": "typedoc",
18
- "docs:clean": "rimraf ./docs/hotmesh && typedoc",
17
+ "docs": "typedoc && cp -R docs/hotmesh/* docs/ && rm -rf docs/hotmesh",
18
+ "docs:clean": "rimraf ./docs/hotmesh && typedoc && cp -R docs/hotmesh/* docs/ && rm -rf docs/hotmesh",
19
19
  "lint": "eslint . --ext .ts",
20
20
  "lint:fix": "eslint . --fix --ext .ts",
21
21
  "start": "ts-node src/index.ts",
@@ -47,6 +47,7 @@
47
47
  "test:durable:retrypolicy": "vitest run tests/durable/retry-policy",
48
48
  "test:durable:sleep": "vitest run tests/durable/sleep/postgres.test.ts",
49
49
  "test:durable:signal": "vitest run tests/durable/signal/postgres.test.ts",
50
+ "test:durable:readonly": "docker compose --profile readonly up -d --build && docker compose exec hotmesh-readonly npx vitest run --config tests/durable/readonly/vitest.config.mts",
50
51
  "test:durable:unknown": "vitest run tests/durable/unknown/postgres.test.ts",
51
52
  "test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
52
53
  "test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
@@ -516,10 +516,12 @@ class WorkerService {
516
516
  if (WorkerService.instances.has(targetTopic)) {
517
517
  return await WorkerService.instances.get(targetTopic);
518
518
  }
519
+ const readonly = providerConfig.readonly ?? false;
519
520
  const workerEntry = {
520
521
  topic: activityTopic,
521
522
  connection: providerConfig,
522
523
  callback: this.wrapActivityFunctions().bind(this),
524
+ readonly,
523
525
  };
524
526
  if (config.workerCredentials) {
525
527
  workerEntry.workerCredentials = config.workerCredentials;
@@ -644,10 +646,12 @@ class WorkerService {
644
646
  const targetNamespace = config?.namespace ?? factory_1.APP_ID;
645
647
  const optionsHash = WorkerService.hashOptions(config?.connection);
646
648
  const targetTopic = `${optionsHash}.${targetNamespace}.${workflowTopic}`;
649
+ const readonly = providerConfig.readonly ?? false;
647
650
  const workerEntry = {
648
651
  topic: taskQueue,
649
652
  workflowName: workflowFunctionName,
650
653
  connection: providerConfig,
654
+ readonly,
651
655
  callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic, workflowFunctionName, config).bind(this),
652
656
  };
653
657
  if (config.workerCredentials) {
@@ -1,10 +1,42 @@
1
1
  import { JobState } from '../../types/job';
2
2
  import { TransitionRule } from '../../types/transition';
3
3
  import { StreamCode } from '../../types';
4
+ /**
5
+ * Evaluates and transforms data-mapping rules against live job state.
6
+ *
7
+ * @remarks
8
+ * MapperService is the bridge between a workflow's declarative mapping
9
+ * rules (including `@pipe` expressions) and the runtime job data. It
10
+ * recursively walks a rule tree, delegating leaf-level resolution to
11
+ * the {@link Pipe} engine. Static helpers such as {@link evaluate}
12
+ * also power transition-condition checks during workflow execution.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const mapper = new MapperService(
17
+ * { greeting: '{data.name}', score: { '@pipe': [['{data.x}', '{data.y}'], ['{@math.add}']] } },
18
+ * jobState,
19
+ * );
20
+ * const result = mapper.mapRules();
21
+ * // => { greeting: 'Alice', score: 7 }
22
+ * ```
23
+ */
4
24
  declare class MapperService {
5
25
  private rules;
6
26
  private data;
27
+ /**
28
+ * @param rules - The mapping rule tree to evaluate. May contain
29
+ * literal values, `{data.*}` references, or nested `@pipe` objects.
30
+ * @param data - The current {@link JobState} used to resolve references.
31
+ */
7
32
  constructor(rules: Record<string, unknown>, data: JobState);
33
+ /**
34
+ * Recursively resolves every rule in the tree against the current job
35
+ * state and returns a fully-evaluated copy.
36
+ *
37
+ * @returns A plain object mirroring the rule structure with all
38
+ * expressions replaced by their resolved values.
39
+ */
8
40
  mapRules(): Record<string, unknown>;
9
41
  private traverseRules;
10
42
  /**
@@ -20,8 +52,31 @@ declare class MapperService {
20
52
  */
21
53
  private resolve;
22
54
  /**
23
- * Evaluates a transition rule against the current job state and incoming Stream message
24
- * to determine which (if any) transition should be taken.
55
+ * Evaluates a transition rule against the current job state and an
56
+ * incoming Stream message code to decide whether the transition fires.
57
+ *
58
+ * @remarks
59
+ * Supports both simple boolean rules (`true` / `false`) and compound
60
+ * match rules with optional AND / OR gating. When the rule includes
61
+ * a `match` array, each entry's `actual` expression is resolved via
62
+ * {@link Pipe.resolve} and compared to the `expected` value.
63
+ *
64
+ * @param transitionRule - A boolean shorthand or a {@link TransitionRule}
65
+ * containing `code`, optional `gate`, and optional `match` conditions.
66
+ * @param context - The current {@link JobState} used to resolve
67
+ * `actual` expressions inside match entries.
68
+ * @param code - The {@link StreamCode} returned by the preceding
69
+ * activity (e.g. `200`).
70
+ * @returns `true` if the transition should be taken, `false` otherwise.
71
+ *
72
+ * @example
73
+ * ```typescript
74
+ * const shouldTransition = MapperService.evaluate(
75
+ * { code: 200, match: [{ expected: true, actual: '{data.isReady}' }] },
76
+ * jobState,
77
+ * 200,
78
+ * );
79
+ * ```
25
80
  */
26
81
  static evaluate(transitionRule: TransitionRule | boolean, context: JobState, code: StreamCode): boolean;
27
82
  }
@@ -2,11 +2,43 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MapperService = void 0;
4
4
  const pipe_1 = require("../pipe");
5
+ /**
6
+ * Evaluates and transforms data-mapping rules against live job state.
7
+ *
8
+ * @remarks
9
+ * MapperService is the bridge between a workflow's declarative mapping
10
+ * rules (including `@pipe` expressions) and the runtime job data. It
11
+ * recursively walks a rule tree, delegating leaf-level resolution to
12
+ * the {@link Pipe} engine. Static helpers such as {@link evaluate}
13
+ * also power transition-condition checks during workflow execution.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const mapper = new MapperService(
18
+ * { greeting: '{data.name}', score: { '@pipe': [['{data.x}', '{data.y}'], ['{@math.add}']] } },
19
+ * jobState,
20
+ * );
21
+ * const result = mapper.mapRules();
22
+ * // => { greeting: 'Alice', score: 7 }
23
+ * ```
24
+ */
5
25
  class MapperService {
26
+ /**
27
+ * @param rules - The mapping rule tree to evaluate. May contain
28
+ * literal values, `{data.*}` references, or nested `@pipe` objects.
29
+ * @param data - The current {@link JobState} used to resolve references.
30
+ */
6
31
  constructor(rules, data) {
7
32
  this.rules = rules;
8
33
  this.data = data;
9
34
  }
35
+ /**
36
+ * Recursively resolves every rule in the tree against the current job
37
+ * state and returns a fully-evaluated copy.
38
+ *
39
+ * @returns A plain object mirroring the rule structure with all
40
+ * expressions replaced by their resolved values.
41
+ */
10
42
  mapRules() {
11
43
  return this.traverseRules(this.rules);
12
44
  }
@@ -46,8 +78,31 @@ class MapperService {
46
78
  return pipe.process();
47
79
  }
48
80
  /**
49
- * Evaluates a transition rule against the current job state and incoming Stream message
50
- * to determine which (if any) transition should be taken.
81
+ * Evaluates a transition rule against the current job state and an
82
+ * incoming Stream message code to decide whether the transition fires.
83
+ *
84
+ * @remarks
85
+ * Supports both simple boolean rules (`true` / `false`) and compound
86
+ * match rules with optional AND / OR gating. When the rule includes
87
+ * a `match` array, each entry's `actual` expression is resolved via
88
+ * {@link Pipe.resolve} and compared to the `expected` value.
89
+ *
90
+ * @param transitionRule - A boolean shorthand or a {@link TransitionRule}
91
+ * containing `code`, optional `gate`, and optional `match` conditions.
92
+ * @param context - The current {@link JobState} used to resolve
93
+ * `actual` expressions inside match entries.
94
+ * @param code - The {@link StreamCode} returned by the preceding
95
+ * activity (e.g. `200`).
96
+ * @returns `true` if the transition should be taken, `false` otherwise.
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const shouldTransition = MapperService.evaluate(
101
+ * { code: 200, match: [{ expected: true, actual: '{data.isReady}' }] },
102
+ * jobState,
103
+ * 200,
104
+ * );
105
+ * ```
51
106
  */
52
107
  static evaluate(transitionRule, context, code) {
53
108
  if (typeof transitionRule === 'boolean') {
@@ -1,44 +1,478 @@
1
1
  import { JobState, JobData } from '../../types/job';
2
2
  import { PipeContext, PipeItem, PipeItems, Pipe as PipeType } from '../../types/pipe';
3
+ /**
4
+ * A functional data-transformation pipeline that resolves expressions
5
+ * row-by-row against live job data.
6
+ *
7
+ * @remarks
8
+ * ## Overview
9
+ *
10
+ * Pipe is the engine behind HotMesh's `@pipe` syntax — a uniform,
11
+ * functional approach to data mapping and transformation. Every
12
+ * ECMAScript operation is expressed as a **function with inputs**,
13
+ * eliminating the syntactic variability of the language (ternaries,
14
+ * property access, instance methods) in favor of a single,
15
+ * composable pattern.
16
+ *
17
+ * ## Postfix notation (Reverse Polish Notation)
18
+ *
19
+ * If you've used an RPN calculator, `@pipe` will feel familiar.
20
+ * Operands come first, then the operator consumes them:
21
+ *
22
+ * ```
23
+ * RPN: 4 4 + → 8
24
+ *
25
+ * @pipe:
26
+ * - [4, 4] ← operands
27
+ * - ["{@math.add}"] ← operator → 8
28
+ * ```
29
+ *
30
+ * That's the entire model. Operands are pushed, an operator pops
31
+ * them, and the result becomes an operand for the next row.
32
+ *
33
+ * ## How it works — row by row
34
+ *
35
+ * A pipe is an ordered array of **rows**. Each row is an array of
36
+ * **cells**. The key rule: **every resolved value on a row flows
37
+ * into cell 0 of the next row** (the operator). The operator
38
+ * consumes all upstream operands, resolves to a new value, and
39
+ * that result *becomes an operand*. Any remaining cells on the
40
+ * same row are resolved independently and appended as additional
41
+ * operands. Then the cycle repeats.
42
+ *
43
+ * ```
44
+ * @pipe:
45
+ * ┌──────────────────────────────────────┐
46
+ * │ Row 0: [operand, operand, operand] │ ← resolve all cells
47
+ * │ │ │ │ │
48
+ * │ └────────┼────────┘ │
49
+ * │ │ │
50
+ * ├─────────────────────┼────────────────┤
51
+ * │ ▼ │
52
+ * │ Row 1: [ OPERATOR , operand] │ ← ALL operands from Row 0
53
+ * │ ▲ receives all ▲ │ flow into cell 0;
54
+ * │ │ from above │ │ result becomes operand;
55
+ * │ └──────────────┘ │ remaining cells resolve
56
+ * │ │ │ │
57
+ * │ └────────────┘ │
58
+ * │ │ │
59
+ * ├─────────────────────┼────────────────┤
60
+ * │ ▼ │
61
+ * │ Row 2: [ OPERATOR , operand] │ ← same pattern repeats
62
+ * └──────────────────────────────────────┘
63
+ * ▼
64
+ * final value = cell 0 of last row
65
+ * ```
66
+ *
67
+ * Step by step:
68
+ *
69
+ * 1. **Row 0** — Every cell is resolved independently (static
70
+ * literals, `{data.*}` references, or nullary functions like
71
+ * `{@date.now}`). The resolved values are all **operands**.
72
+ * 2. **Row 1** — Cell 0 is the **operator** (`{@domain.method}`).
73
+ * It receives *all* operands from Row 0 as its arguments and
74
+ * produces a result. That result replaces cell 0 — it is now
75
+ * an operand. Any remaining cells (cell 1, 2, ...) on this row
76
+ * are resolved independently and become additional operands.
77
+ * 3. **Row 2** — Cell 0 is the next operator. It receives *all*
78
+ * operands from Row 1 (the prior result + any extra cells).
79
+ * The cycle repeats.
80
+ * 4. **Return** — The first cell of the final row is the result.
81
+ *
82
+ * When an additional cell on an operator row needs computed (not
83
+ * just a static value or simple reference), use a **nested
84
+ * `@pipe`** (sub-pipe) to resolve it. See Example 4 below.
85
+ *
86
+ * ## Three types of cell values
87
+ *
88
+ * | Type | Syntax | Example |
89
+ * |------|--------|---------|
90
+ * | **Static** | literal | `42`, `"hello"`, `true` |
91
+ * | **Dynamic** | `{path}` | `{a.output.data.name}` |
92
+ * | **Function** | `{@domain.method}` | `{@string.concat}`, `{@math.add}` |
93
+ *
94
+ * ## Available function domains
95
+ *
96
+ * - **array** — `get`, `length`, `join`, `concat`, `push`, `indexOf`, ...
97
+ * - **bitwise** — `and`, `or`, `xor`, `not`, ...
98
+ * - **conditional** — `ternary`, `equality`, `nullish`, ...
99
+ * - **cron** — `nextDelay`
100
+ * - **date** — `now`, `toLocaleString`, `yyyymmdd`, ...
101
+ * - **json** — `parse`, `stringify`
102
+ * - **logical** — `and`, `or`, `not`
103
+ * - **math** — `add`, `multiply`, `pow`, `max`, `min`, `abs`, ...
104
+ * - **number** — `isEven`, `isOdd`, `gte`, `lte`, ...
105
+ * - **object** — `create`, `get`, `set`, `keys`, ...
106
+ * - **string** — `concat`, `split`, `charAt`, `toLowerCase`, `includes`, ...
107
+ * - **symbol**
108
+ * - **unary**
109
+ *
110
+ * ---
111
+ *
112
+ * ## Example 1 — Simple field mapping (YAML)
113
+ *
114
+ * Most fields need only a one-to-one reference. Curly braces pull
115
+ * values from upstream activity outputs:
116
+ *
117
+ * ```yaml
118
+ * # Map fields from activities a and b into a new shape
119
+ * maps:
120
+ * first: "{a.output.data.first_name}"
121
+ * last: "{a.output.data.last_name}"
122
+ * email: "{b.output.data.email}"
123
+ * age: "{b.output.data.age}"
124
+ * company: "ACME Corp" # static string
125
+ * bonus: 500 # static number
126
+ * ```
127
+ *
128
+ * The equivalent JavaScript object passed to the mapper:
129
+ *
130
+ * ```typescript
131
+ * const rules = {
132
+ * first: '{a.output.data.first_name}',
133
+ * last: '{a.output.data.last_name}',
134
+ * email: '{b.output.data.email}',
135
+ * age: '{b.output.data.age}',
136
+ * company: 'ACME Corp',
137
+ * bonus: 500,
138
+ * };
139
+ * ```
140
+ *
141
+ * ## Example 2 — String transformation (YAML → JS)
142
+ *
143
+ * Build a `user_name` like `jdoe` from "John Doe". Follow the
144
+ * RPN flow — operands feed into the operator on the next row,
145
+ * the result becomes an operand, extra cells append more operands:
146
+ *
147
+ * ```yaml
148
+ * user_name:
149
+ * "@pipe":
150
+ * - ["{a.output.data.first_name}", 0]
151
+ * - ["{@string.charAt}", "{a.output.data.last_name}"]
152
+ * - ["{@string.concat}"]
153
+ * - ["{@string.toLowerCase}"]
154
+ * ```
155
+ *
156
+ * ```typescript
157
+ * // Identical logic in JavaScript
158
+ * const rules = {
159
+ * user_name: {
160
+ * '@pipe': [
161
+ * ['{a.output.data.first_name}', 0],
162
+ * ['{@string.charAt}', '{a.output.data.last_name}'],
163
+ * ['{@string.concat}'],
164
+ * ['{@string.toLowerCase}'],
165
+ * ],
166
+ * },
167
+ * };
168
+ * ```
169
+ *
170
+ * RPN trace (given first_name="John", last_name="Doe"):
171
+ *
172
+ * ```
173
+ * Row 0: ["John", 0] ← two operands
174
+ * Row 1: charAt("John", 0)="J" ← operator consumes both → result "J"
175
+ * then resolve "Doe" ← extra cell → operands are ["J", "Doe"]
176
+ * Row 2: concat("J", "Doe") ← operator → result "JDoe"
177
+ * operands are ["JDoe"]
178
+ * Row 3: toLowerCase("JDoe") ← operator → result "jdoe"
179
+ * ```
180
+ *
181
+ * ## Example 3 — Conditional logic (YAML → JS)
182
+ *
183
+ * Classify an employee as "Senior" or "Junior" based on age:
184
+ *
185
+ * ```yaml
186
+ * status:
187
+ * "@pipe":
188
+ * - ["{b.output.data.age}", 40]
189
+ * - ["{@number.gte}", "Senior", "Junior"]
190
+ * - ["{@conditional.ternary}"]
191
+ * ```
192
+ *
193
+ * ```typescript
194
+ * const rules = {
195
+ * status: {
196
+ * '@pipe': [
197
+ * ['{b.output.data.age}', 40],
198
+ * ['{@number.gte}', 'Senior', 'Junior'],
199
+ * ['{@conditional.ternary}'],
200
+ * ],
201
+ * },
202
+ * };
203
+ * ```
204
+ *
205
+ * RPN trace (given age=30):
206
+ *
207
+ * ```
208
+ * Row 0: [30, 40] ← two operands
209
+ * Row 1: gte(30, 40)=false ← operator consumes both → false
210
+ * resolve "Senior", "Junior" ← extra cells → [false, "Senior", "Junior"]
211
+ * Row 2: ternary(false, "Senior", "Junior") ← operator → "Junior"
212
+ * ```
213
+ *
214
+ * ## Example 4 — Nested pipes (fan-out / fan-in)
215
+ *
216
+ * Extract initials from a full name. Each nested `@pipe` runs
217
+ * independently (fan-out), then the first standard row after them
218
+ * receives all sub-pipe results as inputs (fan-in):
219
+ *
220
+ * ```yaml
221
+ * initials:
222
+ * "@pipe":
223
+ * - ["{a.output.data.full_name}", " "]
224
+ * - "@pipe": # fan-out: first initial
225
+ * - ["{@string.split}", 0]
226
+ * - ["{@array.get}", 0]
227
+ * - ["{@string.charAt}"]
228
+ * - "@pipe": # fan-out: last initial
229
+ * - ["{@string.split}", 1]
230
+ * - ["{@array.get}", 0]
231
+ * - ["{@string.charAt}"]
232
+ * - ["{@string.concat}"] # fan-in: combine both
233
+ * ```
234
+ *
235
+ * ```typescript
236
+ * const rules = {
237
+ * initials: {
238
+ * '@pipe': [
239
+ * ['{a.output.data.full_name}', ' '],
240
+ * { '@pipe': [['{@string.split}', 0], ['{@array.get}', 0], ['{@string.charAt}']] },
241
+ * { '@pipe': [['{@string.split}', 1], ['{@array.get}', 0], ['{@string.charAt}']] },
242
+ * ['{@string.concat}'],
243
+ * ],
244
+ * },
245
+ * };
246
+ * // "Luke Birdeau" → split → ["Luke","Birdeau"]
247
+ * // sub-pipe 1: get(0) → "Luke" → charAt(0) → "L"
248
+ * // sub-pipe 2: get(1) → "Birdeau" → charAt(0) → "B"
249
+ * // fan-in: concat("L", "B") → "LB"
250
+ * ```
251
+ *
252
+ * ## Example 5 — Reduce (iterate and accumulate)
253
+ *
254
+ * Transform an array of `{ full_name }` objects into
255
+ * `{ first, last }` pairs using `@reduce`. Context variables
256
+ * `{$item}`, `{$key}`, `{$index}`, `{$input}`, and `{$output}`
257
+ * are available inside the reducer body:
258
+ *
259
+ * ```typescript
260
+ * const jobData = {
261
+ * a: { output: { data: [
262
+ * { full_name: 'Luke Birdeau' },
263
+ * { full_name: 'John Doe' },
264
+ * ]}}
265
+ * };
266
+ *
267
+ * const rules = [
268
+ * ['{a.output.data}', []], // input array + initial accumulator
269
+ * { '@reduce': [
270
+ * { '@pipe': [['{$output}']] }, // carry forward accumulator
271
+ * { '@pipe': [
272
+ * { '@pipe': [['first']] },
273
+ * { '@pipe': [
274
+ * ['{$item.full_name}', ' '],
275
+ * ['{@string.split}', 0],
276
+ * ['{@array.get}'],
277
+ * ]},
278
+ * { '@pipe': [['last']] },
279
+ * { '@pipe': [
280
+ * ['{$item.full_name}', ' '],
281
+ * ['{@string.split}', 1],
282
+ * ['{@array.get}'],
283
+ * ]},
284
+ * ['{@object.create}'],
285
+ * ]},
286
+ * ['{@array.push}'],
287
+ * ]},
288
+ * ];
289
+ * // => [{ first: 'Luke', last: 'Birdeau' }, { first: 'John', last: 'Doe' }]
290
+ * ```
291
+ *
292
+ * ## Example 6 — Transition conditions
293
+ *
294
+ * Pipes power conditional transitions between workflow activities.
295
+ * In YAML, a transition fires only when the `@pipe` resolves to the
296
+ * `expected` value:
297
+ *
298
+ * ```yaml
299
+ * transitions:
300
+ * t1:
301
+ * - to: a1
302
+ * conditions:
303
+ * match:
304
+ * - expected: false
305
+ * actual:
306
+ * "@pipe":
307
+ * - ["{t1.output.data.a}", "goodbye"]
308
+ * - ["{@conditional.equality}"]
309
+ * ```
310
+ *
311
+ * ## Example 7 — Inline YAML with HotMesh.deploy
312
+ *
313
+ * HotMesh ships with an inline YAML parser, so a complete
314
+ * workflow with data mapping can be deployed in a single call:
315
+ *
316
+ * ```typescript
317
+ * await hotMesh.deploy(`
318
+ * app:
319
+ * id: myapp
320
+ * version: '1'
321
+ * graphs:
322
+ * - subscribes: order.process
323
+ * activities:
324
+ * t1:
325
+ * type: trigger
326
+ * a1:
327
+ * type: worker
328
+ * topic: inventory.check
329
+ * input:
330
+ * maps:
331
+ * itemId: "{t1.output.data.itemId}"
332
+ * output:
333
+ * schema:
334
+ * type: object
335
+ * job:
336
+ * maps:
337
+ * result: "{$self.output.data.available}"
338
+ * transitions:
339
+ * t1:
340
+ * - to: a1
341
+ * `);
342
+ *
343
+ * await hotMesh.activate('1');
344
+ * const response = await hotMesh.pubsub('order.process', { itemId: 'sku-42' });
345
+ * ```
346
+ */
3
347
  declare class Pipe {
4
348
  rules: PipeType;
5
349
  jobData: JobData;
6
350
  context: PipeContext;
351
+ /**
352
+ * @param rules - The ordered row array defining the pipeline.
353
+ * @param jobData - The current job data used to resolve `{data.*}`
354
+ * and `{activity.*}` references.
355
+ * @param context - Optional iteration context (`$item`, `$key`,
356
+ * `$output`, etc.) supplied during `@reduce` execution.
357
+ */
7
358
  constructor(rules: PipeType, jobData: JobData, context?: PipeContext);
8
359
  private isPipeType;
9
360
  private isreduceType;
361
+ /**
362
+ * Returns `true` if the value is a `@pipe` object (i.e. `{ '@pipe': [...] }`).
363
+ *
364
+ * @param obj - The value to test.
365
+ */
10
366
  static isPipeObject(obj: {
11
367
  [key: string]: unknown;
12
368
  } | PipeItem): boolean;
369
+ /**
370
+ * One-shot convenience method that resolves a single value or
371
+ * `@pipe` expression against the given context.
372
+ *
373
+ * @param unresolved - A literal value, a `{data.*}` reference string,
374
+ * or a `@pipe` object.
375
+ * @param context - Partial {@link JobState} used for resolution.
376
+ * @returns The fully resolved value.
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * Pipe.resolve('{data.user.email}', jobState);
381
+ * Pipe.resolve({ '@pipe': [['{data.a}', '{data.b}'], ['{@math.multiply}']] }, jobState);
382
+ * ```
383
+ */
13
384
  static resolve(unresolved: {
14
385
  [key: string]: unknown;
15
386
  } | PipeItem, context: Partial<JobState>): any;
16
387
  /**
17
- * loop through each PipeItem row in this Pipe, resolving and transforming line by line
18
- * @returns {any} the result of the pipe
388
+ * Executes the pipeline row-by-row, resolving and transforming
389
+ * until the final value is produced.
390
+ *
391
+ * @remarks
392
+ * Row 0 is resolved independently (values only). Each subsequent
393
+ * row feeds the prior row's resolved output as arguments to the
394
+ * function named in cell 0. Nested `@pipe` rows are queued
395
+ * (fan-out) and collected into the next standard row (fan-in).
396
+ * `@reduce` rows iterate over the prior row's array output.
397
+ *
398
+ * @param resolved - Optional pre-resolved seed values (used
399
+ * internally by `reduce` to inject the accumulator).
400
+ * @returns The first cell of the final resolved row.
401
+ *
402
+ * @example
403
+ * ```typescript
404
+ * // Split a full name and grab the first element
405
+ * const pipe = new Pipe(
406
+ * [['{a.output.data.full_name}', ' '], ['{@string.split}', 0], ['{@array.get}']],
407
+ * { a: { output: { data: { full_name: 'Luke Birdeau' } } } },
408
+ * );
409
+ * pipe.process(); // => 'Luke'
410
+ * ```
19
411
  */
20
412
  process(resolved?: unknown[] | null): any;
21
413
  cloneUnknown<T>(value: T): T;
22
414
  /**
23
- * Transforms iterable `input` into a single value. Vars $output, $item, $key
24
- * and $input are available. The final statement in the iterator (the reduction)
25
- * is assumed to be the return value. A default $output object may be provided
26
- * to the iterator by placing the the second cell of the preceding row. Otherwise,
27
- * construct the object during first run and ensure it is the first cell of the
28
- * last row of the iterator, so it is returned as the $output for the next cycle
29
- * @param {unknown[]} input
30
- * @returns {unknown}
415
+ * Iterates over an array or object and accumulates a result,
416
+ * similar to `Array.prototype.reduce`.
417
+ *
418
+ * @remarks
419
+ * Inside the reducer body the following context variables are
420
+ * available as cell references:
421
+ *
422
+ * | Variable | Description |
423
+ * |----------|-------------|
424
+ * | `{$input}` | The full input collection |
425
+ * | `{$output}` | The current accumulator |
426
+ * | `{$item}` | The current element |
427
+ * | `{$key}` | The current key (string for objects, index string for arrays) |
428
+ * | `{$index}` | The current numeric index |
429
+ *
430
+ * The preceding row supplies `[inputCollection, initialAccumulator]`.
431
+ * If no initial accumulator is provided, it defaults to `null`.
432
+ *
433
+ * @param input - A two-element array: `[collection, initialAccumulator]`.
434
+ * @returns The final accumulated value, wrapped in an array.
31
435
  * @private
32
436
  */
33
437
  reduce(input: Array<unknown[]>): unknown;
34
438
  private processRow;
439
+ /**
440
+ * Looks up a domain function by its `{@domain.method}` name string
441
+ * and returns the callable.
442
+ *
443
+ * @param functionName - A string like `{@math.add}` or `{@string.concat}`.
444
+ * @returns The resolved function reference.
445
+ * @throws If the domain or method is not registered.
446
+ */
35
447
  static resolveFunction(functionName: string): any;
448
+ /**
449
+ * Resolves every cell in a single row independently — each cell
450
+ * is evaluated for function calls, context variables, or mappable
451
+ * references.
452
+ *
453
+ * @param cells - The array of {@link PipeItems} to resolve.
454
+ * @returns An array of resolved values, one per input cell.
455
+ */
36
456
  processCells(cells: PipeItems): unknown[];
37
457
  private isFunction;
38
458
  private isContextVariable;
39
459
  private isMappable;
460
+ /**
461
+ * Resolves a single cell value by detecting its type — function
462
+ * call (`{@domain.fn}`), context variable (`{$item}`, `{$key}`,
463
+ * etc.), mappable reference (`{data.*}`), or literal.
464
+ *
465
+ * @param currentCell - The cell to resolve.
466
+ * @returns The resolved runtime value.
467
+ */
40
468
  resolveCellValue(currentCell: PipeItem): unknown;
41
469
  private getNestedProperty;
470
+ /**
471
+ * Resolves a `{data.*}` or `{activity.*}` reference by walking
472
+ * the dot-delimited path into {@link jobData}.
473
+ *
474
+ * @param currentCell - The mappable reference string (e.g. `{data.user.name}`).
475
+ */
42
476
  resolveMappableValue(currentCell: string): unknown;
43
477
  resolveContextValue(currentCell: string): unknown;
44
478
  resolveContextTerm(currentCell: string): string;
@@ -5,7 +5,358 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Pipe = void 0;
7
7
  const functions_1 = __importDefault(require("./functions"));
8
+ /**
9
+ * A functional data-transformation pipeline that resolves expressions
10
+ * row-by-row against live job data.
11
+ *
12
+ * @remarks
13
+ * ## Overview
14
+ *
15
+ * Pipe is the engine behind HotMesh's `@pipe` syntax — a uniform,
16
+ * functional approach to data mapping and transformation. Every
17
+ * ECMAScript operation is expressed as a **function with inputs**,
18
+ * eliminating the syntactic variability of the language (ternaries,
19
+ * property access, instance methods) in favor of a single,
20
+ * composable pattern.
21
+ *
22
+ * ## Postfix notation (Reverse Polish Notation)
23
+ *
24
+ * If you've used an RPN calculator, `@pipe` will feel familiar.
25
+ * Operands come first, then the operator consumes them:
26
+ *
27
+ * ```
28
+ * RPN: 4 4 + → 8
29
+ *
30
+ * @pipe:
31
+ * - [4, 4] ← operands
32
+ * - ["{@math.add}"] ← operator → 8
33
+ * ```
34
+ *
35
+ * That's the entire model. Operands are pushed, an operator pops
36
+ * them, and the result becomes an operand for the next row.
37
+ *
38
+ * ## How it works — row by row
39
+ *
40
+ * A pipe is an ordered array of **rows**. Each row is an array of
41
+ * **cells**. The key rule: **every resolved value on a row flows
42
+ * into cell 0 of the next row** (the operator). The operator
43
+ * consumes all upstream operands, resolves to a new value, and
44
+ * that result *becomes an operand*. Any remaining cells on the
45
+ * same row are resolved independently and appended as additional
46
+ * operands. Then the cycle repeats.
47
+ *
48
+ * ```
49
+ * @pipe:
50
+ * ┌──────────────────────────────────────┐
51
+ * │ Row 0: [operand, operand, operand] │ ← resolve all cells
52
+ * │ │ │ │ │
53
+ * │ └────────┼────────┘ │
54
+ * │ │ │
55
+ * ├─────────────────────┼────────────────┤
56
+ * │ ▼ │
57
+ * │ Row 1: [ OPERATOR , operand] │ ← ALL operands from Row 0
58
+ * │ ▲ receives all ▲ │ flow into cell 0;
59
+ * │ │ from above │ │ result becomes operand;
60
+ * │ └──────────────┘ │ remaining cells resolve
61
+ * │ │ │ │
62
+ * │ └────────────┘ │
63
+ * │ │ │
64
+ * ├─────────────────────┼────────────────┤
65
+ * │ ▼ │
66
+ * │ Row 2: [ OPERATOR , operand] │ ← same pattern repeats
67
+ * └──────────────────────────────────────┘
68
+ * ▼
69
+ * final value = cell 0 of last row
70
+ * ```
71
+ *
72
+ * Step by step:
73
+ *
74
+ * 1. **Row 0** — Every cell is resolved independently (static
75
+ * literals, `{data.*}` references, or nullary functions like
76
+ * `{@date.now}`). The resolved values are all **operands**.
77
+ * 2. **Row 1** — Cell 0 is the **operator** (`{@domain.method}`).
78
+ * It receives *all* operands from Row 0 as its arguments and
79
+ * produces a result. That result replaces cell 0 — it is now
80
+ * an operand. Any remaining cells (cell 1, 2, ...) on this row
81
+ * are resolved independently and become additional operands.
82
+ * 3. **Row 2** — Cell 0 is the next operator. It receives *all*
83
+ * operands from Row 1 (the prior result + any extra cells).
84
+ * The cycle repeats.
85
+ * 4. **Return** — The first cell of the final row is the result.
86
+ *
87
+ * When an additional cell on an operator row needs computed (not
88
+ * just a static value or simple reference), use a **nested
89
+ * `@pipe`** (sub-pipe) to resolve it. See Example 4 below.
90
+ *
91
+ * ## Three types of cell values
92
+ *
93
+ * | Type | Syntax | Example |
94
+ * |------|--------|---------|
95
+ * | **Static** | literal | `42`, `"hello"`, `true` |
96
+ * | **Dynamic** | `{path}` | `{a.output.data.name}` |
97
+ * | **Function** | `{@domain.method}` | `{@string.concat}`, `{@math.add}` |
98
+ *
99
+ * ## Available function domains
100
+ *
101
+ * - **array** — `get`, `length`, `join`, `concat`, `push`, `indexOf`, ...
102
+ * - **bitwise** — `and`, `or`, `xor`, `not`, ...
103
+ * - **conditional** — `ternary`, `equality`, `nullish`, ...
104
+ * - **cron** — `nextDelay`
105
+ * - **date** — `now`, `toLocaleString`, `yyyymmdd`, ...
106
+ * - **json** — `parse`, `stringify`
107
+ * - **logical** — `and`, `or`, `not`
108
+ * - **math** — `add`, `multiply`, `pow`, `max`, `min`, `abs`, ...
109
+ * - **number** — `isEven`, `isOdd`, `gte`, `lte`, ...
110
+ * - **object** — `create`, `get`, `set`, `keys`, ...
111
+ * - **string** — `concat`, `split`, `charAt`, `toLowerCase`, `includes`, ...
112
+ * - **symbol**
113
+ * - **unary**
114
+ *
115
+ * ---
116
+ *
117
+ * ## Example 1 — Simple field mapping (YAML)
118
+ *
119
+ * Most fields need only a one-to-one reference. Curly braces pull
120
+ * values from upstream activity outputs:
121
+ *
122
+ * ```yaml
123
+ * # Map fields from activities a and b into a new shape
124
+ * maps:
125
+ * first: "{a.output.data.first_name}"
126
+ * last: "{a.output.data.last_name}"
127
+ * email: "{b.output.data.email}"
128
+ * age: "{b.output.data.age}"
129
+ * company: "ACME Corp" # static string
130
+ * bonus: 500 # static number
131
+ * ```
132
+ *
133
+ * The equivalent JavaScript object passed to the mapper:
134
+ *
135
+ * ```typescript
136
+ * const rules = {
137
+ * first: '{a.output.data.first_name}',
138
+ * last: '{a.output.data.last_name}',
139
+ * email: '{b.output.data.email}',
140
+ * age: '{b.output.data.age}',
141
+ * company: 'ACME Corp',
142
+ * bonus: 500,
143
+ * };
144
+ * ```
145
+ *
146
+ * ## Example 2 — String transformation (YAML → JS)
147
+ *
148
+ * Build a `user_name` like `jdoe` from "John Doe". Follow the
149
+ * RPN flow — operands feed into the operator on the next row,
150
+ * the result becomes an operand, extra cells append more operands:
151
+ *
152
+ * ```yaml
153
+ * user_name:
154
+ * "@pipe":
155
+ * - ["{a.output.data.first_name}", 0]
156
+ * - ["{@string.charAt}", "{a.output.data.last_name}"]
157
+ * - ["{@string.concat}"]
158
+ * - ["{@string.toLowerCase}"]
159
+ * ```
160
+ *
161
+ * ```typescript
162
+ * // Identical logic in JavaScript
163
+ * const rules = {
164
+ * user_name: {
165
+ * '@pipe': [
166
+ * ['{a.output.data.first_name}', 0],
167
+ * ['{@string.charAt}', '{a.output.data.last_name}'],
168
+ * ['{@string.concat}'],
169
+ * ['{@string.toLowerCase}'],
170
+ * ],
171
+ * },
172
+ * };
173
+ * ```
174
+ *
175
+ * RPN trace (given first_name="John", last_name="Doe"):
176
+ *
177
+ * ```
178
+ * Row 0: ["John", 0] ← two operands
179
+ * Row 1: charAt("John", 0)="J" ← operator consumes both → result "J"
180
+ * then resolve "Doe" ← extra cell → operands are ["J", "Doe"]
181
+ * Row 2: concat("J", "Doe") ← operator → result "JDoe"
182
+ * operands are ["JDoe"]
183
+ * Row 3: toLowerCase("JDoe") ← operator → result "jdoe"
184
+ * ```
185
+ *
186
+ * ## Example 3 — Conditional logic (YAML → JS)
187
+ *
188
+ * Classify an employee as "Senior" or "Junior" based on age:
189
+ *
190
+ * ```yaml
191
+ * status:
192
+ * "@pipe":
193
+ * - ["{b.output.data.age}", 40]
194
+ * - ["{@number.gte}", "Senior", "Junior"]
195
+ * - ["{@conditional.ternary}"]
196
+ * ```
197
+ *
198
+ * ```typescript
199
+ * const rules = {
200
+ * status: {
201
+ * '@pipe': [
202
+ * ['{b.output.data.age}', 40],
203
+ * ['{@number.gte}', 'Senior', 'Junior'],
204
+ * ['{@conditional.ternary}'],
205
+ * ],
206
+ * },
207
+ * };
208
+ * ```
209
+ *
210
+ * RPN trace (given age=30):
211
+ *
212
+ * ```
213
+ * Row 0: [30, 40] ← two operands
214
+ * Row 1: gte(30, 40)=false ← operator consumes both → false
215
+ * resolve "Senior", "Junior" ← extra cells → [false, "Senior", "Junior"]
216
+ * Row 2: ternary(false, "Senior", "Junior") ← operator → "Junior"
217
+ * ```
218
+ *
219
+ * ## Example 4 — Nested pipes (fan-out / fan-in)
220
+ *
221
+ * Extract initials from a full name. Each nested `@pipe` runs
222
+ * independently (fan-out), then the first standard row after them
223
+ * receives all sub-pipe results as inputs (fan-in):
224
+ *
225
+ * ```yaml
226
+ * initials:
227
+ * "@pipe":
228
+ * - ["{a.output.data.full_name}", " "]
229
+ * - "@pipe": # fan-out: first initial
230
+ * - ["{@string.split}", 0]
231
+ * - ["{@array.get}", 0]
232
+ * - ["{@string.charAt}"]
233
+ * - "@pipe": # fan-out: last initial
234
+ * - ["{@string.split}", 1]
235
+ * - ["{@array.get}", 0]
236
+ * - ["{@string.charAt}"]
237
+ * - ["{@string.concat}"] # fan-in: combine both
238
+ * ```
239
+ *
240
+ * ```typescript
241
+ * const rules = {
242
+ * initials: {
243
+ * '@pipe': [
244
+ * ['{a.output.data.full_name}', ' '],
245
+ * { '@pipe': [['{@string.split}', 0], ['{@array.get}', 0], ['{@string.charAt}']] },
246
+ * { '@pipe': [['{@string.split}', 1], ['{@array.get}', 0], ['{@string.charAt}']] },
247
+ * ['{@string.concat}'],
248
+ * ],
249
+ * },
250
+ * };
251
+ * // "Luke Birdeau" → split → ["Luke","Birdeau"]
252
+ * // sub-pipe 1: get(0) → "Luke" → charAt(0) → "L"
253
+ * // sub-pipe 2: get(1) → "Birdeau" → charAt(0) → "B"
254
+ * // fan-in: concat("L", "B") → "LB"
255
+ * ```
256
+ *
257
+ * ## Example 5 — Reduce (iterate and accumulate)
258
+ *
259
+ * Transform an array of `{ full_name }` objects into
260
+ * `{ first, last }` pairs using `@reduce`. Context variables
261
+ * `{$item}`, `{$key}`, `{$index}`, `{$input}`, and `{$output}`
262
+ * are available inside the reducer body:
263
+ *
264
+ * ```typescript
265
+ * const jobData = {
266
+ * a: { output: { data: [
267
+ * { full_name: 'Luke Birdeau' },
268
+ * { full_name: 'John Doe' },
269
+ * ]}}
270
+ * };
271
+ *
272
+ * const rules = [
273
+ * ['{a.output.data}', []], // input array + initial accumulator
274
+ * { '@reduce': [
275
+ * { '@pipe': [['{$output}']] }, // carry forward accumulator
276
+ * { '@pipe': [
277
+ * { '@pipe': [['first']] },
278
+ * { '@pipe': [
279
+ * ['{$item.full_name}', ' '],
280
+ * ['{@string.split}', 0],
281
+ * ['{@array.get}'],
282
+ * ]},
283
+ * { '@pipe': [['last']] },
284
+ * { '@pipe': [
285
+ * ['{$item.full_name}', ' '],
286
+ * ['{@string.split}', 1],
287
+ * ['{@array.get}'],
288
+ * ]},
289
+ * ['{@object.create}'],
290
+ * ]},
291
+ * ['{@array.push}'],
292
+ * ]},
293
+ * ];
294
+ * // => [{ first: 'Luke', last: 'Birdeau' }, { first: 'John', last: 'Doe' }]
295
+ * ```
296
+ *
297
+ * ## Example 6 — Transition conditions
298
+ *
299
+ * Pipes power conditional transitions between workflow activities.
300
+ * In YAML, a transition fires only when the `@pipe` resolves to the
301
+ * `expected` value:
302
+ *
303
+ * ```yaml
304
+ * transitions:
305
+ * t1:
306
+ * - to: a1
307
+ * conditions:
308
+ * match:
309
+ * - expected: false
310
+ * actual:
311
+ * "@pipe":
312
+ * - ["{t1.output.data.a}", "goodbye"]
313
+ * - ["{@conditional.equality}"]
314
+ * ```
315
+ *
316
+ * ## Example 7 — Inline YAML with HotMesh.deploy
317
+ *
318
+ * HotMesh ships with an inline YAML parser, so a complete
319
+ * workflow with data mapping can be deployed in a single call:
320
+ *
321
+ * ```typescript
322
+ * await hotMesh.deploy(`
323
+ * app:
324
+ * id: myapp
325
+ * version: '1'
326
+ * graphs:
327
+ * - subscribes: order.process
328
+ * activities:
329
+ * t1:
330
+ * type: trigger
331
+ * a1:
332
+ * type: worker
333
+ * topic: inventory.check
334
+ * input:
335
+ * maps:
336
+ * itemId: "{t1.output.data.itemId}"
337
+ * output:
338
+ * schema:
339
+ * type: object
340
+ * job:
341
+ * maps:
342
+ * result: "{$self.output.data.available}"
343
+ * transitions:
344
+ * t1:
345
+ * - to: a1
346
+ * `);
347
+ *
348
+ * await hotMesh.activate('1');
349
+ * const response = await hotMesh.pubsub('order.process', { itemId: 'sku-42' });
350
+ * ```
351
+ */
8
352
  class Pipe {
353
+ /**
354
+ * @param rules - The ordered row array defining the pipeline.
355
+ * @param jobData - The current job data used to resolve `{data.*}`
356
+ * and `{activity.*}` references.
357
+ * @param context - Optional iteration context (`$item`, `$key`,
358
+ * `$output`, etc.) supplied during `@reduce` execution.
359
+ */
9
360
  constructor(rules, jobData, context) {
10
361
  this.rules = rules;
11
362
  this.jobData = jobData;
@@ -17,12 +368,32 @@ class Pipe {
17
368
  isreduceType(currentRow) {
18
369
  return !Array.isArray(currentRow) && '@reduce' in currentRow;
19
370
  }
371
+ /**
372
+ * Returns `true` if the value is a `@pipe` object (i.e. `{ '@pipe': [...] }`).
373
+ *
374
+ * @param obj - The value to test.
375
+ */
20
376
  static isPipeObject(obj) {
21
377
  return (typeof obj === 'object' &&
22
378
  obj !== null &&
23
379
  !Array.isArray(obj) &&
24
380
  '@pipe' in obj);
25
381
  }
382
+ /**
383
+ * One-shot convenience method that resolves a single value or
384
+ * `@pipe` expression against the given context.
385
+ *
386
+ * @param unresolved - A literal value, a `{data.*}` reference string,
387
+ * or a `@pipe` object.
388
+ * @param context - Partial {@link JobState} used for resolution.
389
+ * @returns The fully resolved value.
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * Pipe.resolve('{data.user.email}', jobState);
394
+ * Pipe.resolve({ '@pipe': [['{data.a}', '{data.b}'], ['{@math.multiply}']] }, jobState);
395
+ * ```
396
+ */
26
397
  static resolve(unresolved, context) {
27
398
  let pipe;
28
399
  if (Pipe.isPipeObject(unresolved)) {
@@ -34,8 +405,29 @@ class Pipe {
34
405
  return pipe.process();
35
406
  }
36
407
  /**
37
- * loop through each PipeItem row in this Pipe, resolving and transforming line by line
38
- * @returns {any} the result of the pipe
408
+ * Executes the pipeline row-by-row, resolving and transforming
409
+ * until the final value is produced.
410
+ *
411
+ * @remarks
412
+ * Row 0 is resolved independently (values only). Each subsequent
413
+ * row feeds the prior row's resolved output as arguments to the
414
+ * function named in cell 0. Nested `@pipe` rows are queued
415
+ * (fan-out) and collected into the next standard row (fan-in).
416
+ * `@reduce` rows iterate over the prior row's array output.
417
+ *
418
+ * @param resolved - Optional pre-resolved seed values (used
419
+ * internally by `reduce` to inject the accumulator).
420
+ * @returns The first cell of the final resolved row.
421
+ *
422
+ * @example
423
+ * ```typescript
424
+ * // Split a full name and grab the first element
425
+ * const pipe = new Pipe(
426
+ * [['{a.output.data.full_name}', ' '], ['{@string.split}', 0], ['{@array.get}']],
427
+ * { a: { output: { data: { full_name: 'Luke Birdeau' } } } },
428
+ * );
429
+ * pipe.process(); // => 'Luke'
430
+ * ```
39
431
  */
40
432
  process(resolved = null) {
41
433
  let index = 0;
@@ -74,14 +466,26 @@ class Pipe {
74
466
  return clonedObj;
75
467
  }
76
468
  /**
77
- * Transforms iterable `input` into a single value. Vars $output, $item, $key
78
- * and $input are available. The final statement in the iterator (the reduction)
79
- * is assumed to be the return value. A default $output object may be provided
80
- * to the iterator by placing the the second cell of the preceding row. Otherwise,
81
- * construct the object during first run and ensure it is the first cell of the
82
- * last row of the iterator, so it is returned as the $output for the next cycle
83
- * @param {unknown[]} input
84
- * @returns {unknown}
469
+ * Iterates over an array or object and accumulates a result,
470
+ * similar to `Array.prototype.reduce`.
471
+ *
472
+ * @remarks
473
+ * Inside the reducer body the following context variables are
474
+ * available as cell references:
475
+ *
476
+ * | Variable | Description |
477
+ * |----------|-------------|
478
+ * | `{$input}` | The full input collection |
479
+ * | `{$output}` | The current accumulator |
480
+ * | `{$item}` | The current element |
481
+ * | `{$key}` | The current key (string for objects, index string for arrays) |
482
+ * | `{$index}` | The current numeric index |
483
+ *
484
+ * The preceding row supplies `[inputCollection, initialAccumulator]`.
485
+ * If no initial accumulator is provided, it defaults to `null`.
486
+ *
487
+ * @param input - A two-element array: `[collection, initialAccumulator]`.
488
+ * @returns The final accumulated value, wrapped in an array.
85
489
  * @private
86
490
  */
87
491
  reduce(input) {
@@ -152,6 +556,14 @@ class Pipe {
152
556
  }
153
557
  }
154
558
  }
559
+ /**
560
+ * Looks up a domain function by its `{@domain.method}` name string
561
+ * and returns the callable.
562
+ *
563
+ * @param functionName - A string like `{@math.add}` or `{@string.concat}`.
564
+ * @returns The resolved function reference.
565
+ * @throws If the domain or method is not registered.
566
+ */
155
567
  static resolveFunction(functionName) {
156
568
  let [prefix, suffix] = functionName.split('.');
157
569
  prefix = prefix.substring(2);
@@ -165,6 +577,14 @@ class Pipe {
165
577
  }
166
578
  return domain[suffix];
167
579
  }
580
+ /**
581
+ * Resolves every cell in a single row independently — each cell
582
+ * is evaluated for function calls, context variables, or mappable
583
+ * references.
584
+ *
585
+ * @param cells - The array of {@link PipeItems} to resolve.
586
+ * @returns An array of resolved values, one per input cell.
587
+ */
168
588
  processCells(cells) {
169
589
  const resolved = [];
170
590
  if (Array.isArray(cells)) {
@@ -193,6 +613,14 @@ class Pipe {
193
613
  currentCell.startsWith('{') &&
194
614
  currentCell.endsWith('}'));
195
615
  }
616
+ /**
617
+ * Resolves a single cell value by detecting its type — function
618
+ * call (`{@domain.fn}`), context variable (`{$item}`, `{$key}`,
619
+ * etc.), mappable reference (`{data.*}`), or literal.
620
+ *
621
+ * @param currentCell - The cell to resolve.
622
+ * @returns The resolved runtime value.
623
+ */
196
624
  resolveCellValue(currentCell) {
197
625
  if (this.isFunction(currentCell)) {
198
626
  const fn = Pipe.resolveFunction(currentCell);
@@ -221,6 +649,12 @@ class Pipe {
221
649
  }
222
650
  return current;
223
651
  }
652
+ /**
653
+ * Resolves a `{data.*}` or `{activity.*}` reference by walking
654
+ * the dot-delimited path into {@link jobData}.
655
+ *
656
+ * @param currentCell - The mappable reference string (e.g. `{data.user.name}`).
657
+ */
224
658
  resolveMappableValue(currentCell) {
225
659
  const term = this.resolveMapTerm(currentCell);
226
660
  return this.getNestedProperty(this.jobData, term);
@@ -23,6 +23,7 @@ declare class StreamConsumerRegistry {
23
23
  }, logger: ILogger, config?: {
24
24
  reclaimDelay?: number;
25
25
  reclaimCount?: number;
26
+ readonly?: boolean;
26
27
  retry?: any;
27
28
  }): Promise<void>;
28
29
  /**
@@ -29,6 +29,7 @@ class StreamConsumerRegistry {
29
29
  topic: taskQueue,
30
30
  reclaimDelay: config?.reclaimDelay,
31
31
  reclaimCount: config?.reclaimCount,
32
+ readonly: config?.readonly || false,
32
33
  throttle,
33
34
  retry: config?.retry,
34
35
  }, stream, logger);
@@ -39,14 +40,17 @@ class StreamConsumerRegistry {
39
40
  logger,
40
41
  };
41
42
  StreamConsumerRegistry.workerConsumers.set(key, entry);
42
- // Create the dispatch callback that routes by workflow_name
43
- const dispatchCallback = StreamConsumerRegistry.createWorkerDispatcher(key);
44
- // Start consuming from the task queue stream
45
- const streamKey = stream.mintKey(key_1.KeyType.STREAMS, {
46
- appId,
47
- topic: taskQueue,
48
- });
49
- router.consumeMessages(streamKey, 'WORKER', guid, dispatchCallback);
43
+ // Only start consuming if not readonly
44
+ if (!config?.readonly) {
45
+ // Create the dispatch callback that routes by workflow_name
46
+ const dispatchCallback = StreamConsumerRegistry.createWorkerDispatcher(key);
47
+ // Start consuming from the task queue stream
48
+ const streamKey = stream.mintKey(key_1.KeyType.STREAMS, {
49
+ appId,
50
+ topic: taskQueue,
51
+ });
52
+ router.consumeMessages(streamKey, 'WORKER', guid, dispatchCallback);
53
+ }
50
54
  }
51
55
  // Register the callback for this workflow name
52
56
  entry.callbacks.set(workflowName, callback);
@@ -51,6 +51,7 @@ class WorkerService {
51
51
  await registry_1.StreamConsumerRegistry.registerWorker(namespace, appId, guid, worker.topic, worker.workflowName, worker.callback, service.stream, service.store, logger, {
52
52
  reclaimDelay: worker.reclaimDelay,
53
53
  reclaimCount: worker.reclaimCount,
54
+ readonly: worker.readonly,
54
55
  retry: worker.retry,
55
56
  });
56
57
  // Still need a router for publishing responses back to engine
@@ -114,6 +115,7 @@ class WorkerService {
114
115
  reclaimDelay: worker.reclaimDelay,
115
116
  reclaimCount: worker.reclaimCount,
116
117
  throttle,
118
+ readonly: worker.readonly || false,
117
119
  retry: worker.retry,
118
120
  }, this.stream, logger);
119
121
  }
@@ -284,6 +284,14 @@ type HotMeshWorker = {
284
284
  user: string;
285
285
  password: string;
286
286
  };
287
+ /**
288
+ * If true, the worker's router will not consume messages from the
289
+ * stream. The worker can still publish responses but will never
290
+ * dequeue or process messages. This is inherited from the
291
+ * connection's `readonly` flag by the Durable layer.
292
+ * @default false
293
+ */
294
+ readonly?: boolean;
287
295
  };
288
296
  type HotMeshConfig = {
289
297
  appId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.14.3",
3
+ "version": "0.14.4",
4
4
  "description": "Durable Workflow",
5
5
  "main": "./build/index.js",
6
6
  "types": "./build/index.d.ts",
@@ -14,8 +14,8 @@
14
14
  "obfuscate": "ts-node scripts/obfuscate.ts",
15
15
  "clean-build": "npm run clean && npm run build",
16
16
  "clean-build-obfuscate": "npm run clean-build && npm run obfuscate",
17
- "docs": "typedoc",
18
- "docs:clean": "rimraf ./docs/hotmesh && typedoc",
17
+ "docs": "typedoc && cp -R docs/hotmesh/* docs/ && rm -rf docs/hotmesh",
18
+ "docs:clean": "rimraf ./docs/hotmesh && typedoc && cp -R docs/hotmesh/* docs/ && rm -rf docs/hotmesh",
19
19
  "lint": "eslint . --ext .ts",
20
20
  "lint:fix": "eslint . --fix --ext .ts",
21
21
  "start": "ts-node src/index.ts",
@@ -47,6 +47,7 @@
47
47
  "test:durable:retrypolicy": "vitest run tests/durable/retry-policy",
48
48
  "test:durable:sleep": "vitest run tests/durable/sleep/postgres.test.ts",
49
49
  "test:durable:signal": "vitest run tests/durable/signal/postgres.test.ts",
50
+ "test:durable:readonly": "docker compose --profile readonly up -d --build && docker compose exec hotmesh-readonly npx vitest run --config tests/durable/readonly/vitest.config.mts",
50
51
  "test:durable:unknown": "vitest run tests/durable/unknown/postgres.test.ts",
51
52
  "test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
52
53
  "test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
package/vitest.config.mts CHANGED
@@ -5,7 +5,7 @@ export default defineConfig({
5
5
  globals: true,
6
6
  environment: 'node',
7
7
  include: ['tests/**/*.test.ts'],
8
- exclude: ['node_modules', 'build', 'config'],
8
+ exclude: ['node_modules', 'build', 'config', 'tests/durable/readonly/**'],
9
9
  testTimeout: 60_000,
10
10
  hookTimeout: 120_000,
11
11
  fileParallelism: false,