@hotmeshio/hotmesh 0.8.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 (86) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/README.md +158 -38
  3. package/build/package.json +62 -67
  4. package/build/services/activities/activity.d.ts +58 -7
  5. package/build/services/activities/activity.js +66 -37
  6. package/build/services/activities/await.d.ts +101 -0
  7. package/build/services/activities/await.js +101 -0
  8. package/build/services/activities/cycle.d.ts +82 -0
  9. package/build/services/activities/cycle.js +86 -8
  10. package/build/services/activities/hook.d.ts +139 -1
  11. package/build/services/activities/hook.js +140 -2
  12. package/build/services/activities/interrupt.d.ts +112 -0
  13. package/build/services/activities/interrupt.js +118 -5
  14. package/build/services/activities/signal.d.ts +108 -3
  15. package/build/services/activities/signal.js +113 -8
  16. package/build/services/activities/trigger.d.ts +56 -4
  17. package/build/services/activities/trigger.js +119 -35
  18. package/build/services/activities/worker.d.ts +107 -0
  19. package/build/services/activities/worker.js +107 -0
  20. package/build/services/collator/index.d.ts +3 -15
  21. package/build/services/collator/index.js +7 -34
  22. package/build/services/engine/index.d.ts +18 -2
  23. package/build/services/engine/index.js +14 -4
  24. package/build/services/exporter/index.d.ts +2 -0
  25. package/build/services/exporter/index.js +1 -0
  26. package/build/services/hotmesh/index.d.ts +471 -236
  27. package/build/services/hotmesh/index.js +473 -238
  28. package/build/services/memflow/client.js +2 -2
  29. package/build/services/memflow/handle.js +1 -1
  30. package/build/services/memflow/index.d.ts +1 -1
  31. package/build/services/memflow/index.js +1 -1
  32. package/build/services/memflow/workflow/all.d.ts +28 -3
  33. package/build/services/memflow/workflow/all.js +28 -3
  34. package/build/services/memflow/workflow/context.d.ts +44 -1
  35. package/build/services/memflow/workflow/context.js +44 -1
  36. package/build/services/memflow/workflow/didRun.d.ts +23 -3
  37. package/build/services/memflow/workflow/didRun.js +23 -3
  38. package/build/services/memflow/workflow/emit.d.ts +43 -4
  39. package/build/services/memflow/workflow/emit.js +43 -4
  40. package/build/services/memflow/workflow/enrich.d.ts +32 -4
  41. package/build/services/memflow/workflow/enrich.js +32 -4
  42. package/build/services/memflow/workflow/entityMethods.d.ts +54 -7
  43. package/build/services/memflow/workflow/entityMethods.js +54 -7
  44. package/build/services/memflow/workflow/execChild.d.ts +96 -8
  45. package/build/services/memflow/workflow/execChild.js +96 -8
  46. package/build/services/memflow/workflow/execHook.d.ts +54 -39
  47. package/build/services/memflow/workflow/execHook.js +52 -38
  48. package/build/services/memflow/workflow/execHookBatch.d.ts +82 -29
  49. package/build/services/memflow/workflow/execHookBatch.js +80 -28
  50. package/build/services/memflow/workflow/hook.d.ts +68 -3
  51. package/build/services/memflow/workflow/hook.js +69 -4
  52. package/build/services/memflow/workflow/index.d.ts +65 -10
  53. package/build/services/memflow/workflow/index.js +65 -10
  54. package/build/services/memflow/workflow/interrupt.d.ts +50 -4
  55. package/build/services/memflow/workflow/interrupt.js +50 -4
  56. package/build/services/memflow/workflow/interruption.d.ts +49 -16
  57. package/build/services/memflow/workflow/interruption.js +49 -16
  58. package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +21 -4
  59. package/build/services/memflow/workflow/isSideEffectAllowed.js +21 -4
  60. package/build/services/memflow/workflow/proxyActivities.d.ts +70 -42
  61. package/build/services/memflow/workflow/proxyActivities.js +70 -42
  62. package/build/services/memflow/workflow/random.d.ts +33 -3
  63. package/build/services/memflow/workflow/random.js +33 -3
  64. package/build/services/memflow/workflow/searchMethods.d.ts +49 -2
  65. package/build/services/memflow/workflow/searchMethods.js +49 -2
  66. package/build/services/memflow/workflow/signal.d.ts +51 -22
  67. package/build/services/memflow/workflow/signal.js +52 -23
  68. package/build/services/memflow/workflow/sleepFor.d.ts +57 -18
  69. package/build/services/memflow/workflow/sleepFor.js +57 -18
  70. package/build/services/memflow/workflow/trace.d.ts +39 -6
  71. package/build/services/memflow/workflow/trace.js +39 -6
  72. package/build/services/memflow/workflow/waitFor.d.ts +55 -18
  73. package/build/services/memflow/workflow/waitFor.js +55 -18
  74. package/build/services/store/index.d.ts +1 -1
  75. package/build/services/store/providers/postgres/postgres.d.ts +1 -1
  76. package/build/services/store/providers/postgres/postgres.js +4 -3
  77. package/build/services/telemetry/index.js +6 -0
  78. package/build/types/activity.d.ts +1 -1
  79. package/build/types/hotmesh.d.ts +1 -1
  80. package/build/types/job.d.ts +1 -1
  81. package/build/types/memflow.d.ts +1 -1
  82. package/build/types/quorum.d.ts +2 -2
  83. package/build/vitest.config.d.ts +2 -0
  84. package/build/vitest.config.js +18 -0
  85. package/package.json +62 -67
  86. package/vitest.config.ts +17 -0
@@ -11,178 +11,160 @@ const router_1 = require("../router");
11
11
  const worker_1 = require("../worker");
12
12
  const enums_1 = require("../../modules/enums");
