@hotmeshio/hotmesh 0.14.2 → 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.
Files changed (32) hide show
  1. package/build/package.json +5 -3
  2. package/build/services/durable/worker.js +4 -0
  3. package/build/services/engine/init.js +1 -1
  4. package/build/services/engine/schema.js +5 -1
  5. package/build/services/mapper/index.d.ts +57 -2
  6. package/build/services/mapper/index.js +57 -2
  7. package/build/services/pipe/index.d.ts +444 -10
  8. package/build/services/pipe/index.js +444 -10
  9. package/build/services/quorum/index.js +1 -1
  10. package/build/services/router/consumption/index.js +20 -2
  11. package/build/services/router/error-handling/index.js +1 -1
  12. package/build/services/store/factory.d.ts +1 -1
  13. package/build/services/store/factory.js +2 -2
  14. package/build/services/store/index.d.ts +1 -1
  15. package/build/services/store/providers/postgres/kvsql.d.ts +11 -1
  16. package/build/services/store/providers/postgres/kvsql.js +22 -12
  17. package/build/services/store/providers/postgres/kvtables.js +39 -6
  18. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +6 -6
  19. package/build/services/store/providers/postgres/kvtypes/hash/scan.js +2 -1
  20. package/build/services/store/providers/postgres/kvtypes/list.js +7 -6
  21. package/build/services/store/providers/postgres/kvtypes/string.js +3 -3
  22. package/build/services/store/providers/postgres/kvtypes/zset.js +7 -7
  23. package/build/services/store/providers/postgres/postgres.d.ts +3 -2
  24. package/build/services/store/providers/postgres/postgres.js +55 -55
  25. package/build/services/store/providers/postgres/time-notify.js +18 -25
  26. package/build/services/store/providers/store-initializable.d.ts +1 -1
  27. package/build/services/stream/registry.d.ts +1 -0
  28. package/build/services/stream/registry.js +12 -8
  29. package/build/services/worker/index.js +3 -1
  30. package/build/types/hotmesh.d.ts +8 -0
  31. package/package.json +5 -3
  32. package/vitest.config.mts +1 -1
@@ -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;