@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.
Files changed (138) hide show
  1. package/.claude/settings.local.json +8 -0
  2. package/README.md +158 -38
  3. package/build/index.d.ts +1 -3
  4. package/build/index.js +1 -5
  5. package/build/modules/utils.js +3 -31
  6. package/build/package.json +63 -79
  7. package/build/services/activities/activity.d.ts +97 -9
  8. package/build/services/activities/activity.js +323 -86
  9. package/build/services/activities/await.d.ts +101 -0
  10. package/build/services/activities/await.js +103 -2
  11. package/build/services/activities/cycle.d.ts +82 -0
  12. package/build/services/activities/cycle.js +86 -8
  13. package/build/services/activities/hook.d.ts +144 -1
  14. package/build/services/activities/hook.js +162 -21
  15. package/build/services/activities/interrupt.d.ts +112 -0
  16. package/build/services/activities/interrupt.js +134 -29
  17. package/build/services/activities/signal.d.ts +111 -4
  18. package/build/services/activities/signal.js +136 -28
  19. package/build/services/activities/trigger.d.ts +56 -4
  20. package/build/services/activities/trigger.js +119 -35
  21. package/build/services/activities/worker.d.ts +107 -0
  22. package/build/services/activities/worker.js +109 -2
  23. package/build/services/collator/index.d.ts +116 -30
  24. package/build/services/collator/index.js +211 -115
  25. package/build/services/connector/factory.d.ts +1 -1
  26. package/build/services/connector/factory.js +1 -11
  27. package/build/services/engine/index.d.ts +22 -6
  28. package/build/services/engine/index.js +49 -18
  29. package/build/services/exporter/index.d.ts +2 -0
  30. package/build/services/exporter/index.js +1 -0
  31. package/build/services/hotmesh/index.d.ts +471 -236
  32. package/build/services/hotmesh/index.js +473 -238
  33. package/build/services/memflow/client.js +2 -2
  34. package/build/services/memflow/handle.js +1 -1
  35. package/build/services/memflow/index.d.ts +1 -1
  36. package/build/services/memflow/index.js +1 -1
  37. package/build/services/memflow/workflow/all.d.ts +28 -3
  38. package/build/services/memflow/workflow/all.js +28 -3
  39. package/build/services/memflow/workflow/context.d.ts +44 -1
  40. package/build/services/memflow/workflow/context.js +44 -1
  41. package/build/services/memflow/workflow/didRun.d.ts +23 -3
  42. package/build/services/memflow/workflow/didRun.js +23 -3
  43. package/build/services/memflow/workflow/emit.d.ts +43 -4
  44. package/build/services/memflow/workflow/emit.js +43 -4
  45. package/build/services/memflow/workflow/enrich.d.ts +32 -4
  46. package/build/services/memflow/workflow/enrich.js +32 -4
  47. package/build/services/memflow/workflow/entityMethods.d.ts +54 -7
  48. package/build/services/memflow/workflow/entityMethods.js +54 -7
  49. package/build/services/memflow/workflow/execChild.d.ts +96 -8
  50. package/build/services/memflow/workflow/execChild.js +96 -8
  51. package/build/services/memflow/workflow/execHook.d.ts +54 -39
  52. package/build/services/memflow/workflow/execHook.js +52 -38
  53. package/build/services/memflow/workflow/execHookBatch.d.ts +82 -29
  54. package/build/services/memflow/workflow/execHookBatch.js +80 -28
  55. package/build/services/memflow/workflow/hook.d.ts +68 -3
  56. package/build/services/memflow/workflow/hook.js +69 -4
  57. package/build/services/memflow/workflow/index.d.ts +65 -10
  58. package/build/services/memflow/workflow/index.js +65 -10
  59. package/build/services/memflow/workflow/interrupt.d.ts +50 -4
  60. package/build/services/memflow/workflow/interrupt.js +50 -4
  61. package/build/services/memflow/workflow/interruption.d.ts +49 -16
  62. package/build/services/memflow/workflow/interruption.js +49 -16
  63. package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +21 -4
  64. package/build/services/memflow/workflow/isSideEffectAllowed.js +21 -4
  65. package/build/services/memflow/workflow/proxyActivities.d.ts +70 -42
  66. package/build/services/memflow/workflow/proxyActivities.js +70 -42
  67. package/build/services/memflow/workflow/random.d.ts +33 -3
  68. package/build/services/memflow/workflow/random.js +33 -3
  69. package/build/services/memflow/workflow/searchMethods.d.ts +49 -2
  70. package/build/services/memflow/workflow/searchMethods.js +49 -2
  71. package/build/services/memflow/workflow/signal.d.ts +51 -22
  72. package/build/services/memflow/workflow/signal.js +52 -23
  73. package/build/services/memflow/workflow/sleepFor.d.ts +57 -18
  74. package/build/services/memflow/workflow/sleepFor.js +57 -18
  75. package/build/services/memflow/workflow/trace.d.ts +39 -6
  76. package/build/services/memflow/workflow/trace.js +39 -6
  77. package/build/services/memflow/workflow/waitFor.d.ts +55 -18
  78. package/build/services/memflow/workflow/waitFor.js +55 -18
  79. package/build/services/router/consumption/index.js +1 -1
  80. package/build/services/search/factory.js +1 -9
  81. package/build/services/store/factory.js +1 -9
  82. package/build/services/store/index.d.ts +6 -1
  83. package/build/services/store/providers/postgres/kvsql.d.ts +4 -0
  84. package/build/services/store/providers/postgres/kvsql.js +4 -0
  85. package/build/services/store/providers/postgres/kvtransaction.d.ts +2 -0
  86. package/build/services/store/providers/postgres/kvtransaction.js +23 -0
  87. package/build/services/store/providers/postgres/kvtypes/hash/basic.d.ts +51 -0
  88. package/build/services/store/providers/postgres/kvtypes/hash/basic.js +193 -1
  89. package/build/services/store/providers/postgres/kvtypes/hash/index.d.ts +4 -0
  90. package/build/services/store/providers/postgres/kvtypes/hash/index.js +6 -0
  91. package/build/services/store/providers/postgres/postgres.d.ts +21 -1
  92. package/build/services/store/providers/postgres/postgres.js +42 -4
  93. package/build/services/stream/factory.js +1 -17
  94. package/build/services/stream/providers/postgres/scout.js +2 -2
  95. package/build/services/sub/factory.js +1 -9
  96. package/build/services/sub/index.d.ts +1 -1
  97. package/build/services/sub/providers/postgres/postgres.d.ts +1 -1
  98. package/build/services/sub/providers/postgres/postgres.js +25 -10
  99. package/build/services/task/index.d.ts +1 -1
  100. package/build/services/task/index.js +2 -6
  101. package/build/services/telemetry/index.js +6 -0
  102. package/build/types/activity.d.ts +1 -1
  103. package/build/types/hotmesh.d.ts +1 -1
  104. package/build/types/index.d.ts +0 -1
  105. package/build/types/index.js +1 -4
  106. package/build/types/job.d.ts +1 -1
  107. package/build/types/memflow.d.ts +1 -1
  108. package/build/types/provider.d.ts +1 -1
  109. package/build/types/quorum.d.ts +2 -2
  110. package/build/vitest.config.d.ts +2 -0
  111. package/build/vitest.config.js +18 -0
  112. package/index.ts +0 -4
  113. package/package.json +63 -79
  114. package/vitest.config.ts +17 -0
  115. package/build/services/connector/providers/ioredis.d.ts +0 -9
  116. package/build/services/connector/providers/ioredis.js +0 -26
  117. package/build/services/connector/providers/redis.d.ts +0 -9
  118. package/build/services/connector/providers/redis.js +0 -38
  119. package/build/services/search/providers/redis/ioredis.d.ts +0 -23
  120. package/build/services/search/providers/redis/ioredis.js +0 -189
  121. package/build/services/search/providers/redis/redis.d.ts +0 -23
  122. package/build/services/search/providers/redis/redis.js +0 -202
  123. package/build/services/store/providers/redis/_base.d.ts +0 -137
  124. package/build/services/store/providers/redis/_base.js +0 -980
  125. package/build/services/store/providers/redis/ioredis.d.ts +0 -20
  126. package/build/services/store/providers/redis/ioredis.js +0 -190
  127. package/build/services/store/providers/redis/redis.d.ts +0 -18
  128. package/build/services/store/providers/redis/redis.js +0 -199
  129. package/build/services/stream/providers/redis/ioredis.d.ts +0 -61
  130. package/build/services/stream/providers/redis/ioredis.js +0 -272
  131. package/build/services/stream/providers/redis/redis.d.ts +0 -61
  132. package/build/services/stream/providers/redis/redis.js +0 -305
  133. package/build/services/sub/providers/redis/ioredis.d.ts +0 -20
  134. package/build/services/sub/providers/redis/ioredis.js +0 -161
  135. package/build/services/sub/providers/redis/redis.d.ts +0 -18
  136. package/build/services/sub/providers/redis/redis.js +0 -148
  137. package/build/types/redis.d.ts +0 -258
  138. package/build/types/redis.js +0 -11
