@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.
- package/build/package.json +4 -3
- package/build/services/durable/worker.js +4 -0
- package/build/services/mapper/index.d.ts +57 -2
- package/build/services/mapper/index.js +57 -2
- package/build/services/pipe/index.d.ts +444 -10
- package/build/services/pipe/index.js +444 -10
- package/build/services/stream/registry.d.ts +1 -0
- package/build/services/stream/registry.js +12 -8
- package/build/services/worker/index.js +2 -0
- package/build/types/hotmesh.d.ts +8 -0
- package/package.json +4 -3
- package/vitest.config.mts +1 -1
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.14.
|
|
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
|
|
24
|
-
*
|
|
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
|
|
50
|
-
*
|
|
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
|
-
*
|
|
18
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
38
|
-
*
|
|
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
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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);
|
|
@@ -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
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
}
|
package/build/types/hotmesh.d.ts
CHANGED
|
@@ -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
|
+
"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,
|