13
13
  /**
14
- * HotMesh transforms Postgres into a durable workflow orchestration engine capable of running
15
- * fault-tolerant workflows across multiple services and systems.
14
+ * A distributed service mesh that turns Postgres into a durable workflow
15
+ * orchestration engine. Every `HotMesh.init()` call creates a **point of
16
+ * presence** — an engine, a quorum member, and zero or more workers — that
17
+ * collaborates with its peers through Postgres LISTEN/NOTIFY to form a
18
+ * self-coordinating mesh with no external dependencies.
16
19
  *
17
- * ## Key Features
20
+ * ## Service Mesh Architecture
18
21
  *
19
- * - **Fault Tolerance**: Automatic retry with exponential backoff and configurable policies
20
- * - **Distributed Execution**: No single point of failure
21
- * - **YAML-Driven**: Model-driven development with declarative workflow definitions
22
- * - **OpenTelemetry**: Built-in observability and tracing
23
- * - **Durable State**: Workflow state persists across system restarts
24
- * - **Pattern Matching**: Pub/sub with wildcard pattern support
25
- * - **Throttling**: Dynamic flow control and backpressure management
26
- * - **Retry Policies**: PostgreSQL-native retry configuration with exponential backoff
22
+ * Each HotMesh instance joins a **quorum** a real-time pub/sub channel
23
+ * backed by Postgres LISTEN/NOTIFY. The quorum is the mesh's nervous
24
+ * system: version activations, throttle commands, roll calls, and custom
25
+ * user messages all propagate instantly to every connected engine and
26
+ * worker across all processes and servers.
27
27
  *
28
- * ## Architecture
28
+ * ## Quick Start
29
29
  *
30
- * HotMesh consists of several specialized modules:
31
- * - **HotMesh**: Core orchestration engine (this class)
32
- * - **MemFlow**: Temporal.io-compatible workflow framework
33
- * - **MeshCall**: Durable function execution (Temporal-like clone)
34
- *
35
- * ## Lifecycle Overview
36
- *
37
- * 1. **Initialize**: Create HotMesh instance with provider configuration
38
- * 2. **Deploy**: Upload YAML workflow definitions to the backend
39
- * 3. **Activate**: Coordinate quorum to enable the workflow version
40
- * 4. **Execute**: Publish events to trigger workflow execution
41
- * 5. **Monitor**: Track progress via OpenTelemetry and built-in observability
42
- *
43
- * ## Basic Usage
44
- *
45
- * @example
46
30
  * ```typescript
47
31
  * import { HotMesh } from '@hotmeshio/hotmesh';
48
32
  * import { Client as Postgres } from 'pg';
49
33
  *
50
- * // Initialize with Postgres backend
51
34
  * const hotMesh = await HotMesh.init({
52
- * appId: 'my-app',
35
+ * appId: 'myapp',
53
36
  * engine: {
54
37
  * connection: {
55
38
  * class: Postgres,
56
- * options: {
57
- * connectionString: 'postgresql://user:pass@localhost:5432/db'
58
- * }
59
- * }
60
- * }
39
+ * options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' },
40
+ * },
41
+ * },
42
+ * workers: [{
43
+ * topic: 'order.process',
44
+ * connection: {
45
+ * class: Postgres,
46
+ * options: { connectionString: 'postgresql://usr:pwd@localhost:5432/db' },
47
+ * },
48
+ * callback: async (data) => ({
49
+ * metadata: { ...data.metadata },
50
+ * data: { orderId: data.data.id, status: 'fulfilled' },
51
+ * }),
52
+ * }],
61
53
  * });
62
54
  *
63
- * // Deploy workflow definition
55
+ * // Deploy a YAML workflow graph
64
56
  * await hotMesh.deploy(`
65
57
  * app:
66
- * id: my-app
58
+ * id: myapp
67
59
  * version: '1'
68
60
  * graphs:
69
- * - subscribes: order.process
61
+ * - subscribes: order.placed
62
+ * publishes: order.fulfilled
63
+ * expire: 600
64
+ *
70
65
  * activities:
71
- * validate:
72
- * type: worker
73
- * topic: order.validate
74
- * approve:
75
- * type: hook
76
- * topic: order.approve
77
- * fulfill:
66
+ * t1:
67
+ * type: trigger
68
+ * process:
78
69
  * type: worker
79
- * topic: order.fulfill
70
+ * topic: order.process
71
+ * input:
72
+ * schema:
73
+ * type: object
74
+ * properties:
75
+ * id: { type: string }
76
+ * maps:
77
+ * id: '{t1.output.data.id}'
80
78
  * transitions:
81
- * validate:
82
- * - to: approve
83
- * approve:
84
- * - to: fulfill
79
+ * t1:
80
+ * - to: process
85
81
  * `);
86
82
  *
87
- * // Activate the workflow version
88
83
  * await hotMesh.activate('1');
89
84
  *
90
- * // Execute workflow (fire-and-forget)
91
- * const jobId = await hotMesh.pub('order.process', {
92
- * orderId: '12345',
93
- * amount: 99.99
94
- * });
85
+ * // Fire-and-forget
86
+ * const jobId = await hotMesh.pub('order.placed', { id: 'ORD-123' });
95
87
  *
96
- * // Execute workflow and wait for result
97
- * const result = await hotMesh.pubsub('order.process', {
98
- * orderId: '12345',
99
- * amount: 99.99
100
- * });
88
+ * // Request/response (blocks until workflow completes)
89
+ * const result = await hotMesh.pubsub('order.placed', { id: 'ORD-456' });
101
90
  * ```
102
91
  *
103
- * ## Postgres Backend Example
92
+ * ## Quorum: The Mesh Control Plane
104
93
  *
105
- * @example
106
- * ```typescript
107
- * import { HotMesh } from '@hotmeshio/hotmesh';
108
- * import { Client as Postgres } from 'pg';
94
+ * The quorum channel is a broadcast bus available to every mesh member.
95
+ * Use it for operational control, observability, and custom messaging.
109
96
  *
110
- * const hotMesh = await HotMesh.init({
111
- * appId: 'my-app',
112
- * engine: {
113
- * connection: {
114
- * class: Postgres,
115
- * options: {
116
- * connectionString: 'postgresql://user:pass@localhost:5432/db'
117
- * }
118
- * }
97
+ * ```typescript
98
+ * // Roll call — discover every engine and worker in the mesh
99
+ * const members = await hotMesh.rollCall();
100
+ * // => [{ engine_id, worker_topic, throttle, system: { CPULoad, ... } }, ...]
101
+ *
102
+ * // Subscribe to ALL quorum traffic (throttle, activate, pong, job, user)
103
+ * await hotMesh.subQuorum((topic, message) => {
104
+ * switch (message.type) {
105
+ * case 'pong': // roll call response from a mesh member
106
+ * console.log(`Member ${message.guid} on topic ${message.profile?.worker_topic}`);
107
+ * break;
108
+ * case 'throttle': // a throttle command was broadcast
109
+ * console.log(`Throttle ${message.throttle}ms on ${message.topic ?? 'all'}`);
110
+ * break;
111
+ * case 'activate': // a version activation is in progress
112
+ * console.log(`Activating version ${message.until_version}`);
113
+ * break;
114
+ * case 'job': // a workflow completed and published its result
115
+ * console.log(`Job done on ${message.topic}:`, message.job);
116
+ * break;
117
+ * case 'user': // a custom user message
118
+ * console.log(`User event ${message.topic}:`, message.message);
119
+ * break;
119
120
  * }
120
121
  * });
121
- * ```
122
122
  *
123
- * ## Advanced Features
124
- *
125
- * **Pattern Subscriptions**: Listen to multiple workflow topics
126
- * ```typescript
127
- * await hotMesh.psub('order.*', (topic, message) => {
128
- * console.log(`Received ${topic}:`, message);
123
+ * // Publish a custom message to every mesh member
124
+ * await hotMesh.pubQuorum({
125
+ * type: 'user',
126
+ * topic: 'deploy.notify',
127
+ * message: { version: '2.1.0', deployer: 'ci-pipeline' },
129
128
  * });
130
129
  * ```
131
130
  *
132
- * **Throttling**: Control processing rates
133
- * ```typescript
134
- * // Pause all processing for 5 seconds
135
- * await hotMesh.throttle({ throttle: 5000 });
136
- *
137
- * // Emergency stop (pause indefinitely)
138
- * await hotMesh.throttle({ throttle: -1 });
139
- * ```
131
+ * ## Throttling: Backpressure Across the Mesh
140
132
  *
141
- * **Workflow Interruption**: Gracefully stop running workflows
142
- * ```typescript
143
- * await hotMesh.interrupt('order.process', jobId, {
144
- * reason: 'User cancellation'
145
- * });
146
- * ```
133
+ * Throttle commands propagate instantly to every targeted member via
134
+ * the quorum channel, providing fine-grained flow control.
147
135
  *
148
- * **State Inspection**: Query workflow state and progress
149
136
  * ```typescript
150
- * const state = await hotMesh.getState('order.process', jobId);
151
- * const status = await hotMesh.getStatus(jobId);
152
- * ```
137
+ * // Pause the ENTIRE mesh (emergency stop)
138
+ * await hotMesh.throttle({ throttle: -1 });
153
139
  *
154
- * ## Distributed Coordination
140
+ * // Resume the entire mesh
141
+ * await hotMesh.throttle({ throttle: 0 });
155
142
  *
156
- * HotMesh automatically handles distributed coordination through its quorum system:
143
+ * // Slow a specific worker topic to 1 message per 500ms
144
+ * await hotMesh.throttle({ throttle: 500, topic: 'order.process' });
157
145
  *
158
- * ```typescript
159
- * // Check quorum health
160
- * const members = await hotMesh.rollCall();
146
+ * // Throttle a single engine/worker instance by GUID
147
+ * await hotMesh.throttle({ throttle: 2000, guid: 'abc-123' });
161
148
  *
162
- * // Coordinate version activation across all instances
163
- * await hotMesh.activate('2', 1000); // 1 second delay for consensus
149
+ * // Combine: throttle a specific topic on a specific instance
150
+ * await hotMesh.throttle({ throttle: 1000, guid: 'abc-123', topic: 'order.process' });
164
151
  * ```
165
152
  *
166
- * ## Integration with Higher-Level Modules
153
+ * ## Lifecycle
167
154
  *
168
- * For most use cases, consider using the higher-level modules:
169
- * - **MemFlow**: For Temporal.io-style workflows with TypeScript functions
170
- * - **MeshCall**: For durable function calls and RPC patterns
155
+ * 1. **`init`** Create an engine + workers; join the quorum.
156
+ * 2. **`deploy`** Upload a YAML graph to Postgres (inactive).
157
+ * 3. **`activate`** Coordinate the quorum to switch to the new version.
158
+ * 4. **`pub` / `pubsub`** — Trigger workflow execution.
159
+ * 5. **`stop`** — Leave the quorum and release connections.
171
160
  *
172
- * ## Cleanup
161
+ * ## Higher-Level Modules
173
162
  *
174
- * Always clean up resources when shutting down:
175
- * ```typescript
176
- * // Stop this instance
177
- * hotMesh.stop();
163
+ * For most use cases, prefer the higher-level wrappers:
164
+ * - **MemFlow** — Temporal-style durable workflow functions.
165
+ * - **MeshCall** Durable function calls and RPC patterns.
178
166
  *
179
- * // Stop all instances (typically in signal handlers)
180
- * await HotMesh.stop();
181
- * ```
182
- *
183
- * @see {@link https://docs.hotmesh.io/} - Complete documentation
184
- * @see {@link https://github.com/hotmeshio/samples-typescript} - Examples and tutorials
185
- * @see {@link https://zenodo.org/records/12168558} - White paper on the architecture
167
+ * @see {@link https://hotmeshio.github.io/sdk-typescript/} - API reference
186
168
  */