@@ -9,7 +9,145 @@ const telemetry_1 = require("../telemetry");
9
9
  const stream_1 = require("../../types/stream");
10
10
  const activity_1 = require("./activity");
11
11
  /**
12
- * Supports `signal hook`, `time hook`, and `cycle hook` patterns
12
+ * A versatile pause/resume activity that supports three distinct patterns:
13
+ * **time hook** (sleep), **web hook** (external signal), and **passthrough**
14
+ * (immediate transition with optional data mapping).
15
+ *
16
+ * The hook activity is the most flexible activity type. Depending on its
17
+ * YAML configuration, it operates in one of the following modes:
18
+ *
19
+ * ## Time Hook (Sleep)
20
+ *
21
+ * Pauses the flow for a specified duration in seconds. The `sleep` value
22
+ * can be a literal number or a `@pipe` expression for dynamic delays
23
+ * (e.g., exponential backoff).
24
+ *
25
+ * ```yaml
26
+ * app:
27
+ * id: myapp
28
+ * version: '1'
29
+ * graphs:
30
+ * - subscribes: job.start
31
+ * expire: 300
32
+ *
33
+ * activities:
34
+ * t1:
35
+ * type: trigger
36
+ *
37
+ * delay:
38
+ * type: hook
39
+ * sleep: 60 # pause for 60 seconds
40
+ * job:
41
+ * maps:
42
+ * paused_at: '{$self.output.metadata.ac}'
43
+ *
44
+ * resume:
45
+ * type: hook
46
+ *
47
+ * transitions:
48
+ * t1:
49
+ * - to: delay
50
+ * delay:
51
+ * - to: resume
52
+ * ```
53
+ *
54
+ * ## Web Hook (External Signal)
55
+ *
56
+ * Registers a webhook listener on a named topic. The flow pauses until
57
+ * an external signal is sent to the hook's topic. The signal data becomes
58
+ * available as `$self.hook.data`. The `hooks` section at the graph level
59
+ * routes incoming signals to the waiting activity.
60
+ *
61
+ * ```yaml
62
+ * app:
63
+ * id: myapp
64
+ * version: '1'
65
+ * graphs:
66
+ * - subscribes: order.placed
67
+ * expire: 3600
68
+ *
69
+ * activities:
70
+ * t1:
71
+ * type: trigger
72
+ *
73
+ * wait_for_approval:
74
+ * type: hook
75
+ * hook:
76
+ * type: object
77
+ * properties:
78
+ * approved: { type: boolean }
79
+ * job:
80
+ * maps:
81
+ * approved: '{$self.hook.data.approved}'
82
+ *
83
+ * done:
84
+ * type: hook
85
+ *
86
+ * transitions:
87
+ * t1:
88
+ * - to: wait_for_approval
89
+ * wait_for_approval:
90
+ * - to: done
91
+ *
92
+ * hooks:
93
+ * order.approval: # external topic that delivers the signal
94
+ * - to: wait_for_approval
95
+ * conditions:
96
+ * match:
97
+ * - expected: '{t1.output.data.id}'
98
+ * actual: '{$self.hook.data.id}'
99
+ * ```
100
+ *
101
+ * ## Passthrough (No Hook)
102
+ *
103
+ * When neither `sleep` nor `hook` is configured, the hook activity acts
104
+ * as a passthrough: it maps data and immediately transitions to children.
105
+ * This is useful for data transformation, convergence points, or as a
106
+ * cycle pivot (with `cycle: true`).
107
+ *
108
+ * ```yaml
109
+ * app:
110
+ * id: myapp
111
+ * version: '1'
112
+ * graphs:
113
+ * - subscribes: job.start
114
+ *
115
+ * activities:
116
+ * t1:
117
+ * type: trigger
118
+ *
119
+ * pivot:
120
+ * type: hook
121
+ * cycle: true # enables re-entry from a cycle activity
122
+ * output:
123
+ * maps:
124
+ * retryCount: 0
125
+ * job:
126
+ * maps:
127
+ * counter: '{$self.output.data.retryCount}'
128
+ *
129
+ * do_work:
130
+ * type: worker
131
+ * topic: work.do
132
+ *
133
+ * transitions:
134
+ * t1:
135
+ * - to: pivot
136
+ * pivot:
137
+ * - to: do_work
138
+ * ```
139
+ *
140
+ * ## Execution Model
141
+ *
142
+ * - **With `sleep` or `hook`**: Category A (duplex). Leg 1 registers the
143
+ * hook and saves state. Leg 2 fires when the timer expires or the
144
+ * external signal arrives (via `processTimeHookEvent` or
145
+ * `processWebHookEvent`).
146
+ * - **Without `sleep` or `hook`**: Category B (passthrough). Uses the
147
+ * crash-safe `executeLeg1StepProtocol` to map data and transition
148
+ * to adjacent activities.
149
+ *
150
+ * @see {@link HookActivity} for the TypeScript interface
13
151
  */
