@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
@@ -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
- await this.verifyEntry();
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
- await this.setState(transaction);
32
- await collator_1.CollatorService.notarizeEarlyCompletion(this, transaction);
33
- await this.setStatus(this.adjacencyList.length - 1, transaction);
34
- const multiResponse = (await transaction.exec());
35
- //todo: this should execute BEFORE the status is decremented
36
- if (this.config.subtype === 'all') {
37
- await this.hookAll();
38
- }
39
- else {
40
- await this.hookOne();
41
- }
42
- //transition to adjacent activities
43
- const jobStatus = this.resolveStatus(multiResponse);
44
- const attrs = { 'app.job.jss': jobStatus };
45
- const messageIds = await this.transition(this.adjacencyList, jobStatus);
46
- if (messageIds.length) {
47
- attrs['app.activity.mids'] = messageIds.join(',');
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(attrs);
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 hookOne() {
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.hook(topic, signalInputData, status, code);
220
+ return await this.engine.signal(topic, signalInputData, status, code, transaction);
113
221
  }
114
222
  /**
115
- * The signal activity will hook all paused jobs that share the same job key.
223
+ * Signals all paused jobs that share the same job key, resuming their execution.
116
224
  */
117
- async hookAll() {
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: `hookAll` will now resume all paused jobs that share the same job key
131
- return await this.engine.hookAll(this.config.topic, signalInputData, keyResolverData, indexQueryFacets);
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, JobStatus } from '../../types/job';
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
- await this.setStateNX(initialStatus, options?.entity || resolvedEntity);
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
- const transaction = this.store.transact();
38
- await this.setState(transaction);
39
- await this.setStats(transaction);
40
- if (options?.pending) {
41
- await this.setExpired(options?.pending, transaction);
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
- await collator_1.CollatorService.notarizeInception(this, this.context.metadata.guid, transaction);
44
- await transaction.exec();
133
+ //best-effort parent notification
45
134
  this.execAdjacentParent();
46
- telemetry.mapActivityAttributes();
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);