@hotmeshio/hotmesh 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +8 -0
- package/README.md +158 -38
- package/build/index.d.ts +1 -3
- package/build/index.js +1 -5
- package/build/modules/utils.js +3 -31
- package/build/package.json +63 -79
- package/build/services/activities/activity.d.ts +97 -9
- package/build/services/activities/activity.js +323 -86
- package/build/services/activities/await.d.ts +101 -0
- package/build/services/activities/await.js +103 -2
- package/build/services/activities/cycle.d.ts +82 -0
- package/build/services/activities/cycle.js +86 -8
- package/build/services/activities/hook.d.ts +144 -1
- package/build/services/activities/hook.js +162 -21
- package/build/services/activities/interrupt.d.ts +112 -0
- package/build/services/activities/interrupt.js +134 -29
- package/build/services/activities/signal.d.ts +111 -4
- package/build/services/activities/signal.js +136 -28
- package/build/services/activities/trigger.d.ts +56 -4
- package/build/services/activities/trigger.js +119 -35
- package/build/services/activities/worker.d.ts +107 -0
- package/build/services/activities/worker.js +109 -2
- package/build/services/collator/index.d.ts +116 -30
- package/build/services/collator/index.js +211 -115
- package/build/services/connector/factory.d.ts +1 -1
- package/build/services/connector/factory.js +1 -11
- package/build/services/engine/index.d.ts +22 -6
- package/build/services/engine/index.js +49 -18
- package/build/services/exporter/index.d.ts +2 -0
- package/build/services/exporter/index.js +1 -0
- package/build/services/hotmesh/index.d.ts +471 -236
- package/build/services/hotmesh/index.js +473 -238
- package/build/services/memflow/client.js +2 -2
- package/build/services/memflow/handle.js +1 -1
- package/build/services/memflow/index.d.ts +1 -1
- package/build/services/memflow/index.js +1 -1
- package/build/services/memflow/workflow/all.d.ts +28 -3
- package/build/services/memflow/workflow/all.js +28 -3
- package/build/services/memflow/workflow/context.d.ts +44 -1
- package/build/services/memflow/workflow/context.js +44 -1
- package/build/services/memflow/workflow/didRun.d.ts +23 -3
- package/build/services/memflow/workflow/didRun.js +23 -3
- package/build/services/memflow/workflow/emit.d.ts +43 -4
- package/build/services/memflow/workflow/emit.js +43 -4
- package/build/services/memflow/workflow/enrich.d.ts +32 -4
- package/build/services/memflow/workflow/enrich.js +32 -4
- package/build/services/memflow/workflow/entityMethods.d.ts +54 -7
- package/build/services/memflow/workflow/entityMethods.js +54 -7
- package/build/services/memflow/workflow/execChild.d.ts +96 -8
- package/build/services/memflow/workflow/execChild.js +96 -8
- package/build/services/memflow/workflow/execHook.d.ts +54 -39
- package/build/services/memflow/workflow/execHook.js +52 -38
- package/build/services/memflow/workflow/execHookBatch.d.ts +82 -29
- package/build/services/memflow/workflow/execHookBatch.js +80 -28
- package/build/services/memflow/workflow/hook.d.ts +68 -3
- package/build/services/memflow/workflow/hook.js +69 -4
- package/build/services/memflow/workflow/index.d.ts +65 -10
- package/build/services/memflow/workflow/index.js +65 -10
- package/build/services/memflow/workflow/interrupt.d.ts +50 -4
- package/build/services/memflow/workflow/interrupt.js +50 -4
- package/build/services/memflow/workflow/interruption.d.ts +49 -16
- package/build/services/memflow/workflow/interruption.js +49 -16
- package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +21 -4
- package/build/services/memflow/workflow/isSideEffectAllowed.js +21 -4
- package/build/services/memflow/workflow/proxyActivities.d.ts +70 -42
- package/build/services/memflow/workflow/proxyActivities.js +70 -42
- package/build/services/memflow/workflow/random.d.ts +33 -3
- package/build/services/memflow/workflow/random.js +33 -3
- package/build/services/memflow/workflow/searchMethods.d.ts +49 -2
- package/build/services/memflow/workflow/searchMethods.js +49 -2
- package/build/services/memflow/workflow/signal.d.ts +51 -22
- package/build/services/memflow/workflow/signal.js +52 -23
- package/build/services/memflow/workflow/sleepFor.d.ts +57 -18
- package/build/services/memflow/workflow/sleepFor.js +57 -18
- package/build/services/memflow/workflow/trace.d.ts +39 -6
- package/build/services/memflow/workflow/trace.js +39 -6
- package/build/services/memflow/workflow/waitFor.d.ts +55 -18
- package/build/services/memflow/workflow/waitFor.js +55 -18
- package/build/services/router/consumption/index.js +1 -1
- package/build/services/search/factory.js +1 -9
- package/build/services/store/factory.js +1 -9
- package/build/services/store/index.d.ts +6 -1
- package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
- package/build/services/store/providers/postgres/kvsql.js +4 -0
- package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
- package/build/services/store/providers/postgres/kvtransaction.js +23 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
- package/build/services/store/providers/postgres/kvtypes/hash/basic.js +193 -1
- package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
- package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
- package/build/services/store/providers/postgres/postgres.d.ts +21 -1
- package/build/services/store/providers/postgres/postgres.js +42 -4
- package/build/services/stream/factory.js +1 -17
- package/build/services/stream/providers/postgres/scout.js +2 -2
- package/build/services/sub/factory.js +1 -9
- package/build/services/sub/index.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
- package/build/services/sub/providers/postgres/postgres.js +25 -10
- package/build/services/task/index.d.ts +1 -1
- package/build/services/task/index.js +2 -6
- package/build/services/telemetry/index.js +6 -0
- package/build/types/activity.d.ts +1 -1
- package/build/types/hotmesh.d.ts +1 -1
- package/build/types/index.d.ts +0 -1
- package/build/types/index.js +1 -4
- package/build/types/job.d.ts +1 -1
- package/build/types/memflow.d.ts +1 -1
- package/build/types/provider.d.ts +1 -1
- package/build/types/quorum.d.ts +2 -2
- package/build/vitest.config.d.ts +2 -0
- package/build/vitest.config.js +18 -0
- package/index.ts +0 -4
- package/package.json +63 -79
- package/vitest.config.ts +17 -0
- package/build/services/connector/providers/ioredis.d.ts +0 -9
- package/build/services/connector/providers/ioredis.js +0 -26
- package/build/services/connector/providers/redis.d.ts +0 -9
- package/build/services/connector/providers/redis.js +0 -38
- package/build/services/search/providers/redis/ioredis.d.ts +0 -23
- package/build/services/search/providers/redis/ioredis.js +0 -189
- package/build/services/search/providers/redis/redis.d.ts +0 -23
- package/build/services/search/providers/redis/redis.js +0 -202
- package/build/services/store/providers/redis/_base.d.ts +0 -137
- package/build/services/store/providers/redis/_base.js +0 -980
- package/build/services/store/providers/redis/ioredis.d.ts +0 -20
- package/build/services/store/providers/redis/ioredis.js +0 -190
- package/build/services/store/providers/redis/redis.d.ts +0 -18
- package/build/services/store/providers/redis/redis.js +0 -199
- package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
- package/build/services/stream/providers/redis/ioredis.js +0 -272
- package/build/services/stream/providers/redis/redis.d.ts +0 -61
- package/build/services/stream/providers/redis/redis.js +0 -305
- package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
- package/build/services/sub/providers/redis/ioredis.js +0 -161
- package/build/services/sub/providers/redis/redis.d.ts +0 -18
- package/build/services/sub/providers/redis/redis.js +0 -148
- package/build/types/redis.d.ts +0 -258
- package/build/types/redis.js +0 -11
|
@@ -7,6 +7,111 @@ const mapper_1 = require("../mapper");
|
|
|
7
7
|
const pipe_1 = require("../pipe");
|
|
8
8
|
const telemetry_1 = require("../telemetry");
|
|
9
9
|
const activity_1 = require("./activity");
|
|
10
|
+
/**
|
|
11
|
+
* Sends a signal to one or more paused flows, resuming their execution.
|
|
12
|
+
* The `signal` activity is the counterpart to a `Hook` activity
|
|
13
|
+
* configured with a webhook listener. It allows any flow to reach into
|
|
14
|
+
* another flow and deliver data to a waiting hook, regardless of the
|
|
15
|
+
* relationship between the flows.
|
|
16
|
+
*
|
|
17
|
+
* ## YAML Configuration — Signal One
|
|
18
|
+
*
|
|
19
|
+
* Resumes a single paused flow by publishing to the hook's topic. Use
|
|
20
|
+
* `subtype: one` when you know the specific hook topic to signal.
|
|
21
|
+
*
|
|
22
|
+
* ```yaml
|
|
23
|
+
* app:
|
|
24
|
+
* id: myapp
|
|
25
|
+
* version: '1'
|
|
26
|
+
* graphs:
|
|
27
|
+
* - subscribes: signal.start
|
|
28
|
+
* expire: 120
|
|
29
|
+
*
|
|
30
|
+
* activities:
|
|
31
|
+
* t1:
|
|
32
|
+
* type: trigger
|
|
33
|
+
*
|
|
34
|
+
* resume_hook:
|
|
35
|
+
* type: signal
|
|
36
|
+
* subtype: one
|
|
37
|
+
* topic: my.hook.topic # the hook's registered topic
|
|
38
|
+
* status: success # optional: success (default) or pending
|
|
39
|
+
* code: 200 # optional: 200 (default) or 202 (keep-alive)
|
|
40
|
+
* signal:
|
|
41
|
+
* schema:
|
|
42
|
+
* type: object
|
|
43
|
+
* properties:
|
|
44
|
+
* approved: { type: boolean }
|
|
45
|
+
* maps:
|
|
46
|
+
* approved: true # data delivered to the hook
|
|
47
|
+
*
|
|
48
|
+
* done:
|
|
49
|
+
* type: hook
|
|
50
|
+
*
|
|
51
|
+
* transitions:
|
|
52
|
+
* t1:
|
|
53
|
+
* - to: resume_hook
|
|
54
|
+
* resume_hook:
|
|
55
|
+
* - to: done
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* A `code: 202` signal delivers data but keeps the hook alive for
|
|
59
|
+
* additional signals. A `code: 200` (default) closes the hook.
|
|
60
|
+
*
|
|
61
|
+
* ## YAML Configuration — Signal All
|
|
62
|
+
*
|
|
63
|
+
* Resumes all paused flows that share a common job key facet. Use
|
|
64
|
+
* `subtype: all` for fan-out patterns where multiple waiting flows
|
|
65
|
+
* should be resumed simultaneously.
|
|
66
|
+
*
|
|
67
|
+
* ```yaml
|
|
68
|
+
* app:
|
|
69
|
+
* id: myapp
|
|
70
|
+
* version: '1'
|
|
71
|
+
* graphs:
|
|
72
|
+
* - subscribes: signal.fan.out
|
|
73
|
+
* expire: 120
|
|
74
|
+
*
|
|
75
|
+
* activities:
|
|
76
|
+
* t1:
|
|
77
|
+
* type: trigger
|
|
78
|
+
*
|
|
79
|
+
* resume_all:
|
|
80
|
+
* type: signal
|
|
81
|
+
* subtype: all
|
|
82
|
+
* topic: hook.resume
|
|
83
|
+
* key_name: parent_job_id # index facet name
|
|
84
|
+
* key_value: '{$job.metadata.jid}'
|
|
85
|
+
* scrub: true # clean up indexes after use
|
|
86
|
+
* resolver:
|
|
87
|
+
* maps:
|
|
88
|
+
* data:
|
|
89
|
+
* parent_job_id: '{$job.metadata.jid}'
|
|
90
|
+
* scrub: true
|
|
91
|
+
* signal:
|
|
92
|
+
* maps:
|
|
93
|
+
* done: true # data delivered to all matching hooks
|
|
94
|
+
*
|
|
95
|
+
* done:
|
|
96
|
+
* type: hook
|
|
97
|
+
*
|
|
98
|
+
* transitions:
|
|
99
|
+
* t1:
|
|
100
|
+
* - to: resume_all
|
|
101
|
+
* resume_all:
|
|
102
|
+
* - to: done
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* ## Execution Model
|
|
106
|
+
*
|
|
107
|
+
* Signal is a **Category B (Leg1-only with children)** activity:
|
|
108
|
+
* - Bundles the hook signal with the Leg 1 completion marker in a
|
|
109
|
+
* single transaction (`hookOne`) or fires best-effort (`hookAll`).
|
|
110
|
+
* - Executes the crash-safe `executeLeg1StepProtocol` to transition
|
|
111
|
+
* to adjacent activities.
|
|
112
|
+
*
|
|
113
|
+
* @see {@link SignalActivity} for the TypeScript interface
|
|
114
|
+
*/
|
|
10
115
|
class Signal extends activity_1.Activity {
|
|
11
116
|
constructor(config, data, metadata, hook, engine, context) {
|
|
12
117
|
super(config, data, metadata, hook, engine, context);
|
|
@@ -20,34 +125,36 @@ class Signal extends activity_1.Activity {
|
|
|
20
125
|
});
|
|
21
126
|
let telemetry;
|
|
22
127
|
try {
|
|
23
|
-
|
|
128
|
+
//Category B: entry with step resume
|
|
129
|
+
await this.verifyLeg1Entry();
|
|
24
130
|
telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
|
|
25
131
|
telemetry.startActivitySpan(this.leg);
|
|
26
|
-
//save state and notarize early completion (signals only run leg1)
|
|
27
|
-
const transaction = this.store.transact();
|
|
28
132
|
this.adjacencyList = await this.filterAdjacent();
|
|
29
133
|
this.mapOutputData();
|
|
30
134
|
this.mapJobData();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
135
|
+
//Step A: Bundle signal hook with Leg1 completion marker.
|
|
136
|
+
//hookOne is transactional; hookAll is best-effort (complex multi-step).
|
|
137
|
+
if (!collator_1.CollatorService.isGuidStep1Done(this.guidLedger)) {
|
|
138
|
+
const txn1 = this.store.transact();
|
|
139
|
+
if (this.config.subtype === 'all') {
|
|
140
|
+
await this.signalAll();
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
await this.signalOne(txn1);
|
|
144
|
+
}
|
|
145
|
+
await this.setState(txn1);
|
|
146
|
+
if (this.adjacentIndex === 0) {
|
|
147
|
+
await collator_1.CollatorService.notarizeLeg1Completion(this, txn1);
|
|
148
|
+
}
|
|
149
|
+
await collator_1.CollatorService.notarizeStep1(this, this.context.metadata.guid, txn1);
|
|
150
|
+
await txn1.exec();
|
|
151
|
+
//update in-memory guidLedger so executeLeg1StepProtocol skips Step A
|
|
152
|
+
this.guidLedger += collator_1.CollatorService.WEIGHTS.STEP1_WORK;
|
|
48
153
|
}
|
|
154
|
+
//Steps B and C: spawn children + semaphore + edge capture
|
|
155
|
+
await this.executeLeg1StepProtocol(this.adjacencyList.length - 1);
|
|
49
156
|
telemetry.mapActivityAttributes();
|
|
50
|
-
telemetry.setActivityAttributes(
|
|
157
|
+
telemetry.setActivityAttributes({});
|
|
51
158
|
return this.context.metadata.aid;
|
|
52
159
|
}
|
|
53
160
|
catch (error) {
|
|
@@ -102,19 +209,20 @@ class Signal extends activity_1.Activity {
|
|
|
102
209
|
}
|
|
103
210
|
}
|
|
104
211
|
/**
|
|
105
|
-
* The signal activity will hook one
|
|
212
|
+
* The signal activity will hook one. Accepts an optional transaction
|
|
213
|
+
* so the hook publish can be bundled with the Leg1 completion marker.
|
|
106
214
|
*/
|
|
107
|
-
async
|
|
215
|
+
async signalOne(transaction) {
|
|
108
216
|
const topic = pipe_1.Pipe.resolve(this.config.topic, this.context);
|
|
109
217
|
const signalInputData = this.mapSignalData();
|
|
110
218
|
const status = pipe_1.Pipe.resolve(this.config.status, this.context);
|
|
111
219
|
const code = pipe_1.Pipe.resolve(this.config.code, this.context);
|
|
112
|
-
return await this.engine.
|
|
220
|
+
return await this.engine.signal(topic, signalInputData, status, code, transaction);
|
|
113
221
|
}
|
|
114
222
|
/**
|
|
115
|
-
*
|
|
223
|
+
* Signals all paused jobs that share the same job key, resuming their execution.
|
|
116
224
|
*/
|
|
117
|
-
async
|
|
225
|
+
async signalAll() {
|
|
118
226
|
//prep 1) generate `input signal data` (essentially the webhook payload)
|
|
119
227
|
const signalInputData = this.mapSignalData();
|
|
120
228
|
//prep 2) generate data that resolves the job key (per the YAML config)
|
|
@@ -127,8 +235,8 @@ class Signal extends activity_1.Activity {
|
|
|
127
235
|
const key_name = pipe_1.Pipe.resolve(this.config.key_name, this.context);
|
|
128
236
|
const key_value = pipe_1.Pipe.resolve(this.config.key_value, this.context);
|
|
129
237
|
const indexQueryFacets = [`${key_name}:${key_value}`];
|
|
130
|
-
//execute: `
|
|
131
|
-
return await this.engine.
|
|
238
|
+
//execute: `signalAll` will now resume all paused jobs that share the same job key
|
|
239
|
+
return await this.engine.signalAll(this.config.topic, signalInputData, keyResolverData, indexQueryFacets);
|
|
132
240
|
}
|
|
133
241
|
}
|
|
134
242
|
exports.Signal = Signal;
|
|
@@ -1,14 +1,67 @@
|
|
|
1
1
|
import { EngineService } from '../engine';
|
|
2
2
|
import { ActivityData, ActivityMetadata, ActivityType, TriggerActivity } from '../../types/activity';
|
|
3
|
-
import { JobState, ExtensionType
|
|
3
|
+
import { JobState, ExtensionType } from '../../types/job';
|
|
4
4
|
import { ProviderTransaction } from '../../types/provider';
|
|
5
|
-
import { StringScalarType } from '../../types/serializer';
|
|
6
5
|
import { Activity } from './activity';
|
|
6
|
+
/**
|
|
7
|
+
* The entry point for every workflow graph. Each graph must have exactly
|
|
8
|
+
* one `trigger` activity, which executes when the graph's `subscribes`
|
|
9
|
+
* topic receives a message (via `hotMesh.pub` or `hotMesh.pubsub`).
|
|
10
|
+
*
|
|
11
|
+
* The trigger initializes the job, sets its unique ID and key, binds
|
|
12
|
+
* the incoming payload as the trigger's output data, and transitions
|
|
13
|
+
* to adjacent activities defined in the `transitions` section.
|
|
14
|
+
*
|
|
15
|
+
* ## YAML Configuration
|
|
16
|
+
*
|
|
17
|
+
* ```yaml
|
|
18
|
+
* app:
|
|
19
|
+
* id: myapp
|
|
20
|
+
* version: '1'
|
|
21
|
+
* graphs:
|
|
22
|
+
* - subscribes: order.placed # the trigger fires when this topic receives a message
|
|
23
|
+
* publishes: order.processed # emitted when the graph completes
|
|
24
|
+
* expire: 120
|
|
25
|
+
*
|
|
26
|
+
* activities:
|
|
27
|
+
* t1:
|
|
28
|
+
* type: trigger
|
|
29
|
+
* entity: '{$self.input.data.entityType}'
|
|
30
|
+
* job:
|
|
31
|
+
* maps:
|
|
32
|
+
* myField: '{$self.output.data.inputField}'
|
|
33
|
+
* stats:
|
|
34
|
+
* id: '{$self.input.data.workflowId}'
|
|
35
|
+
* key: '{$self.input.data.parentId}'
|
|
36
|
+
* parent: '{$self.input.data.originJobId}'
|
|
37
|
+
* adjacent: '{$self.input.data.parentJobId}'
|
|
38
|
+
*
|
|
39
|
+
* process:
|
|
40
|
+
* type: worker
|
|
41
|
+
* topic: order.process
|
|
42
|
+
*
|
|
43
|
+
* transitions:
|
|
44
|
+
* t1:
|
|
45
|
+
* - to: process
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* ## Key Behaviors
|
|
49
|
+
*
|
|
50
|
+
* - **Job ID Resolution**: If `stats.id` is provided, it resolves via `@pipe`
|
|
51
|
+
* expressions against the input data. Otherwise a UUID is generated.
|
|
52
|
+
* - **Duplicate Detection**: If a job with the same ID already exists,
|
|
53
|
+
* a `DuplicateJobError` is thrown (unless it's a crash-recovery scenario).
|
|
54
|
+
* - **Pending Mode**: When invoked with `{ pending: <seconds> }`, the trigger
|
|
55
|
+
* creates the job but does not transition to children until resumed.
|
|
56
|
+
* - **Crash Recovery**: Uses a 3-step inception protocol with GUID ledger
|
|
57
|
+
* to ensure atomic job creation survives process crashes.
|
|
58
|
+
*
|
|
59
|
+
* @see {@link TriggerActivity} for the TypeScript interface
|
|
60
|
+
*/
|
|
7
61
|
declare class Trigger extends Activity {
|
|
8
62
|
config: TriggerActivity;
|
|
9
63
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|
|
10
64
|
process(options?: ExtensionType): Promise<string>;
|
|
11
|
-
transitionAndLogAdjacent(options: ExtensionType, jobStatus: JobStatus, attrs: StringScalarType): Promise<void>;
|
|
12
65
|
/**
|
|
13
66
|
* `pending` flows will not transition from the trigger to adjacent children until resumed
|
|
14
67
|
*/
|
|
@@ -31,7 +84,6 @@ declare class Trigger extends Activity {
|
|
|
31
84
|
getJobStatus(): number;
|
|
32
85
|
resolveJobId(context: Partial<JobState>): string;
|
|
33
86
|
resolveJobKey(context: Partial<JobState>): string;
|
|
34
|
-
setStateNX(status?: number, entity?: string | undefined): Promise<void>;
|
|
35
87
|
setStats(transaction?: ProviderTransaction): Promise<void>;
|
|
36
88
|
}
|
|
37
89
|
export { Trigger };
|
|
@@ -10,6 +10,61 @@ const serializer_1 = require("../serializer");
|
|
|
10
10
|
const telemetry_1 = require("../telemetry");
|
|
11
11
|
const mapper_1 = require("../mapper");
|
|
12
12
|
const activity_1 = require("./activity");
|
|
13
|
+
/**
|
|
14
|
+
* The entry point for every workflow graph. Each graph must have exactly
|
|
15
|
+
* one `trigger` activity, which executes when the graph's `subscribes`
|
|
16
|
+
* topic receives a message (via `hotMesh.pub` or `hotMesh.pubsub`).
|
|
17
|
+
*
|
|
18
|
+
* The trigger initializes the job, sets its unique ID and key, binds
|
|
19
|
+
* the incoming payload as the trigger's output data, and transitions
|
|
20
|
+
* to adjacent activities defined in the `transitions` section.
|
|
21
|
+
*
|
|
22
|
+
* ## YAML Configuration
|
|
23
|
+
*
|
|
24
|
+
* ```yaml
|
|
25
|
+
* app:
|
|
26
|
+
* id: myapp
|
|
27
|
+
* version: '1'
|
|
28
|
+
* graphs:
|
|
29
|
+
* - subscribes: order.placed # the trigger fires when this topic receives a message
|
|
30
|
+
* publishes: order.processed # emitted when the graph completes
|
|
31
|
+
* expire: 120
|
|
32
|
+
*
|
|
33
|
+
* activities:
|
|
34
|
+
* t1:
|
|
35
|
+
* type: trigger
|
|
36
|
+
* entity: '{$self.input.data.entityType}'
|
|
37
|
+
* job:
|
|
38
|
+
* maps:
|
|
39
|
+
* myField: '{$self.output.data.inputField}'
|
|
40
|
+
* stats:
|
|
41
|
+
* id: '{$self.input.data.workflowId}'
|
|
42
|
+
* key: '{$self.input.data.parentId}'
|
|
43
|
+
* parent: '{$self.input.data.originJobId}'
|
|
44
|
+
* adjacent: '{$self.input.data.parentJobId}'
|
|
45
|
+
*
|
|
46
|
+
* process:
|
|
47
|
+
* type: worker
|
|
48
|
+
* topic: order.process
|
|
49
|
+
*
|
|
50
|
+
* transitions:
|
|
51
|
+
* t1:
|
|
52
|
+
* - to: process
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* ## Key Behaviors
|
|
56
|
+
*
|
|
57
|
+
* - **Job ID Resolution**: If `stats.id` is provided, it resolves via `@pipe`
|
|
58
|
+
* expressions against the input data. Otherwise a UUID is generated.
|
|
59
|
+
* - **Duplicate Detection**: If a job with the same ID already exists,
|
|
60
|
+
* a `DuplicateJobError` is thrown (unless it's a crash-recovery scenario).
|
|
61
|
+
* - **Pending Mode**: When invoked with `{ pending: <seconds> }`, the trigger
|
|
62
|
+
* creates the job but does not transition to children until resumed.
|
|
63
|
+
* - **Crash Recovery**: Uses a 3-step inception protocol with GUID ledger
|
|
64
|
+
* to ensure atomic job creation survives process crashes.
|
|
65
|
+
*
|
|
66
|
+
* @see {@link TriggerActivity} for the TypeScript interface
|
|
67
|
+
*/
|
|
13
68
|
class Trigger extends activity_1.Activity {
|
|
14
69
|
constructor(config, data, metadata, hook, engine, context) {
|
|
15
70
|
super(config, data, metadata, hook, engine, context);
|
|
@@ -30,40 +85,84 @@ class Trigger extends activity_1.Activity {
|
|
|
30
85
|
const initialStatus = this.initStatus(options, this.adjacencyList.length);
|
|
31
86
|
//config.entity is a pipe expression; if 'entity' exists, it will resolve
|
|
32
87
|
const resolvedEntity = new mapper_1.MapperService({ entity: this.config.entity }, this.context).mapRules()?.entity;
|
|
33
|
-
|
|
88
|
+
const msgGuid = this.context.metadata.guid || (0, utils_1.guid)();
|
|
89
|
+
this.context.metadata.guid = msgGuid;
|
|
90
|
+
const { id: appId } = await this.engine.getVID();
|
|
91
|
+
//═══ Step 1: Inception (atomic job creation + GUID seed) ═══
|
|
92
|
+
const txn1 = this.store.transact();
|
|
93
|
+
await this.store.setStateNX(this.context.metadata.jid, appId, initialStatus, options?.entity || resolvedEntity, txn1);
|
|
94
|
+
await this.store.collateSynthetic(this.context.metadata.jid, msgGuid, collator_1.CollatorService.WEIGHTS.STEP1_WORK, txn1);
|
|
95
|
+
const results1 = (await txn1.exec());
|
|
96
|
+
const jobCreated = Number(results1[0]) > 0;
|
|
97
|
+
const guidValue = Number(results1[1]);
|
|
98
|
+
if (!jobCreated) {
|
|
99
|
+
if (guidValue > collator_1.CollatorService.WEIGHTS.STEP1_WORK) {
|
|
100
|
+
//crash recovery: GUID was seeded on a prior attempt; resume
|
|
101
|
+
this.guidLedger = guidValue;
|
|
102
|
+
this.logger.info('trigger-crash-recovery', {
|
|
103
|
+
job_id: this.context.metadata.jid,
|
|
104
|
+
guid: msgGuid,
|
|
105
|
+
guidLedger: guidValue,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
//true duplicate: another process owns this job
|
|
110
|
+
throw new errors_1.DuplicateJobError(this.context.metadata.jid);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
34
113
|
await this.setStatus(initialStatus);
|
|
35
114
|
this.bindSearchData(options);
|
|
36
115
|
this.bindMarkerData(options);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
await this.
|
|
116
|
+
//═══ Step 2: Work (state + children + GUID Step 2) ═══
|
|
117
|
+
if (!collator_1.CollatorService.isGuidStep2Done(this.guidLedger)) {
|
|
118
|
+
const txn2 = this.store.transact();
|
|
119
|
+
await this.setState(txn2);
|
|
120
|
+
await this.setStats(txn2);
|
|
121
|
+
if (options?.pending) {
|
|
122
|
+
await this.setExpired(options.pending, txn2);
|
|
123
|
+
}
|
|
124
|
+
//publish children (unless pending or job already complete)
|
|
125
|
+
if (isNaN(options?.pending) && this.adjacencyList.length && initialStatus > 0) {
|
|
126
|
+
for (const child of this.adjacencyList) {
|
|
127
|
+
await this.engine.router?.publishMessage(null, child, txn2);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await collator_1.CollatorService.notarizeStep2(this, msgGuid, txn2);
|
|
131
|
+
await txn2.exec();
|
|
42
132
|
}
|
|
43
|
-
|
|
44
|
-
await transaction.exec();
|
|
133
|
+
//best-effort parent notification
|
|
45
134
|
this.execAdjacentParent();
|
|
46
|
-
|
|
135
|
+
//═══ Step 3: Completion (if job immediately complete) ═══
|
|
136
|
+
//NOTE: runJobCompletionTasks is non-transactional here because
|
|
137
|
+
//the trigger runs inline within pub(). Subscribers register AFTER
|
|
138
|
+
//pub() returns, so pub notifications must be fire-and-forget to
|
|
139
|
+
//avoid a race. The GUID marker commits in its own transaction.
|
|
47
140
|
const jobStatus = Number(this.context.metadata.js);
|
|
141
|
+
const needsCompletion = this.shouldEmit() ||
|
|
142
|
+
this.isJobComplete(jobStatus) ||
|
|
143
|
+
this.shouldPersistJob();
|
|
144
|
+
if (needsCompletion && !collator_1.CollatorService.isGuidStep3Done(this.guidLedger)) {
|
|
145
|
+
await this.engine.runJobCompletionTasks(this.context, { emit: !this.isJobComplete(jobStatus) && !this.shouldPersistJob() });
|
|
146
|
+
//NOTE: notarizeStep3 is fire-and-forget for the trigger.
|
|
147
|
+
//pubOneTimeSubs (inside runJobCompletionTasks) sends a NOTIFY
|
|
148
|
+
//that must not be processed until registerJobCallback runs
|
|
149
|
+
//AFTER pub() returns. An `await` here yields to the event loop,
|
|
150
|
+
//which could deliver the NOTIFY before the callback is registered.
|
|
151
|
+
const txn3 = this.store.transact();
|
|
152
|
+
collator_1.CollatorService.notarizeStep3(this, msgGuid, txn3)
|
|
153
|
+
.then(() => txn3.exec())
|
|
154
|
+
.catch((err) => this.logger.error('trigger-notarize-step3-error', { error: err }));
|
|
155
|
+
}
|
|
156
|
+
//telemetry
|
|
157
|
+
telemetry.mapActivityAttributes();
|
|
48
158
|
telemetry.setJobAttributes({ 'app.job.jss': jobStatus });
|
|
49
159
|
const attrs = { 'app.job.jss': jobStatus };
|
|
50
|
-
await this.transitionAndLogAdjacent(options, jobStatus, attrs);
|
|
51
160
|
telemetry.setActivityAttributes(attrs);
|
|
52
161
|
return this.context.metadata.jid;
|
|
53
162
|
}
|
|
54
163
|
catch (error) {
|
|
55
164
|
telemetry?.setActivityError(error.message);
|
|
56
165
|
if (error instanceof errors_1.DuplicateJobError) {
|
|
57
|
-
//todo: verify baseline in x-AZ rollover
|
|
58
|
-
await (0, utils_1.sleepFor)(1000);
|
|
59
|
-
const isOverage = await collator_1.CollatorService.isInceptionOverage(this, this.context.metadata.guid);
|
|
60
|
-
if (isOverage) {
|
|
61
|
-
this.logger.info('trigger-collation-overage', {
|
|
62
|
-
job_id: error.jobId,
|
|
63
|
-
guid: this.context.metadata.guid,
|
|
64
|
-
});
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
166
|
this.logger.error('duplicate-job-error', {
|
|
68
167
|
job_id: error.jobId,
|
|
69
168
|
guid: this.context.metadata.guid,
|
|
@@ -84,15 +183,6 @@ class Trigger extends activity_1.Activity {
|
|
|
84
183
|
});
|
|
85
184
|
}
|
|
86
185
|
}
|
|
87
|
-
async transitionAndLogAdjacent(options = {}, jobStatus, attrs) {
|
|
88
|
-
//todo: enable resume from pending state
|
|
89
|
-
if (isNaN(options.pending)) {
|
|
90
|
-
const messageIds = await this.transition(this.adjacencyList, jobStatus);
|
|
91
|
-
if (messageIds.length) {
|
|
92
|
-
attrs['app.activity.mids'] = messageIds.join(',');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
186
|
/**
|
|
97
187
|
* `pending` flows will not transition from the trigger to adjacent children until resumed
|
|
98
188
|
*/
|
|
@@ -231,12 +321,6 @@ class Trigger extends activity_1.Activity {
|
|
|
231
321
|
const jobKey = this.config.stats?.key;
|
|
232
322
|
return jobKey ? pipe_1.Pipe.resolve(jobKey, context) : '';
|
|
233
323
|
}
|
|
234
|
-
async setStateNX(status, entity) {
|
|
235
|
-
const jobId = this.context.metadata.jid;
|
|
236
|
-
if (!await this.store.setStateNX(jobId, this.engine.appId, status, entity)) {
|
|
237
|
-
throw new errors_1.DuplicateJobError(jobId);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
324
|
async setStats(transaction) {
|
|
241
325
|
const md = this.context.metadata;
|
|
242
326
|
if (md.key && this.config.stats?.measures) {
|
|
@@ -3,6 +3,113 @@ import { ActivityData, ActivityMetadata, ActivityType, WorkerActivity } from '..
|
|
|
3
3
|
import { JobState } from '../../types/job';
|
|
4
4
|
import { ProviderTransaction } from '../../types/provider';
|
|
5
5
|
import { Activity } from './activity';
|
|
6
|
+
/**
|
|
7
|
+
* Dispatches work to a registered callback function. The `worker` activity
|
|
8
|
+
* publishes a message to its configured `topic` stream, where a worker
|
|
9
|
+
* process picks it up, executes the callback, and returns a response
|
|
10
|
+
* that the engine captures as the activity's output.
|
|
11
|
+
*
|
|
12
|
+
* ## YAML Configuration
|
|
13
|
+
*
|
|
14
|
+
* ```yaml
|
|
15
|
+
* app:
|
|
16
|
+
* id: myapp
|
|
17
|
+
* version: '1'
|
|
18
|
+
* graphs:
|
|
19
|
+
* - subscribes: order.placed
|
|
20
|
+
* expire: 120
|
|
21
|
+
*
|
|
22
|
+
* activities:
|
|
23
|
+
* t1:
|
|
24
|
+
* type: trigger
|
|
25
|
+
*
|
|
26
|
+
* a1:
|
|
27
|
+
* type: worker
|
|
28
|
+
* topic: work.do # matches the registered worker topic
|
|
29
|
+
* input:
|
|
30
|
+
* schema:
|
|
31
|
+
* type: object
|
|
32
|
+
* properties:
|
|
33
|
+
* x: { type: string }
|
|
34
|
+
* maps:
|
|
35
|
+
* x: '{t1.output.data.inputField}'
|
|
36
|
+
* output:
|
|
37
|
+
* schema:
|
|
38
|
+
* type: object
|
|
39
|
+
* properties:
|
|
40
|
+
* y: { type: string }
|
|
41
|
+
* job:
|
|
42
|
+
* maps:
|
|
43
|
+
* result: '{$self.output.data.y}'
|
|
44
|
+
*
|
|
45
|
+
* transitions:
|
|
46
|
+
* t1:
|
|
47
|
+
* - to: a1
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* ## Worker Registration (JavaScript)
|
|
51
|
+
*
|
|
52
|
+
* Workers are registered at initialization time via the `workers` array
|
|
53
|
+
* in `HotMesh.init`. Each worker binds a `topic` to a `callback` function.
|
|
54
|
+
*
|
|
55
|
+
* ```typescript
|
|
56
|
+
* const hotMesh = await HotMesh.init({
|
|
57
|
+
* appId: 'myapp',
|
|
58
|
+
* engine: { connection },
|
|
59
|
+
* workers: [{
|
|
60
|
+
* topic: 'work.do',
|
|
61
|
+
* connection,
|
|
62
|
+
* callback: async (data: StreamData) => ({
|
|
63
|
+
* metadata: { ...data.metadata },
|
|
64
|
+
* data: { y: `${data.data.x} transformed` }
|
|
65
|
+
* })
|
|
66
|
+
* }]
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*
|
|
70
|
+
* ## Retry Policy
|
|
71
|
+
*
|
|
72
|
+
* Retry behavior is configured at the **worker level** (not in YAML) via
|
|
73
|
+
* the `retryPolicy` option. Failed callbacks are retried with exponential
|
|
74
|
+
* backoff until `maximumAttempts` is exhausted. The `maximumInterval` caps
|
|
75
|
+
* the delay between retries.
|
|
76
|
+
*
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const hotMesh = await HotMesh.init({
|
|
79
|
+
* appId: 'myapp',
|
|
80
|
+
* engine: { connection },
|
|
81
|
+
* workers: [{
|
|
82
|
+
* topic: 'work.backoff',
|
|
83
|
+
* connection,
|
|
84
|
+
* retryPolicy: {
|
|
85
|
+
* maximumAttempts: 5, // retry up to 5 times
|
|
86
|
+
* backoffCoefficient: 2, // exponential: 2^0, 2^1, 2^2, ... seconds
|
|
87
|
+
* maximumInterval: '30s', // cap delay at 30 seconds
|
|
88
|
+
* },
|
|
89
|
+
* callback: async (data: StreamData) => {
|
|
90
|
+
* const result = await doWork(data.data);
|
|
91
|
+
* return {
|
|
92
|
+
* code: 200,
|
|
93
|
+
* status: StreamStatus.SUCCESS,
|
|
94
|
+
* metadata: { ...data.metadata },
|
|
95
|
+
* data: { result },
|
|
96
|
+
* } as StreamDataResponse;
|
|
97
|
+
* },
|
|
98
|
+
* }]
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* ## Execution Model
|
|
103
|
+
*
|
|
104
|
+
* Worker is a **Category A (duplex)** activity:
|
|
105
|
+
* - **Leg 1** (`process`): Maps input data and publishes a message to the
|
|
106
|
+
* worker's topic stream.
|
|
107
|
+
* - **Leg 2** (`processEvent`, inherited): Receives the worker's response,
|
|
108
|
+
* maps output data, and executes the step protocol to transition to
|
|
109
|
+
* adjacent activities.
|
|
110
|
+
*
|
|
111
|
+
* @see {@link WorkerActivity} for the TypeScript interface
|
|
112
|
+
*/
|
|
6
113
|
declare class Worker extends Activity {
|
|
7
114
|
config: WorkerActivity;
|
|
8
115
|
constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
|