14
152
  class Hook extends activity_1.Activity {
15
153
  constructor(config, data, metadata, hook, engine, context) {
@@ -24,15 +162,21 @@ class Hook extends activity_1.Activity {
24
162
  });
25
163
  let telemetry;
26
164
  try {
27
- await this.verifyEntry();
165
+ //Phase 1: Load state and verify entry (all paths use verifyLeg1Entry
166
+ //for GUID-ledger-backed crash recovery)
167
+ const isResume = await this.verifyLeg1Entry();
28
168
  telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
29
169
  telemetry.startActivitySpan(this.leg);
30
- if (this.doesHook()) {
31
- //sleep and wait to awaken upon a signal
32
- await this.doHook(telemetry);
170
+ //Phase 2: Route based on RUNTIME evaluation (not static config)
171
+ if (this.isConfiguredAsHook() && this.doesHook()) {
172
+ //Category A: duplexed hook registration (Leg2 handles completion)
173
+ if (!isResume) {
174
+ await this.doHook(telemetry);
175
+ }
176
+ //If resume, Leg1 already ran — Leg2 will handle completion
33
177
  }
34
178
  else {
35
- //end the activity and transition to its children
179
+ //Category B: passthrough with crash-safe step protocol + GUID ledger
36
180
  await this.doPassThrough(telemetry);
37
181
  }
38
182
  return this.context.metadata.aid;
@@ -76,6 +220,13 @@ class Hook extends activity_1.Activity {
76
220
  });
77
221
  }
78
222
  }