187
169
  class HotMesh {
188
170
  /**
@@ -220,12 +202,19 @@ class HotMesh {
220
202
  *
221
203
  * ## Retry Policy Configuration
222
204
  *
223
- * HotMesh supports robust retry policies with exponential backoff for PostgreSQL.
224
- * Configure retry behavior at the stream level for automatic fault tolerance.
205
+ * HotMesh supports retry policies with exponential backoff. Retry behavior
206
+ * can be configured independently on both the `engine` and individual
207
+ * `workers`. They are **not inherited**; each operates at its own level.
208
+ *
209
+ * - **Engine `retryPolicy`**: Stamps messages the engine publishes with
210
+ * retry metadata (stored as Postgres columns). Workers that consume
211
+ * these messages will use the embedded config when handling failures.
212
+ * - **Worker `retryPolicy`**: Used as the fallback when the consumed
213
+ * message does not carry explicit retry metadata.
225
214
  *
226
215
  * @example Basic Configuration
227
216
  * ```typescript
228
- * const config: HotMeshConfig = {
217
+ * const hotMesh = await HotMesh.init({
229
218
  * appId: 'myapp',
230
219
  * engine: {
231
220
  * connection: {
@@ -235,46 +224,49 @@ class HotMesh {
235
224
  * }
236
225
  * }
237
226
  * },
238
- * workers [...]
239
- * };
240
- * const hotMesh = await HotMesh.init(config);
227
+ * workers: [...]
228
+ * });
241
229
  * ```
242
230
  *
243
- * @example With Retry Policy (PostgreSQL)
231
+ * @example Engine Retry Policy
244
232
  * ```typescript
245
- * import { HotMesh } from '@hotmeshio/hotmesh';
246
- * import { Client as Postgres } from 'pg';
247
- *
248
233
  * const hotMesh = await HotMesh.init({
249
- * appId: 'my-app',
234
+ * appId: 'myapp',
250
235
  * engine: {
251
236
  * connection: {
252
237
  * class: Postgres,
253
238
  * options: { connectionString: 'postgresql://...' }
254
239
  * },
255
- * // Default retry policy for engine streams
256
240
  * retryPolicy: {
257
- * maximumAttempts: 5, // Retry up to 5 times
258
- * backoffCoefficient: 2, // Exponential: 2^0, 2^1, 2^2...
259
- * maximumInterval: '300s' // Cap delay at 5 minutes
241
+ * maximumAttempts: 5,
242
+ * backoffCoefficient: 2,
243
+ * maximumInterval: '300s'
260
244
  * }
261
- * },
245
+ * }
246
+ * });
247
+ * ```
248
+ *
249
+ * @example Worker Retry Policy
250
+ * ```typescript
251
+ * const hotMesh = await HotMesh.init({
252
+ * appId: 'myapp',
253
+ * engine: { connection },
262
254
  * workers: [{
263
255
  * topic: 'order.process',
264
- * connection: {
265
- * class: Postgres,
266
- * options: { connectionString: 'postgresql://...' }
267
- * },
268
- * // Worker-specific retry policy
256
+ * connection,
269
257
  * retryPolicy: {
270
- * maximumAttempts: 10,
271
- * backoffCoefficient: 1.5,
272
- * maximumInterval: '600s'
258
+ * maximumAttempts: 5,
259
+ * backoffCoefficient: 2,
260
+ * maximumInterval: '30s',
273
261
  * },
274
- * callback: async (data) => {
275
- * // Your business logic here
276
- * // Failures will automatically retry with exponential backoff
277
- * return { status: 'success', data: processedData };
262
+ * callback: async (data: StreamData) => {
263
+ * const result = await doWork(data.data);
264
+ * return {
265
+ * code: 200,
266
+ * status: StreamStatus.SUCCESS,
267
+ * metadata: { ...data.metadata },
268
+ * data: { result },
269
+ * } as StreamDataResponse;
278
270
  * }
279
271
  * }]
280
272
  * });
@@ -303,8 +295,8 @@ class HotMesh {
303
295
  return instance;
304
296
  }
305
297
  /**
306
- * returns a guid using the same core guid
307
- * generator used by the HotMesh (nanoid)
298
+ * Generates a unique ID using the same nanoid generator used
299
+ * internally by HotMesh for job IDs and GUIDs.
308
300
  */
309
301
  static guid() {
310
302
  return (0, utils_1.guid)();
@@ -408,24 +400,31 @@ class HotMesh {
408
400
  }
409
401
  // ************* PUB/SUB METHODS *************
410
402
  /**
411
- * Starts a workflow
403
+ * Publishes a message to a workflow topic, starting a new job.
404
+ * Returns the job ID immediately (fire-and-forget). Use `pubsub`
405
+ * to block until the workflow completes.
406
+ *
412
407
  * @example
413
408
  * ```typescript
414
- * await hotMesh.pub('a.b.c', { key: 'value' });
409
+ * const jobId = await hotMesh.pub('order.placed', {
410
+ * id: 'ORD-123',
411
+ * amount: 99.99,
412
+ * });
413
+ * console.log(`Started job ${jobId}`);
415
414
  * ```
416
415
  */
417
416
  async pub(topic, data = {}, context, extended) {
418
417
  return await this.engine?.pub(topic, data, context, extended);
419
418
  }
420
419
  /**
421
- * Subscribe (listen) to all output and interim emissions of a single
422
- * workflow topic. NOTE: Postgres does not support patterned
423
- * unsubscription, so this method is not supported for Postgres.
420
+ * Subscribes to all output and interim emissions from a specific
421
+ * workflow topic. The callback fires each time a job on that topic
422
+ * completes or emits an interim result.
424
423
  *
425
424
  * @example
426
425
  * ```typescript
427
- * await hotMesh.psub('a.b.c', (topic, message) => {
428
- * console.log(message);
426
+ * await hotMesh.sub('order.fulfilled', (topic, message) => {
427
+ * console.log(`Order completed:`, message.data);
429
428
  * });
430
429
  * ```
431
430
  */
@@ -433,18 +432,21 @@ class HotMesh {
433
432
  return await this.engine?.sub(topic, callback);
434
433
  }
435
434
  /**
436
- * Stop listening in on a single workflow topic
435
+ * Unsubscribes from a single workflow topic previously registered
436
+ * with `sub()`.
437
437
  */
438
438
  async unsub(topic) {
439
439
  return await this.engine?.unsub(topic);
440
440
  }
441
441
  /**
442
- * Listen to all output and interim emissions of a workflow topic
443
- * matching a wildcard pattern.
442
+ * Subscribes to workflow emissions matching a wildcard pattern.
443
+ * Useful for monitoring an entire domain of workflows at once.
444
+ *
444
445
  * @example
445
446
  * ```typescript
446
- * await hotMesh.psub('a.b.c*', (topic, message) => {
447
- * console.log(message);
447
+ * // Listen to all order-related workflow completions
448
+ * await hotMesh.psub('order.*', (topic, message) => {
449
+ * console.log(`${topic} completed:`, message.data);
448
450
  * });
449
451
  * ```
450
452
  */
@@ -452,56 +454,107 @@ class HotMesh {
452
454
  return await this.engine?.psub(wild, callback);
453
455
  }
454
456
  /**
455
- * Patterned unsubscribe. NOTE: Postgres does not support patterned
456
- * unsubscription, so this method is not supported for Postgres.
457
+ * Unsubscribes from a wildcard pattern previously registered with `psub()`.
457
458
  */
458
459
  async punsub(wild) {
459
460
  return await this.engine?.punsub(wild);
460
461
  }
461
462
  /**
462
- * Starts a workflow and awaits the response
463
+ * Publishes a message to a workflow topic and blocks until the workflow
464
+ * completes, returning the final job output. Internally subscribes to
465
+ * the workflow's `publishes` topic before publishing, then unsubscribes
466
+ * after receiving the result.
467
+ *
463
468
  * @example
464
469
  * ```typescript
465
- * await hotMesh.pubsub('a.b.c', { key: 'value' });
470
+ * const result = await hotMesh.pubsub('order.placed', {
471
+ * id: 'ORD-789',
472
+ * amount: 49.99,
473
+ * });
474
+ * console.log('Order result:', result.data);
466
475
  * ```
467
476
  */
468
477
  async pubsub(topic, data = {}, context, timeout) {
469
478
  return await this.engine?.pubsub(topic, data, context, timeout);
470
479
  }
471
480
  /**
472
- * Add a transition message to the workstream, resuming leg 2 of a paused
473
- * reentrant activity (e.g., await, worker, hook)
481
+ * Adds a transition message to the workstream, resuming Leg 2 of a
482
+ * paused reentrant activity (e.g., `await`, `worker`, `hook`). This
483
+ * is typically called by the engine internally but is exposed for
484
+ * advanced use cases like custom activity implementations.
474
485
  */
475
486
  async add(streamData) {
476
487
  return (await this.engine.add(streamData));
477
488
  }
478
489
  // ************* QUORUM METHODS *************
479
490
  /**
480
- * Request a roll call from the quorum (engine and workers)
491
+ * Broadcasts a roll call across the mesh and collects responses from
492
+ * every connected engine and worker. Each member replies with its
493
+ * `QuorumProfile` — including GUID, worker topic, stream depth,
494
+ * throttle rate, and system health (CPU, memory, network).
495
+ *
496
+ * Use this for service discovery, health checks, and capacity planning.
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * const members = await hotMesh.rollCall();
501
+ * for (const member of members) {
502
+ * console.log(
503
+ * `${member.engine_id} | topic=${member.worker_topic ?? 'engine'} ` +
504
+ * `| throttle=${member.throttle}ms | depth=${member.stream_depth}`,
505
+ * );
506
+ * }
507
+ * ```
481
508
  */
482
509
  async rollCall(delay) {
483
510
  return await this.quorum?.rollCall(delay);
484
511
  }
485
512
  /**
486
- * Sends a throttle message to the quorum (engine and/or workers)
487
- * to limit the rate of processing. Pass `-1` to throttle indefinitely.
488
- * The value must be a non-negative integer and not exceed `MAX_DELAY` ms.
513
+ * Broadcasts a throttle command to the mesh via the quorum channel.
514
+ * Targeted members insert a delay (in milliseconds) before processing
515
+ * their next stream message, providing instant backpressure control
516
+ * across any combination of engines and workers.
517
+ *
518
+ * Throttling is **stateless** — no data is lost. Messages accumulate
519
+ * in Postgres streams and are processed once the throttle is lifted.
489
520
  *
490
- * When throttling is set, the quorum will pause for the specified time
491
- * before processing the next message. Target specific engines and
492
- * workers by passing a `guid` and/or `topic`. Pass no arguments to
493
- * throttle the entire quorum.
521
+ * ## Targeting
494
522
  *
495
- * In this example, all processing has been paused indefinitely for
496
- * the entire quorum. This is equivalent to an emergency stop.
523
+ * | Option | Effect |
524
+ * |---------|--------|
525
+ * | *(none)* | Throttle the **entire mesh** (all engines + all workers) |
526
+ * | `topic` | Throttle all workers subscribed to this topic |
527
+ * | `guid` | Throttle a single engine or worker instance |
528
+ * | `topic` + `guid` | Throttle a specific topic on a specific instance |
497
529
  *
498
- * HotMesh is a stateless sequence engine, so the throttle can be adjusted up
499
- * and down with no loss of data.
530
+ * ## Special Values
500
531
  *
532
+ * | Value | Effect |
533
+ * |--------|--------|
534
+ * | `0` | Resume normal processing (remove throttle) |
535
+ * | `-1` | Pause indefinitely (emergency stop) |
536
+ * | `500` | 500ms delay between messages |
501
537
  *
502
538
  * @example
503
539
  * ```typescript
540
+ * // Emergency stop: pause the entire mesh
504
541
  * await hotMesh.throttle({ throttle: -1 });
542
+ *
543
+ * // Resume the entire mesh
544
+ * await hotMesh.throttle({ throttle: 0 });
545
+ *
546
+ * // Slow a specific worker topic to 1 msg per second
547
+ * await hotMesh.throttle({ throttle: 1000, topic: 'order.process' });
548
+ *
549
+ * // Throttle a single instance by GUID
550
+ * await hotMesh.throttle({ throttle: 2000, guid: 'abc-123' });
551
+ *
552
+ * // Throttle a specific topic on a specific instance
553
+ * await hotMesh.throttle({
554
+ * throttle: 500,
555
+ * guid: 'abc-123',
556
+ * topic: 'payment.charge',
557
+ * });
505
558
  * ```
506
559
  */
507
560
  async throttle(options) {
@@ -529,19 +582,114 @@ class HotMesh {
529
582
  return await this.quorum?.pub(throttleMessage);
530
583
  }
531
584
  /**
532
- * Publish a message to the quorum (engine and/or workers)
585
+ * Publishes a message to every mesh member via the quorum channel
586
+ * (Postgres LISTEN/NOTIFY). Any `QuorumMessage` type can be sent,
587
+ * but the `user` type is the most common for application-level
588
+ * messaging.
589
+ *
590
+ * @example
591
+ * ```typescript
592
+ * // Broadcast a custom event to all mesh members
593
+ * await hotMesh.pubQuorum({
594
+ * type: 'user',
595
+ * topic: 'deploy.notify',
596
+ * message: { version: '2.1.0', deployer: 'ci-pipeline' },
597
+ * });
598
+ *
599
+ * // Broadcast a config-reload signal
600
+ * await hotMesh.pubQuorum({
601
+ * type: 'user',
602
+ * topic: 'config.reload',
603
+ * message: { features: { darkMode: true } },
604
+ * });
605
+ * ```
533
606
  */
534
607
  async pubQuorum(quorumMessage) {
535
608
  return await this.quorum?.pub(quorumMessage);
536
609
  }
537
610
  /**
538
- * Subscribe to quorum events (engine and workers)
611
+ * Subscribes to the quorum channel, receiving **every** message
612
+ * broadcast across the mesh in real time. This is the primary
613
+ * observability hook into the service mesh — use it to monitor
614
+ * version activations, throttle commands, roll call responses,
615
+ * workflow completions, and custom user events.
616
+ *
617
+ * Messages arrive as typed `QuorumMessage` unions. Switch on
618
+ * `message.type` to handle each:
619
+ *
620
+ * | Type | When it fires |
621
+ * |-------------|---------------|
622
+ * | `pong` | A mesh member responds to a roll call |
623
+ * | `throttle` | A throttle command was broadcast |
624
+ * | `activate` | A version activation is in progress |
625
+ * | `job` | A workflow completed and published its result |
626
+ * | `user` | A custom user message (via `pubQuorum`) |
627
+ * | `ping` | A roll call was initiated |
628
+ * | `work` | A work distribution event |
629
+ * | `cron` | A cron/scheduled event |
630
+ *
631
+ * @example
632
+ * ```typescript
633
+ * // Build a real-time mesh dashboard
634
+ * await hotMesh.subQuorum((topic, message) => {
635
+ * switch (message.type) {
636
+ * case 'pong':
637
+ * dashboard.updateMember(message.guid, {
638
+ * topic: message.profile?.worker_topic,
639
+ * throttle: message.profile?.throttle,
640
+ * depth: message.profile?.stream_depth,
641
+ * cpu: message.profile?.system?.CPULoad,
642
+ * });
643
+ * break;
644
+ *
645
+ * case 'throttle':
646
+ * dashboard.logThrottle(
647
+ * message.throttle,
648
+ * message.topic,
649
+ * message.guid,
650
+ * );
651
+ * break;
652
+ *
653
+ * case 'job':
654
+ * dashboard.logCompletion(message.topic, message.job);
655
+ * break;
656
+ *
657
+ * case 'user':
658
+ * dashboard.logUserEvent(message.topic, message.message);
659
+ * break;
660
+ * }
661
+ * });
662
+ * ```
663
+ *
664
+ * @example
665
+ * ```typescript
666
+ * // React to custom deployment events
667
+ * await hotMesh.subQuorum((topic, message) => {
668
+ * if (message.type === 'user' && message.topic === 'config.reload') {
669
+ * reloadFeatureFlags(message.message);
670
+ * }
671
+ * });
672
+ * ```
673
+ *
674
+ * @example
675
+ * ```typescript
676
+ * // Log all mesh activity for audit
677
+ * await hotMesh.subQuorum((topic, message) => {
678
+ * auditLog.append({
679
+ * timestamp: Date.now(),
680
+ * type: message.type,
681
+ * guid: message.guid,
682
+ * topic: message.topic,
683
+ * payload: message,
684
+ * });
685
+ * });
686
+ * ```
539
687
  */
540
688
  async subQuorum(callback) {
541
689
  return await this.quorum?.sub(callback);
542
690
  }
543
691
  /**
544
- * Unsubscribe from quorum events (engine and workers)
692
+ * Unsubscribes a callback previously registered with `subQuorum()`.
545
693
  */
546
694
  async unsubQuorum(callback) {
547
695
  return await this.quorum?.unsub(callback);
@@ -556,43 +704,78 @@ class HotMesh {
556
704
  return await this.engine?.plan(path);
557
705
  }
558
706
  /**
559
- * When the app YAML descriptor file is ready, the `deploy` function can be called.
560
- * This function is responsible for merging all referenced YAML source
561
- * files and writing the JSON output to the file system and to the provider backend. It
562
- * is also possible to embed the YAML in-line as a string.
707
+ * Deploys a YAML workflow graph to Postgres. Accepts a file path or
708
+ * an inline YAML string. Referenced `$ref` files are resolved and
709
+ * merged. The deployed version is **inactive** until `activate()` is
710
+ * called.
711
+ *
712
+ * @example
713
+ * ```typescript
714
+ * // Deploy from an inline YAML string
715
+ * await hotMesh.deploy(`
716
+ * app:
717
+ * id: myapp
718
+ * version: '2'
719
+ * graphs:
720
+ * - subscribes: order.placed
721
+ * activities:
722
+ * t1:
723
+ * type: trigger
724
+ * process:
725
+ * type: worker
726
+ * topic: order.process
727
+ * transitions:
728
+ * t1:
729
+ * - to: process
730
+ * `);
563
731
  *
564
- * *The version will not be active until activation is explicitly called.*
732
+ * // Deploy from a file path (resolves $ref references)
733
+ * await hotMesh.deploy('./workflows/order.yaml');
734
+ * ```
565
735
  */
566
736
  async deploy(pathOrYAML) {
567
737
  return await this.engine?.deploy(pathOrYAML);
568
738
  }
569
739
  /**
570
- * Once the app YAML file is deployed to the provider backend, the `activate` function can be
571
- * called to enable it for the entire quorum at the same moment.
740
+ * Activates a deployed version across the entire mesh. The quorum
741
+ * coordinates a synchronized switch-over:
742
+ *
743
+ * 1. Roll call to verify quorum health.
744
+ * 2. Broadcast `nocache` mode — all engines consult Postgres for the
745
+ * active version on every request.
746
+ * 3. Set the new version as active.
747
+ * 4. Broadcast `cache` mode — engines resume caching.
748
+ *
749
+ * The optional `delay` adds a pause (in ms) for the quorum to reach
750
+ * consensus under heavy traffic. Combine with `throttle()` for
751
+ * zero-downtime version switches.
572
752
  *
573
- * The approach is to establish the coordinated health of the system through series
574
- * of call/response exchanges. Once it is established that the quorum is healthy,
575
- * the quorum is instructed to run their engine in `no-cache` mode, ensuring
576
- * that the provider backend is consulted for the active app version each time a
577
- * call is processed. This ensures that all engines are running the same version
578
- * of the app, switching over at the same moment and then enabling `cache` mode
579
- * to improve performance.
753
+ * @example
754
+ * ```typescript
755
+ * // Simple activation
756
+ * await hotMesh.activate('2');
580
757
  *
581
- * *Add a delay for the quorum to reach consensus if traffic is busy, but
582
- * also consider throttling traffic flow to an acceptable level.*
758
+ * // With consensus delay under heavy traffic
759
+ * await hotMesh.throttle({ throttle: 500 }); // slow the mesh
760
+ * await hotMesh.activate('2', 2000); // activate with 2s consensus window
761
+ * await hotMesh.throttle({ throttle: 0 }); // resume full speed
762
+ * ```
583
763
  */
584
764
  async activate(version, delay) {
585
765
  return await this.quorum?.activate(version, delay);
586
766
  }
587
767
  /**
588
- * Returns the job state as a JSON object, useful
589
- * for understanding dependency chains
768
+ * Exports the full job state as a structured JSON object, including
769
+ * activity data, transitions, and dependency chains. Useful for
770
+ * debugging, auditing, and visualizing workflow execution.
590
771
  */
591
772
  async export(jobId) {
592
773
  return await this.engine?.export(jobId);
593
774
  }
594
775
  /**
595
- * Returns all data (HGETALL) for a job.
776
+ * Returns all raw key-value pairs for a job's HASH record. This is
777
+ * the lowest-level read — it returns internal engine fields alongside
778
+ * user data. Prefer `getState()` for structured output.
596
779
  */
597
780
  async getRaw(jobId) {
598
781
  return await this.engine?.getRaw(jobId);
@@ -605,36 +788,42 @@ class HotMesh {
605
788
  return await this.engine?.getStats(topic, query);
606
789
  }
607
790
  /**
608
- * Returns the status of a job. This is a numeric
609
- * semaphore value that indicates the job's state.
610
- * Any non-positive value indicates a completed job.
611
- * Jobs with a value of `-1` are pending and will
612
- * automatically be scrubbed after a set period.
613
- * Jobs a value around -1billion have been interrupted
614
- * and will be scrubbed after a set period. Jobs with
615
- * a value of 0 completed normally. Jobs with a
616
- * positive value are still running.
791
+ * Returns the numeric status semaphore for a job.
792
+ *
793
+ * | Value | Meaning |
794
+ * |------------------|---------|
795
+ * | `> 0` | Running (count of open activities) |
796
+ * | `0` | Completed normally |
797
+ * | `-1` | Pending (awaiting activation) |
798
+ * | `< -100,000,000` | Interrupted (abnormal termination) |
617
799
  */
618
800
  async getStatus(jobId) {
619
801
  return this.engine?.getStatus(jobId);
620
802
  }
621
803
  /**
622
- * Returns the job state (data and metadata) for a job.
804
+ * Returns the structured job state (data and metadata) for a job,
805
+ * scoped to the given workflow topic.
806
+ *
807
+ * @example
808
+ * ```typescript
809
+ * const state = await hotMesh.getState('order.placed', jobId);
810
+ * console.log(state.data); // workflow output data
811
+ * console.log(state.metadata); // jid, aid, timestamps, etc.
812
+ * ```
623
813
  */
624
814
  async getState(topic, jobId) {
625
815
  return this.engine?.getState(topic, jobId);
626
816
  }
627
817
  /**
628
- * Returns searchable/queryable data for a job. In this
629
- * example a literal field is also searched (the colon
630
- * is used to track job status and is a reserved field;
631
- * it can be read but not written).
818
+ * Returns specific searchable fields from a job's HASH record.
819
+ * Pass field names to retrieve; use `":"` to read the reserved
820
+ * status field.
632
821
  *
633
822
  * @example
634
823
  * ```typescript
635
- * const fields = ['fred', 'barney', '":"'];
636
- * const queryState = await hotMesh.getQueryState('123', fields);
637
- * //returns { fred: 'flintstone', barney: 'rubble', ':': '1' }
824
+ * const fields = ['orderId', 'status', '":"'];
825
+ * const data = await hotMesh.getQueryState(jobId, fields);
826
+ * // => { orderId: 'ORD-123', status: 'paid', ':': '0' }
638
827
  * ```
639
828
  */
640
829
  async getQueryState(jobId, fields) {
@@ -653,39 +842,83 @@ class HotMesh {
653
842
  return await this.engine?.resolveQuery(topic, query);
654
843
  }
655
844
  /**
656
- * Interrupt an active job
845
+ * Interrupts (terminates) an active workflow job. The job's status is
846
+ * set to an error code indicating abnormal termination, and any pending
847
+ * activities or timers are cancelled.
848
+ *
849
+ * @example
850
+ * ```typescript
851
+ * await hotMesh.interrupt('order.placed', jobId, {
852
+ * reason: 'Customer cancelled',
853
+ * descend: true, // also interrupt child workflows
854
+ * });
855
+ * ```
657
856
  */
658
857
  async interrupt(topic, jobId, options = {}) {
659
858
  return await this.engine?.interrupt(topic, jobId, options);
660
859
  }
661
860
  /**
662
- * Immediately deletes (DEL) a completed job from the system.
663
- *
664
- * *Scrubbed jobs must be complete with a non-positive `status` value*
861
+ * Immediately deletes a completed job from the system. The job must
862
+ * have a non-positive status (completed or interrupted). Running jobs
863
+ * must be interrupted first.
665
864
  */
666
865
  async scrub(jobId) {
667
866
  await this.engine?.scrub(jobId);
668
867
  }
669
868
  /**
670
- * Re/entry point for an active job. This is used to resume a paused job
671
- * and close the reentry point or leave it open for subsequent reentry.
672
- * Because `hooks` are public entry points, they include a `topic`
673
- * which is established in the app YAML file.
869
+ * Sends a signal to a paused workflow, resuming its execution.
870
+ * The `topic` must match a hook rule defined in the YAML graph's
871
+ * `hooks` section. The engine locates the exact activity and
872
+ * dimension for reentry based on the hook rule's match conditions.
674
873
  *
675
- * When this method is called, a hook rule will be located to establish
676
- * the exact activity and activity dimension for reentry.
874
+ * Use this to deliver external data (approval decisions, webhook
875
+ * payloads, partner responses) into a workflow that is sleeping
876
+ * on a hook activity or awaiting a `waitFor()` signal.
877
+ *
878
+ * @example
879
+ * ```typescript
880
+ * // Resume a paused approval workflow with external data
881
+ * await hotMesh.signal('order.approval', {
882
+ * id: jobId,
883
+ * approved: true,
884
+ * reviewer: 'manager@example.com',
885
+ * });
886
+ * ```
887
+ *
888
+ * @example
889
+ * ```typescript
890
+ * // Signal a MemFlow workflow waiting on waitFor('payment-received')
891
+ * await hotMesh.signal(`${appId}.wfs.signal`, {
892
+ * id: 'payment-received',
893
+ * data: { amount: 99.99, currency: 'USD' },
894
+ * });
895
+ * ```
677
896
  */
678
- async hook(topic, data, status, code) {
679
- return await this.engine?.hook(topic, data, status, code);
897
+ async signal(topic, data, status, code) {
898
+ return await this.engine?.signal(topic, data, status, code);
680
899
  }
681
900
  /**
901
+ * Fan-out variant of `signal()` that delivers data to **all**
902
+ * paused workflows matching a search query. Useful for resuming
903
+ * a batch of workflows waiting on the same external event.
904
+ *
682
905
  * @private
683
906
  */
684
- async hookAll(hookTopic, data, query, queryFacets = []) {
685
- return await this.engine?.hookAll(hookTopic, data, query, queryFacets);
907
+ async signalAll(hookTopic, data, query, queryFacets = []) {
908
+ return await this.engine?.signalAll(hookTopic, data, query, queryFacets);
686
909
  }
687
910
  /**
688
- * Stop all points of presence, workers and engines
911
+ * Stops **all** HotMesh instances in the current process — engines,
912
+ * workers, and connections. Typically called in signal handlers
913
+ * (`SIGTERM`, `SIGINT`) for graceful shutdown.
914
+ *
915
+ * @example
916
+ * ```typescript
917
+ * process.on('SIGTERM', async () => {
918
+ * await HotMesh.stop();
919
+ * process.exit(0);
920
+ * });
921
+ * ```
689
922
  */
690
923
  static async stop() {
691
924
  if (!this.disconnecting) {
@@ -695,7 +928,9 @@ class HotMesh {
695
928
  }
696
929
  }
697
930
  /**
698
- * Stop this point of presence, workers and engines
931
+ * Stops this specific HotMesh instance its engine, quorum
932
+ * membership, and all attached workers. Other instances in the
933
+ * same process are unaffected.
699
934
  */
700
935
  stop() {
701
936
  this.engine?.taskService.cancelCleanup();