223
+ /**
224
+ * Static config check: does this activity have a hook or sleep config?
225
+ * Used for routing before context is loaded.
226
+ */
227
+ isConfiguredAsHook() {
228
+ return !!this.config.sleep || !!this.config.hook?.topic;
229
+ }
79
230
  /**
80
231
  * does this activity use a time-hook or web-hook
81
232
  */
@@ -92,29 +243,19 @@ class Hook extends activity_1.Activity {
92
243
  this.mapOutputData();
93
244
  this.mapJobData();
94
245
  await this.setState(transaction);
95
- await collator_1.CollatorService.authorizeReentry(this, transaction);
246
+ await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
96
247
  await this.setStatus(0, transaction);
97
248
  await transaction.exec();
98
249
  telemetry.mapActivityAttributes();
99
250
  }
100
251
  async doPassThrough(telemetry) {
101
- const transaction = this.store.transact();
102
- let multiResponse;
103
252
  this.adjacencyList = await this.filterAdjacent();
104
253
  this.mapOutputData();
105
254
  this.mapJobData();
106
- await this.setState(transaction);
107
- await collator_1.CollatorService.notarizeEarlyCompletion(this, transaction);
108
- await this.setStatus(this.adjacencyList.length - 1, transaction);
109
- multiResponse = (await transaction.exec());
255
+ //Category B: use Leg1 step protocol for crash-safe edge capture
256
+ await this.executeLeg1StepProtocol(this.adjacencyList.length - 1);
110
257
  telemetry.mapActivityAttributes();
111
- const jobStatus = this.resolveStatus(multiResponse);
112
- const attrs = { 'app.job.jss': jobStatus };
113
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
114
- if (messageIds.length) {
115
- attrs['app.activity.mids'] = messageIds.join(',');
116
- }
117
- telemetry.setActivityAttributes(attrs);
258
+ telemetry.setActivityAttributes({});
118
259
  }
119
260
  async getHookRule(topic) {
120
261
  const rules = await this.store.getHookRules();
@@ -126,7 +267,7 @@ class Hook extends activity_1.Activity {
126
267
  }
127
268
  else if (this.config.sleep) {
128
269
  const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
129
- await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '');
270
+ await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '', transaction);
130
271
  return this.context.metadata.jid;
131
272
  }
132
273
  }
@@ -3,6 +3,118 @@ import { TelemetryService } from '../telemetry';
3
3
  import { ActivityData, ActivityMetadata, ActivityType, InterruptActivity } from '../../types/activity';
4
4
  import { JobInterruptOptions, JobState } from '../../types/job';
5
5
  import { Activity } from './activity';
6
+ /**
7
+ * Terminates a flow by sending an interrupt signal. The `interrupt` activity
8
+ * can target the current flow (self-interrupt) or any other flow by its
9
+ * job ID (remote interrupt). Interrupted jobs have their status set to a
10
+ * value less than -100,000,000, indicating abnormal termination.
11
+ *
12
+ * ## YAML Configuration — Self-Interrupt
13
+ *
14
+ * When no `target` is specified, the activity interrupts the current flow.
15
+ * The flow terminates immediately after this activity executes. Use
16
+ * conditional transitions to route to an interrupt only when needed.
17
+ *
18
+ * ```yaml
19
+ * app:
20
+ * id: myapp
21
+ * version: '1'
22
+ * graphs:
23
+ * - subscribes: validation.check
24
+ * expire: 120
25
+ *
26
+ * activities:
27
+ * t1:
28
+ * type: trigger
29
+ *
30
+ * validate:
31
+ * type: worker
32
+ * topic: validate.input
33
+ *
34
+ * cancel:
35
+ * type: interrupt
36
+ * reason: 'Validation failed'
37
+ * throw: true
38
+ * code: 410
39
+ * job:
40
+ * maps:
41
+ * cancelled_at: '{$self.output.metadata.ac}'
42
+ *
43
+ * proceed:
44
+ * type: hook
45
+ *
46
+ * transitions:
47
+ * t1:
48
+ * - to: validate
49
+ * validate:
50
+ * - to: cancel
51
+ * conditions:
52
+ * code: 422 # interrupt only on validation failure
53
+ * - to: proceed
54
+ * ```
55
+ *
56
+ * ## YAML Configuration — Remote Interrupt
57
+ *
58
+ * When `target` is specified, the activity interrupts another flow while
59
+ * the current flow continues to transition to adjacent activities.
60
+ *
61
+ * ```yaml
62
+ * app:
63
+ * id: myapp
64
+ * version: '1'
65
+ * graphs:
66
+ * - subscribes: parent.flow
67
+ * expire: 120
68
+ *
69
+ * activities:
70
+ * t1:
71
+ * type: trigger
72
+ * job:
73
+ * maps:
74
+ * childJobId: '{$self.output.data.childJobId}'
75
+ *
76
+ * stop_child:
77
+ * type: interrupt
78
+ * topic: child.flow.topic # topic of the target flow
79
+ * target: '{t1.output.data.childJobId}'
80
+ * throw: false # do not throw (silent cancellation)
81
+ * descend: true # also interrupt descendant sub-flows
82
+ * job:
83
+ * maps:
84
+ * interrupted: true
85
+ *
86
+ * done:
87
+ * type: hook
88
+ *
89
+ * transitions:
90
+ * t1:
91
+ * - to: stop_child
92
+ * stop_child:
93
+ * - to: done
94
+ * ```
95
+ *
96
+ * ## Configuration Properties
97
+ *
98
+ * | Property | Type | Default | Description |
99
+ * |------------|---------|--------------------|-------------|
100
+ * | `target` | string | (current job) | Job ID to interrupt. Supports `@pipe` expressions. |
101
+ * | `topic` | string | (current topic) | Topic of the target flow |
102
+ * | `reason` | string | `'Job Interrupted'`| Error message attached to the interruption |
103
+ * | `throw` | boolean | `true` | Whether to throw a `JobInterrupted` error |
104
+ * | `descend` | boolean | `false` | Whether to cascade to child/descendant flows |
105
+ * | `code` | number | `410` | Error code attached to the interruption |
106
+ * | `stack` | string | — | Optional stack trace |
107
+ *
108
+ * ## Execution Model
109
+ *
110
+ * - **Self-interrupt (no `target`)**: Category C. Verifies entry, maps job
111
+ * data, sets status to -1, and fires the interrupt. No children.
112
+ * - **Remote interrupt (with `target`)**: Category B. Fires the interrupt
113
+ * best-effort, then uses `executeLeg1StepProtocol` to transition to
114
+ * adjacent activities.
115
+ *
116
+ * @see {@link InterruptActivity} for the TypeScript interface
117
+ */
6
118
  declare class Interrupt extends Activity {
7
119
  config: InterruptActivity;
8
120
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
@@ -6,6 +6,118 @@ const collator_1 = require("../collator");
6
6
  const pipe_1 = require("../pipe");
7
7
  const telemetry_1 = require("../telemetry");
8
8
  const activity_1 = require("./activity");
9
+ /**
10
+ * Terminates a flow by sending an interrupt signal. The `interrupt` activity
11
+ * can target the current flow (self-interrupt) or any other flow by its
12
+ * job ID (remote interrupt). Interrupted jobs have their status set to a
13
+ * value less than -100,000,000, indicating abnormal termination.
14
+ *
15
+ * ## YAML Configuration — Self-Interrupt
16
+ *
17
+ * When no `target` is specified, the activity interrupts the current flow.
18
+ * The flow terminates immediately after this activity executes. Use
19
+ * conditional transitions to route to an interrupt only when needed.
20
+ *
21
+ * ```yaml
22
+ * app:
23
+ * id: myapp
24
+ * version: '1'
25
+ * graphs:
26
+ * - subscribes: validation.check
27
+ * expire: 120
28
+ *
29
+ * activities:
30
+ * t1:
31
+ * type: trigger
32
+ *
33
+ * validate:
34
+ * type: worker
35
+ * topic: validate.input
36
+ *
37
+ * cancel:
38
+ * type: interrupt
39
+ * reason: 'Validation failed'
40
+ * throw: true
41
+ * code: 410
42
+ * job:
43
+ * maps:
44
+ * cancelled_at: '{$self.output.metadata.ac}'
45
+ *
46
+ * proceed:
47
+ * type: hook
48
+ *
49
+ * transitions:
50
+ * t1:
51
+ * - to: validate
52
+ * validate:
53
+ * - to: cancel
54
+ * conditions:
55
+ * code: 422 # interrupt only on validation failure
56
+ * - to: proceed
57
+ * ```
58
+ *
59
+ * ## YAML Configuration — Remote Interrupt
60
+ *
61
+ * When `target` is specified, the activity interrupts another flow while
62
+ * the current flow continues to transition to adjacent activities.
63
+ *
64
+ * ```yaml
65
+ * app:
66
+ * id: myapp
67
+ * version: '1'
68
+ * graphs:
69
+ * - subscribes: parent.flow
70
+ * expire: 120
71
+ *
72
+ * activities:
73
+ * t1:
74
+ * type: trigger
75
+ * job:
76
+ * maps:
77
+ * childJobId: '{$self.output.data.childJobId}'
78
+ *
79
+ * stop_child:
80
+ * type: interrupt
81
+ * topic: child.flow.topic # topic of the target flow
82
+ * target: '{t1.output.data.childJobId}'
83
+ * throw: false # do not throw (silent cancellation)
84
+ * descend: true # also interrupt descendant sub-flows
85
+ * job:
86
+ * maps:
87
+ * interrupted: true
88
+ *
89
+ * done:
90
+ * type: hook
91
+ *
92
+ * transitions:
93
+ * t1:
94
+ * - to: stop_child
95
+ * stop_child:
96
+ * - to: done
97
+ * ```
98
+ *
99
+ * ## Configuration Properties
100
+ *
101
+ * | Property | Type | Default | Description |
102
+ * |------------|---------|--------------------|-------------|
103
+ * | `target` | string | (current job) | Job ID to interrupt. Supports `@pipe` expressions. |
104
+ * | `topic` | string | (current topic) | Topic of the target flow |
105
+ * | `reason` | string | `'Job Interrupted'`| Error message attached to the interruption |
106
+ * | `throw` | boolean | `true` | Whether to throw a `JobInterrupted` error |
107
+ * | `descend` | boolean | `false` | Whether to cascade to child/descendant flows |
108
+ * | `code` | number | `410` | Error code attached to the interruption |
109
+ * | `stack` | string | — | Optional stack trace |
110
+ *
111
+ * ## Execution Model
112
+ *
113
+ * - **Self-interrupt (no `target`)**: Category C. Verifies entry, maps job
114
+ * data, sets status to -1, and fires the interrupt. No children.
115
+ * - **Remote interrupt (with `target`)**: Category B. Fires the interrupt
116
+ * best-effort, then uses `executeLeg1StepProtocol` to transition to
117
+ * adjacent activities.
118
+ *
119
+ * @see {@link InterruptActivity} for the TypeScript interface
120
+ */
9
121
  class Interrupt extends activity_1.Activity {
10
122
  constructor(config, data, metadata, hook, engine, context) {
11
123
  super(config, data, metadata, hook, engine, context);
@@ -19,13 +131,18 @@ class Interrupt extends activity_1.Activity {
19
131
  });
20
132
  let telemetry;
21
133
  try {
22
- await this.verifyEntry();
23
- telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
24
- telemetry.startActivitySpan(this.leg);
25
- if (this.isInterruptingSelf()) {
134
+ if (!this.config.target) {
135
+ //Category C: self-interrupt (no children, no semaphore edge risk)
136
+ await this.verifyEntry();
137
+ telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
138
+ telemetry.startActivitySpan(this.leg);
26
139
  await this.interruptSelf(telemetry);
27
140
  }
28
141
  else {
142
+ //Category B: interrupt another (spawns children, needs step protocol)
143
+ await this.verifyLeg1Entry();
144
+ telemetry = new telemetry_1.TelemetryService(this.engine.appId, this.config, this.metadata, this.context);
145
+ telemetry.startActivitySpan(this.leg);
29
146
  await this.interruptAnother(telemetry);
30
147
  }
31
148
  }
@@ -69,20 +186,21 @@ class Interrupt extends activity_1.Activity {
69
186
  }
70
187
  }
71
188
  async interruptSelf(telemetry) {
72
- // Apply final updates to THIS job's state
73
189
  if (this.config.job?.maps) {
74
190
  this.mapJobData();
75
- await this.setState();
76
191
  }
77
- // Interrupt THIS job
78
- const messageId = await this.interrupt();
79
- // Notarize completion and log
192
+ // Bundle state + Leg1 completion + semaphore in one transaction
80
193
  telemetry.mapActivityAttributes();
81
194
  const transaction = this.store.transact();
82
- await collator_1.CollatorService.notarizeEarlyCompletion(this, transaction);
195
+ if (this.config.job?.maps) {
196
+ await this.setState(transaction);
197
+ }
198
+ await collator_1.CollatorService.notarizeLeg1Completion(this, transaction);
83
199
  await this.setStatus(-1, transaction);
84
200
  const txResponse = (await transaction.exec());
85
201
  const jobStatus = this.resolveStatus(txResponse);
202
+ // Interrupt fires AFTER proof commits (best-effort)
203
+ const messageId = await this.interrupt();
86
204
  telemetry.setActivityAttributes({
87
205
  'app.activity.mid': messageId,
88
206
  'app.job.jss': jobStatus,
@@ -90,31 +208,18 @@ class Interrupt extends activity_1.Activity {
90
208
  return this.context.metadata.aid;
91
209
  }
92
210
  async interruptAnother(telemetry) {
93
- // Interrupt ANOTHER job
94
- const messageId = await this.interrupt();
95
- const attrs = { 'app.activity.mid': messageId };
211
+ // Interrupt ANOTHER job (best-effort, fires before step protocol)
212
+ await this.interrupt();
96
213
  // Apply updates to THIS job's state
97
- telemetry.mapActivityAttributes();
98
214
  this.adjacencyList = await this.filterAdjacent();
99
215
  if (this.config.job?.maps || this.config.output?.maps) {
100
216
  this.mapOutputData();
101
217
  this.mapJobData();
102
- const transaction = this.store.transact();
103
- await this.setState(transaction);
104
- }
105
- // Notarize completion
106
- const transaction = this.store.transact();
107
- await collator_1.CollatorService.notarizeEarlyCompletion(this, transaction);
108
- await this.setStatus(this.adjacencyList.length - 1, transaction);
109
- const txResponse = (await transaction.exec());
110
- const jobStatus = this.resolveStatus(txResponse);
111
- attrs['app.job.jss'] = jobStatus;
112
- // Transition next generation and log
113
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
114
- if (messageIds.length) {
115
- attrs['app.activity.mids'] = messageIds.join(',');
116
218
  }
117
- telemetry.setActivityAttributes(attrs);
219
+ //Category B: use Leg1 step protocol for crash-safe edge capture
220
+ await this.executeLeg1StepProtocol(this.adjacencyList.length - 1);
221
+ telemetry.mapActivityAttributes();
222
+ telemetry.setActivityAttributes({});
118
223
  return this.context.metadata.aid;
119
224
  }
120
225
  isInterruptingSelf() {
@@ -1,7 +1,113 @@
1
1
  import { EngineService } from '../engine';
2
2
  import { ActivityData, ActivityMetadata, ActivityType, SignalActivity } from '../../types/activity';
3
3
  import { JobState } from '../../types/job';
4
+ import { ProviderTransaction } from '../../types/provider';
4
5
  import { Activity } from './activity';
6
+ /**
7
+ * Sends a signal to one or more paused flows, resuming their execution.
8
+ * The `signal` activity is the counterpart to a `Hook` activity
9
+ * configured with a webhook listener. It allows any flow to reach into
10
+ * another flow and deliver data to a waiting hook, regardless of the
11
+ * relationship between the flows.
12
+ *
13
+ * ## YAML Configuration — Signal One
14
+ *
15
+ * Resumes a single paused flow by publishing to the hook's topic. Use
16
+ * `subtype: one` when you know the specific hook topic to signal.
17
+ *
18
+ * ```yaml
19
+ * app:
20
+ * id: myapp
21
+ * version: '1'
22
+ * graphs:
23
+ * - subscribes: signal.start
24
+ * expire: 120
25
+ *
26
+ * activities:
27
+ * t1:
28
+ * type: trigger
29
+ *
30
+ * resume_hook:
31
+ * type: signal
32
+ * subtype: one
33
+ * topic: my.hook.topic # the hook's registered topic
34
+ * status: success # optional: success (default) or pending
35
+ * code: 200 # optional: 200 (default) or 202 (keep-alive)
36
+ * signal:
37
+ * schema:
38
+ * type: object
39
+ * properties:
40
+ * approved: { type: boolean }
41
+ * maps:
42
+ * approved: true # data delivered to the hook
43
+ *
44
+ * done:
45
+ * type: hook
46
+ *
47
+ * transitions:
48
+ * t1:
49
+ * - to: resume_hook
50
+ * resume_hook:
51
+ * - to: done
52
+ * ```
53
+ *
54
+ * A `code: 202` signal delivers data but keeps the hook alive for
55
+ * additional signals. A `code: 200` (default) closes the hook.
56
+ *
57
+ * ## YAML Configuration — Signal All
58
+ *
59
+ * Resumes all paused flows that share a common job key facet. Use
60
+ * `subtype: all` for fan-out patterns where multiple waiting flows
61
+ * should be resumed simultaneously.
62
+ *
63
+ * ```yaml
64
+ * app:
65
+ * id: myapp
66
+ * version: '1'
67
+ * graphs:
68
+ * - subscribes: signal.fan.out
69
+ * expire: 120
70
+ *
71
+ * activities:
72
+ * t1:
73
+ * type: trigger
74
+ *
75
+ * resume_all:
76
+ * type: signal
77
+ * subtype: all
78
+ * topic: hook.resume
79
+ * key_name: parent_job_id # index facet name
80
+ * key_value: '{$job.metadata.jid}'
81
+ * scrub: true # clean up indexes after use
82
+ * resolver:
83
+ * maps:
84
+ * data:
85
+ * parent_job_id: '{$job.metadata.jid}'
86
+ * scrub: true
87
+ * signal:
88
+ * maps:
89
+ * done: true # data delivered to all matching hooks
90
+ *
91
+ * done:
92
+ * type: hook
93
+ *
94
+ * transitions:
95
+ * t1:
96
+ * - to: resume_all
97
+ * resume_all:
98
+ * - to: done
99
+ * ```
100
+ *
101
+ * ## Execution Model
102
+ *
103
+ * Signal is a **Category B (Leg1-only with children)** activity:
104
+ * - Bundles the hook signal with the Leg 1 completion marker in a
105
+ * single transaction (`hookOne`) or fires best-effort (`hookAll`).
106
+ * - Executes the crash-safe `executeLeg1StepProtocol` to transition
107
+ * to adjacent activities.
108
+ *
109
+ * @see {@link SignalActivity} for the TypeScript interface
110
+ */
5
111
  declare class Signal extends Activity {
6
112
  config: SignalActivity;
7
113
  constructor(config: ActivityType, data: ActivityData, metadata: ActivityMetadata, hook: ActivityData | null, engine: EngineService, context?: JobState);
@@ -9,12 +115,13 @@ declare class Signal extends Activity {
9
115
  mapSignalData(): Record<string, any>;
10
116
  mapResolverData(): Record<string, any>;
11
117
  /**
12
- * The signal activity will hook one
118
+ * The signal activity will hook one. Accepts an optional transaction
119
+ * so the hook publish can be bundled with the Leg1 completion marker.
13
120
  */
14
- hookOne(): Promise<string>;
121
+ signalOne(transaction?: ProviderTransaction): Promise<string>;
15
122
  /**
16
- * The signal activity will hook all paused jobs that share the same job key.
123
+ * Signals all paused jobs that share the same job key, resuming their execution.
17
124
  */
18
- hookAll(): Promise<string[]>;
125
+ signalAll(): Promise<string[]>;
19
126
  }
20
127
  export { Signal };