@hotmeshio/hotmesh 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +178 -43
- package/build/index.d.ts +12 -11
- package/build/index.js +15 -13
- package/build/modules/enums.d.ts +23 -34
- package/build/modules/enums.js +26 -38
- package/build/modules/errors.d.ts +16 -16
- package/build/modules/errors.js +37 -37
- package/build/package.json +63 -67
- package/build/services/activities/activity.d.ts +58 -7
- package/build/services/activities/activity.js +67 -38
- package/build/services/activities/await.d.ts +101 -0
- package/build/services/activities/await.js +101 -0
- package/build/services/activities/cycle.d.ts +82 -0
- package/build/services/activities/cycle.js +86 -8
- package/build/services/activities/hook.d.ts +139 -1
- package/build/services/activities/hook.js +140 -2
- package/build/services/activities/interrupt.d.ts +112 -0
- package/build/services/activities/interrupt.js +118 -5
- package/build/services/activities/signal.d.ts +108 -3
- package/build/services/activities/signal.js +113 -8
- package/build/services/activities/trigger.d.ts +56 -4
- package/build/services/activities/trigger.js +119 -35
- package/build/services/activities/worker.d.ts +107 -0
- package/build/services/activities/worker.js +107 -0
- package/build/services/collator/index.d.ts +3 -15
- package/build/services/collator/index.js +7 -34
- package/build/services/dba/index.d.ts +171 -0
- package/build/services/dba/index.js +280 -0
- package/build/services/{memflow → durable}/client.d.ts +3 -3
- package/build/services/{memflow → durable}/client.js +15 -15
- package/build/services/{memflow → durable}/connection.d.ts +2 -2
- package/build/services/{memflow → durable}/connection.js +1 -1
- package/build/services/{memflow → durable}/exporter.d.ts +6 -6
- package/build/services/{memflow → durable}/exporter.js +2 -2
- package/build/services/{memflow → durable}/handle.d.ts +4 -4
- package/build/services/{memflow → durable}/handle.js +3 -3
- package/build/services/{memflow → durable}/index.d.ts +126 -34
- package/build/services/{memflow → durable}/index.js +146 -50
- package/build/services/{memflow → durable}/interceptor.d.ts +45 -22
- package/build/services/{memflow → durable}/interceptor.js +54 -21
- package/build/services/{memflow → durable}/schemas/factory.d.ts +4 -4
- package/build/services/{memflow → durable}/schemas/factory.js +5 -5
- package/build/services/{memflow → durable}/search.d.ts +1 -1
- package/build/services/{memflow → durable}/search.js +4 -4
- package/build/services/{memflow → durable}/worker.d.ts +11 -11
- package/build/services/{memflow → durable}/worker.js +61 -60
- package/build/services/durable/workflow/all.d.ts +32 -0
- package/build/services/durable/workflow/all.js +40 -0
- package/build/services/{memflow → durable}/workflow/common.d.ts +5 -5
- package/build/services/durable/workflow/common.js +47 -0
- package/build/services/durable/workflow/context.d.ts +49 -0
- package/build/services/durable/workflow/context.js +88 -0
- package/build/services/durable/workflow/didRun.d.ts +27 -0
- package/build/services/durable/workflow/didRun.js +42 -0
- package/build/services/durable/workflow/emit.d.ts +50 -0
- package/build/services/durable/workflow/emit.js +68 -0
- package/build/services/durable/workflow/enrich.d.ts +37 -0
- package/build/services/durable/workflow/enrich.js +45 -0
- package/build/services/durable/workflow/entityMethods.d.ts +61 -0
- package/build/services/durable/workflow/entityMethods.js +80 -0
- package/build/services/durable/workflow/execChild.d.ts +106 -0
- package/build/services/durable/workflow/execChild.js +194 -0
- package/build/services/durable/workflow/execHook.d.ts +80 -0
- package/build/services/durable/workflow/execHook.js +97 -0
- package/build/services/durable/workflow/execHookBatch.d.ts +107 -0
- package/build/services/durable/workflow/execHookBatch.js +129 -0
- package/build/services/durable/workflow/hook.d.ts +74 -0
- package/build/services/durable/workflow/hook.js +123 -0
- package/build/services/durable/workflow/index.d.ts +129 -0
- package/build/services/{memflow → durable}/workflow/index.js +66 -11
- package/build/services/durable/workflow/interrupt.d.ts +55 -0
- package/build/services/durable/workflow/interrupt.js +70 -0
- package/build/services/durable/workflow/interruption.d.ts +61 -0
- package/build/services/durable/workflow/interruption.js +76 -0
- package/build/services/durable/workflow/isSideEffectAllowed.d.ts +27 -0
- package/build/services/{memflow → durable}/workflow/isSideEffectAllowed.js +21 -4
- package/build/services/durable/workflow/proxyActivities.d.ts +119 -0
- package/build/services/durable/workflow/proxyActivities.js +214 -0
- package/build/services/durable/workflow/random.d.ts +36 -0
- package/build/services/durable/workflow/random.js +46 -0
- package/build/services/durable/workflow/searchMethods.d.ts +53 -0
- package/build/services/durable/workflow/searchMethods.js +72 -0
- package/build/services/durable/workflow/signal.d.ts +58 -0
- package/build/services/durable/workflow/signal.js +79 -0
- package/build/services/durable/workflow/sleepFor.d.ts +63 -0
- package/build/services/durable/workflow/sleepFor.js +91 -0
- package/build/services/durable/workflow/trace.d.ts +47 -0
- package/build/services/durable/workflow/trace.js +66 -0
- package/build/services/durable/workflow/waitFor.d.ts +66 -0
- package/build/services/durable/workflow/waitFor.js +93 -0
- package/build/services/engine/index.d.ts +18 -2
- package/build/services/engine/index.js +14 -4
- package/build/services/exporter/index.d.ts +2 -0
- package/build/services/exporter/index.js +1 -0
- package/build/services/hotmesh/index.d.ts +471 -236
- package/build/services/hotmesh/index.js +473 -238
- package/build/services/store/index.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.d.ts +1 -1
- package/build/services/store/providers/postgres/postgres.js +4 -3
- package/build/services/telemetry/index.js +6 -0
- package/build/services/{meshcall → virtual}/index.d.ts +29 -29
- package/build/services/{meshcall → virtual}/index.js +49 -49
- package/build/services/{meshcall → virtual}/schemas/factory.d.ts +1 -1
- package/build/services/{meshcall → virtual}/schemas/factory.js +1 -1
- package/build/types/activity.d.ts +1 -1
- package/build/types/dba.d.ts +64 -0
- package/build/types/{memflow.d.ts → durable.d.ts} +75 -19
- package/build/types/error.d.ts +5 -5
- package/build/types/exporter.d.ts +1 -1
- package/build/types/hotmesh.d.ts +1 -1
- package/build/types/index.d.ts +5 -4
- package/build/types/job.d.ts +1 -1
- package/build/types/quorum.d.ts +2 -2
- package/build/types/{meshcall.d.ts → virtual.d.ts} +15 -15
- package/build/types/virtual.js +2 -0
- package/index.ts +15 -13
- package/package.json +63 -67
- package/vitest.config.ts +17 -0
- package/.claude/settings.local.json +0 -7
- package/build/services/memflow/workflow/all.d.ts +0 -7
- package/build/services/memflow/workflow/all.js +0 -15
- package/build/services/memflow/workflow/common.js +0 -47
- package/build/services/memflow/workflow/context.d.ts +0 -6
- package/build/services/memflow/workflow/context.js +0 -45
- package/build/services/memflow/workflow/didRun.d.ts +0 -7
- package/build/services/memflow/workflow/didRun.js +0 -22
- package/build/services/memflow/workflow/emit.d.ts +0 -11
- package/build/services/memflow/workflow/emit.js +0 -29
- package/build/services/memflow/workflow/enrich.d.ts +0 -9
- package/build/services/memflow/workflow/enrich.js +0 -17
- package/build/services/memflow/workflow/entityMethods.d.ts +0 -14
- package/build/services/memflow/workflow/entityMethods.js +0 -33
- package/build/services/memflow/workflow/execChild.d.ts +0 -18
- package/build/services/memflow/workflow/execChild.js +0 -106
- package/build/services/memflow/workflow/execHook.d.ts +0 -65
- package/build/services/memflow/workflow/execHook.js +0 -83
- package/build/services/memflow/workflow/execHookBatch.d.ts +0 -54
- package/build/services/memflow/workflow/execHookBatch.js +0 -77
- package/build/services/memflow/workflow/hook.d.ts +0 -9
- package/build/services/memflow/workflow/hook.js +0 -58
- package/build/services/memflow/workflow/index.d.ts +0 -74
- package/build/services/memflow/workflow/interrupt.d.ts +0 -9
- package/build/services/memflow/workflow/interrupt.js +0 -24
- package/build/services/memflow/workflow/interruption.d.ts +0 -28
- package/build/services/memflow/workflow/interruption.js +0 -43
- package/build/services/memflow/workflow/isSideEffectAllowed.d.ts +0 -10
- package/build/services/memflow/workflow/proxyActivities.d.ts +0 -91
- package/build/services/memflow/workflow/proxyActivities.js +0 -176
- package/build/services/memflow/workflow/random.d.ts +0 -6
- package/build/services/memflow/workflow/random.js +0 -16
- package/build/services/memflow/workflow/searchMethods.d.ts +0 -6
- package/build/services/memflow/workflow/searchMethods.js +0 -25
- package/build/services/memflow/workflow/signal.d.ts +0 -29
- package/build/services/memflow/workflow/signal.js +0 -50
- package/build/services/memflow/workflow/sleepFor.d.ts +0 -24
- package/build/services/memflow/workflow/sleepFor.js +0 -52
- package/build/services/memflow/workflow/trace.d.ts +0 -14
- package/build/services/memflow/workflow/trace.js +0 -33
- package/build/services/memflow/workflow/waitFor.d.ts +0 -29
- package/build/services/memflow/workflow/waitFor.js +0 -56
- /package/build/services/{memflow → durable}/entity.d.ts +0 -0
- /package/build/services/{memflow → durable}/entity.js +0 -0
- /package/build/types/{memflow.js → dba.js} +0 -0
- /package/build/types/{meshcall.js → durable.js} +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hook = void 0;
|
|
4
|
+
const common_1 = require("./common");
|
|
5
|
+
const context_1 = require("./context");
|
|
6
|
+
const isSideEffectAllowed_1 = require("./isSideEffectAllowed");
|
|
7
|
+
/**
|
|
8
|
+
* Spawns a hook execution against an existing workflow job. The hook runs
|
|
9
|
+
* in an isolated dimensional thread within the target job's namespace,
|
|
10
|
+
* allowing it to read/write the same job state without interfering with
|
|
11
|
+
* the main workflow thread.
|
|
12
|
+
*
|
|
13
|
+
* This is the low-level primitive behind `execHook()`. Use `hook()`
|
|
14
|
+
* directly when you need fire-and-forget hook execution or when you
|
|
15
|
+
* manage signal coordination yourself.
|
|
16
|
+
*
|
|
17
|
+
* ## Target Resolution
|
|
18
|
+
*
|
|
19
|
+
* - If `taskQueue` and `workflowName` (or `entity`) are provided, the
|
|
20
|
+
* hook targets that specific workflow type.
|
|
21
|
+
* - If neither is provided, the hook targets the **current** workflow.
|
|
22
|
+
* However, targeting the same topic as the current workflow is
|
|
23
|
+
* rejected to prevent infinite loops.
|
|
24
|
+
*
|
|
25
|
+
* ## Idempotency
|
|
26
|
+
*
|
|
27
|
+
* The `isSideEffectAllowed` guard ensures hooks fire exactly once —
|
|
28
|
+
* on replay, the hook is not re-spawned.
|
|
29
|
+
*
|
|
30
|
+
* ## Examples
|
|
31
|
+
*
|
|
32
|
+
* ```typescript
|
|
33
|
+
* import { Durable } from '@hotmeshio/hotmesh';
|
|
34
|
+
*
|
|
35
|
+
* // Fire-and-forget: spawn a hook without waiting for its result
|
|
36
|
+
* export async function notifyWorkflow(userId: string): Promise<void> {
|
|
37
|
+
* await Durable.workflow.hook({
|
|
38
|
+
* taskQueue: 'notifications',
|
|
39
|
+
* workflowName: 'sendNotification',
|
|
40
|
+
* args: [userId, 'Your order has shipped'],
|
|
41
|
+
* });
|
|
42
|
+
* // Continues immediately, does not wait for the hook
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // Manual signal coordination (equivalent to execHook)
|
|
48
|
+
* export async function manualHookPattern(itemId: string): Promise<string> {
|
|
49
|
+
* const signalId = `process-${itemId}`;
|
|
50
|
+
*
|
|
51
|
+
* await Durable.workflow.hook({
|
|
52
|
+
* taskQueue: 'processors',
|
|
53
|
+
* workflowName: 'processItem',
|
|
54
|
+
* args: [itemId, signalId],
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Manually wait for the hook to signal back
|
|
58
|
+
* return await Durable.workflow.waitFor<string>(signalId);
|
|
59
|
+
* }
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* ```typescript
|
|
63
|
+
* // Hook with retry configuration
|
|
64
|
+
* await Durable.workflow.hook({
|
|
65
|
+
* taskQueue: 'enrichment',
|
|
66
|
+
* workflowName: 'enrichProfile',
|
|
67
|
+
* args: [profileId],
|
|
68
|
+
* config: {
|
|
69
|
+
* maximumAttempts: 5,
|
|
70
|
+
* backoffCoefficient: 2,
|
|
71
|
+
* maximumInterval: '1m',
|
|
72
|
+
* },
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @param {HookOptions} options - Hook configuration including target workflow and arguments.
|
|
77
|
+
* @returns {Promise<string>} The resulting hook/stream ID.
|
|
78
|
+
*/
|
|
79
|
+
async function hook(options) {
|
|
80
|
+
const { workflowId, connection, namespace, workflowTopic } = (0, context_1.getContext)();
|
|
81
|
+
const hotMeshClient = await common_1.WorkerService.getHotMesh(workflowTopic, {
|
|
82
|
+
connection,
|
|
83
|
+
namespace,
|
|
84
|
+
});
|
|
85
|
+
if (await (0, isSideEffectAllowed_1.isSideEffectAllowed)(hotMeshClient, 'hook')) {
|
|
86
|
+
const targetWorkflowId = options.workflowId ?? workflowId;
|
|
87
|
+
let targetTopic;
|
|
88
|
+
if (options.entity || (options.taskQueue && options.workflowName)) {
|
|
89
|
+
targetTopic = `${options.taskQueue ?? options.entity}-${options.entity ?? options.workflowName}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
targetTopic = workflowTopic;
|
|
93
|
+
}
|
|
94
|
+
// DEFENSIVE CHECK: Prevent infinite loops
|
|
95
|
+
if (targetTopic === workflowTopic &&
|
|
96
|
+
!options.entity &&
|
|
97
|
+
!options.taskQueue) {
|
|
98
|
+
throw new Error(`Durable Hook Error: Potential infinite loop detected!\n\n` +
|
|
99
|
+
`The hook would target the same workflow topic ('${workflowTopic}') as the current workflow, ` +
|
|
100
|
+
`creating an infinite loop.\n\n` +
|
|
101
|
+
`To fix this, provide either:\n` +
|
|
102
|
+
`1. 'taskQueue' parameter: Durable.workflow.hook({ taskQueue: 'your-queue', workflowName: '${options.workflowName}', args: [...] })\n` +
|
|
103
|
+
`2. 'entity' parameter: Durable.workflow.hook({ entity: 'your-entity', args: [...] })\n\n` +
|
|
104
|
+
`Current workflow topic: ${workflowTopic}\n` +
|
|
105
|
+
`Target topic would be: ${targetTopic}\n` +
|
|
106
|
+
`Provided options: ${JSON.stringify({
|
|
107
|
+
workflowName: options.workflowName,
|
|
108
|
+
taskQueue: options.taskQueue,
|
|
109
|
+
entity: options.entity,
|
|
110
|
+
}, null, 2)}`);
|
|
111
|
+
}
|
|
112
|
+
const payload = {
|
|
113
|
+
arguments: [...options.args],
|
|
114
|
+
id: targetWorkflowId,
|
|
115
|
+
workflowTopic: targetTopic,
|
|
116
|
+
backoffCoefficient: options.config?.backoffCoefficient || common_1.HMSH_DURABLE_EXP_BACKOFF,
|
|
117
|
+
maximumAttempts: options.config?.maximumAttempts || common_1.HMSH_DURABLE_MAX_ATTEMPTS,
|
|
118
|
+
maximumInterval: (0, common_1.s)(options?.config?.maximumInterval ?? common_1.HMSH_DURABLE_MAX_INTERVAL),
|
|
119
|
+
};
|
|
120
|
+
return await hotMeshClient.signal(`${namespace}.flow.signal`, payload, common_1.StreamStatus.PENDING, 202);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.hook = hook;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { getContext } from './context';
|
|
2
|
+
import { didRun } from './didRun';
|
|
3
|
+
import { isSideEffectAllowed } from './isSideEffectAllowed';
|
|
4
|
+
import { trace } from './trace';
|
|
5
|
+
import { enrich } from './enrich';
|
|
6
|
+
import { emit } from './emit';
|
|
7
|
+
import { execChild, startChild } from './execChild';
|
|
8
|
+
import { execHook } from './execHook';
|
|
9
|
+
import { execHookBatch } from './execHookBatch';
|
|
10
|
+
import { proxyActivities } from './proxyActivities';
|
|
11
|
+
import { search } from './searchMethods';
|
|
12
|
+
import { random } from './random';
|
|
13
|
+
import { signal } from './signal';
|
|
14
|
+
import { hook } from './hook';
|
|
15
|
+
import { interrupt } from './interrupt';
|
|
16
|
+
import { didInterrupt } from './interruption';
|
|
17
|
+
import { all } from './all';
|
|
18
|
+
import { sleepFor } from './sleepFor';
|
|
19
|
+
import { waitFor } from './waitFor';
|
|
20
|
+
import { HotMesh } from './common';
|
|
21
|
+
import { entity } from './entityMethods';
|
|
22
|
+
/**
|
|
23
|
+
* The workflow-internal API surface, exposed as `Durable.workflow`. Every
|
|
24
|
+
* method on this class is designed to be called **inside** a workflow
|
|
25
|
+
* function — they participate in deterministic replay and durable state
|
|
26
|
+
* management.
|
|
27
|
+
*
|
|
28
|
+
* ## Core Primitives
|
|
29
|
+
*
|
|
30
|
+
* | Method | Purpose |
|
|
31
|
+
* |--------|---------|
|
|
32
|
+
* | {@link proxyActivities} | Create durable activity proxies with retry |
|
|
33
|
+
* | {@link sleepFor} | Durable, crash-safe sleep |
|
|
34
|
+
* | {@link waitFor} | Pause until a signal is received |
|
|
35
|
+
* | {@link signal} | Send data to a waiting workflow |
|
|
36
|
+
* | {@link execChild} | Spawn and await a child workflow |
|
|
37
|
+
* | {@link startChild} | Spawn a child workflow (fire-and-forget) |
|
|
38
|
+
* | {@link execHook} | Spawn a hook and await its signal response |
|
|
39
|
+
* | {@link execHookBatch} | Spawn multiple hooks in parallel |
|
|
40
|
+
* | {@link hook} | Low-level hook spawning |
|
|
41
|
+
* | {@link interrupt} | Terminate a running workflow |
|
|
42
|
+
*
|
|
43
|
+
* ## Data & Observability
|
|
44
|
+
*
|
|
45
|
+
* | Method | Purpose |
|
|
46
|
+
* |--------|---------|
|
|
47
|
+
* | {@link search} | Read/write flat HASH key-value data |
|
|
48
|
+
* | {@link enrich} | One-shot HASH enrichment |
|
|
49
|
+
* | {@link entity} | Structured JSONB document storage |
|
|
50
|
+
* | {@link emit} | Publish events to the event bus |
|
|
51
|
+
* | {@link trace} | Emit OpenTelemetry trace spans |
|
|
52
|
+
*
|
|
53
|
+
* ## Utilities
|
|
54
|
+
*
|
|
55
|
+
* | Method | Purpose |
|
|
56
|
+
* |--------|---------|
|
|
57
|
+
* | {@link getContext} | Access workflow ID, namespace, replay state |
|
|
58
|
+
* | {@link random} | Deterministic pseudo-random numbers |
|
|
59
|
+
* | {@link all} | Workflow-safe `Promise.all` |
|
|
60
|
+
* | {@link didInterrupt} | Type guard for engine control-flow errors |
|
|
61
|
+
*
|
|
62
|
+
* ## Example
|
|
63
|
+
*
|
|
64
|
+
* ```typescript
|
|
65
|
+
* import { Durable } from '@hotmeshio/hotmesh';
|
|
66
|
+
* import * as activities from './activities';
|
|
67
|
+
*
|
|
68
|
+
* export async function orderWorkflow(orderId: string): Promise<string> {
|
|
69
|
+
* // Proxy activities for durable execution
|
|
70
|
+
* const { validateOrder, processPayment, sendReceipt } =
|
|
71
|
+
* Durable.workflow.proxyActivities<typeof activities>({
|
|
72
|
+
* activities,
|
|
73
|
+
* retryPolicy: { maximumAttempts: 3 },
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* await validateOrder(orderId);
|
|
77
|
+
*
|
|
78
|
+
* // Durable sleep (survives restarts)
|
|
79
|
+
* await Durable.workflow.sleepFor('5 seconds');
|
|
80
|
+
*
|
|
81
|
+
* const receipt = await processPayment(orderId);
|
|
82
|
+
*
|
|
83
|
+
* // Store searchable metadata
|
|
84
|
+
* await Durable.workflow.enrich({ orderId, status: 'paid' });
|
|
85
|
+
*
|
|
86
|
+
* // Wait for external approval signal
|
|
87
|
+
* const approval = await Durable.workflow.waitFor<{ ok: boolean }>('approve');
|
|
88
|
+
* if (!approval.ok) return 'cancelled';
|
|
89
|
+
*
|
|
90
|
+
* await sendReceipt(orderId, receipt);
|
|
91
|
+
* return receipt;
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare class WorkflowService {
|
|
96
|
+
/**
|
|
97
|
+
* @private
|
|
98
|
+
* The constructor is private to prevent instantiation;
|
|
99
|
+
* all methods are static.
|
|
100
|
+
*/
|
|
101
|
+
private constructor();
|
|
102
|
+
static getContext: typeof getContext;
|
|
103
|
+
static didRun: typeof didRun;
|
|
104
|
+
static isSideEffectAllowed: typeof isSideEffectAllowed;
|
|
105
|
+
static trace: typeof trace;
|
|
106
|
+
static enrich: typeof enrich;
|
|
107
|
+
static emit: typeof emit;
|
|
108
|
+
static execChild: typeof execChild;
|
|
109
|
+
static executeChild: typeof execChild;
|
|
110
|
+
static startChild: typeof startChild;
|
|
111
|
+
static execHook: typeof execHook;
|
|
112
|
+
static execHookBatch: typeof execHookBatch;
|
|
113
|
+
static proxyActivities: typeof proxyActivities;
|
|
114
|
+
static search: typeof search;
|
|
115
|
+
static entity: typeof entity;
|
|
116
|
+
static random: typeof random;
|
|
117
|
+
static signal: typeof signal;
|
|
118
|
+
static hook: typeof hook;
|
|
119
|
+
static didInterrupt: typeof didInterrupt;
|
|
120
|
+
static interrupt: typeof interrupt;
|
|
121
|
+
static all: typeof all;
|
|
122
|
+
static sleepFor: typeof sleepFor;
|
|
123
|
+
static waitFor: typeof waitFor;
|
|
124
|
+
/**
|
|
125
|
+
* Return a handle to the HotMesh client hosting the workflow execution.
|
|
126
|
+
* @returns {Promise<HotMesh>} The HotMesh client instance.
|
|
127
|
+
*/
|
|
128
|
+
static getHotMesh(): Promise<HotMesh>;
|
|
129
|
+
}
|
|
@@ -23,20 +23,75 @@ const waitFor_1 = require("./waitFor");
|
|
|
23
23
|
const common_1 = require("./common");
|
|
24
24
|
const entityMethods_1 = require("./entityMethods");
|
|
25
25
|
/**
|
|
26
|
-
* The
|
|
27
|
-
*
|
|
28
|
-
*
|
|
26
|
+
* The workflow-internal API surface, exposed as `Durable.workflow`. Every
|
|
27
|
+
* method on this class is designed to be called **inside** a workflow
|
|
28
|
+
* function — they participate in deterministic replay and durable state
|
|
29
|
+
* management.
|
|
30
|
+
*
|
|
31
|
+
* ## Core Primitives
|
|
32
|
+
*
|
|
33
|
+
* | Method | Purpose |
|
|
34
|
+
* |--------|---------|
|
|
35
|
+
* | {@link proxyActivities} | Create durable activity proxies with retry |
|
|
36
|
+
* | {@link sleepFor} | Durable, crash-safe sleep |
|
|
37
|
+
* | {@link waitFor} | Pause until a signal is received |
|
|
38
|
+
* | {@link signal} | Send data to a waiting workflow |
|
|
39
|
+
* | {@link execChild} | Spawn and await a child workflow |
|
|
40
|
+
* | {@link startChild} | Spawn a child workflow (fire-and-forget) |
|
|
41
|
+
* | {@link execHook} | Spawn a hook and await its signal response |
|
|
42
|
+
* | {@link execHookBatch} | Spawn multiple hooks in parallel |
|
|
43
|
+
* | {@link hook} | Low-level hook spawning |
|
|
44
|
+
* | {@link interrupt} | Terminate a running workflow |
|
|
45
|
+
*
|
|
46
|
+
* ## Data & Observability
|
|
47
|
+
*
|
|
48
|
+
* | Method | Purpose |
|
|
49
|
+
* |--------|---------|
|
|
50
|
+
* | {@link search} | Read/write flat HASH key-value data |
|
|
51
|
+
* | {@link enrich} | One-shot HASH enrichment |
|
|
52
|
+
* | {@link entity} | Structured JSONB document storage |
|
|
53
|
+
* | {@link emit} | Publish events to the event bus |
|
|
54
|
+
* | {@link trace} | Emit OpenTelemetry trace spans |
|
|
55
|
+
*
|
|
56
|
+
* ## Utilities
|
|
57
|
+
*
|
|
58
|
+
* | Method | Purpose |
|
|
59
|
+
* |--------|---------|
|
|
60
|
+
* | {@link getContext} | Access workflow ID, namespace, replay state |
|
|
61
|
+
* | {@link random} | Deterministic pseudo-random numbers |
|
|
62
|
+
* | {@link all} | Workflow-safe `Promise.all` |
|
|
63
|
+
* | {@link didInterrupt} | Type guard for engine control-flow errors |
|
|
64
|
+
*
|
|
65
|
+
* ## Example
|
|
29
66
|
*
|
|
30
|
-
* @example
|
|
31
67
|
* ```typescript
|
|
32
|
-
* import {
|
|
68
|
+
* import { Durable } from '@hotmeshio/hotmesh';
|
|
69
|
+
* import * as activities from './activities';
|
|
70
|
+
*
|
|
71
|
+
* export async function orderWorkflow(orderId: string): Promise<string> {
|
|
72
|
+
* // Proxy activities for durable execution
|
|
73
|
+
* const { validateOrder, processPayment, sendReceipt } =
|
|
74
|
+
* Durable.workflow.proxyActivities<typeof activities>({
|
|
75
|
+
* activities,
|
|
76
|
+
* retryPolicy: { maximumAttempts: 3 },
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* await validateOrder(orderId);
|
|
80
|
+
*
|
|
81
|
+
* // Durable sleep (survives restarts)
|
|
82
|
+
* await Durable.workflow.sleepFor('5 seconds');
|
|
83
|
+
*
|
|
84
|
+
* const receipt = await processPayment(orderId);
|
|
85
|
+
*
|
|
86
|
+
* // Store searchable metadata
|
|
87
|
+
* await Durable.workflow.enrich({ orderId, status: 'paid' });
|
|
88
|
+
*
|
|
89
|
+
* // Wait for external approval signal
|
|
90
|
+
* const approval = await Durable.workflow.waitFor<{ ok: boolean }>('approve');
|
|
91
|
+
* if (!approval.ok) return 'cancelled';
|
|
33
92
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* MemFlow.workflow.waitFor<boolean>('my-sig-nal-1'),
|
|
37
|
-
* MemFlow.workflow.waitFor<number>('my-sig-nal-2')
|
|
38
|
-
* ]);
|
|
39
|
-
* return [s1, s2];
|
|
93
|
+
* await sendReceipt(orderId, receipt);
|
|
94
|
+
* return receipt;
|
|
40
95
|
* }
|
|
41
96
|
* ```
|
|
42
97
|
*/
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { JobInterruptOptions } from './common';
|
|
2
|
+
/**
|
|
3
|
+
* Terminates a running workflow job by its ID. The target job's status
|
|
4
|
+
* is set to an error code indicating abnormal termination, and any
|
|
5
|
+
* pending activities or timers are cancelled.
|
|
6
|
+
*
|
|
7
|
+
* This is the workflow-internal interrupt — it can only be called from
|
|
8
|
+
* within a workflow function. For external interruption, use
|
|
9
|
+
* `hotMesh.interrupt()` directly.
|
|
10
|
+
*
|
|
11
|
+
* The interrupt fires exactly once per workflow execution — the
|
|
12
|
+
* `isSideEffectAllowed` guard prevents re-interrupting on replay.
|
|
13
|
+
*
|
|
14
|
+
* ## Examples
|
|
15
|
+
*
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { Durable } from '@hotmeshio/hotmesh';
|
|
18
|
+
*
|
|
19
|
+
* // Cancel a child workflow from the parent
|
|
20
|
+
* export async function supervisorWorkflow(): Promise<void> {
|
|
21
|
+
* const childId = await Durable.workflow.startChild({
|
|
22
|
+
* taskQueue: 'workers',
|
|
23
|
+
* workflowName: 'longTask',
|
|
24
|
+
* args: [],
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Wait for a timeout, then cancel the child
|
|
28
|
+
* await Durable.workflow.sleepFor('5 minutes');
|
|
29
|
+
* await Durable.workflow.interrupt(childId, {
|
|
30
|
+
* reason: 'Timed out waiting for child',
|
|
31
|
+
* descend: true, // also interrupt any grandchild workflows
|
|
32
|
+
* });
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* ```typescript
|
|
37
|
+
* // Self-interrupt on validation failure
|
|
38
|
+
* export async function validatedWorkflow(input: string): Promise<void> {
|
|
39
|
+
* const { workflowId } = Durable.workflow.getContext();
|
|
40
|
+
* const { validate } = Durable.workflow.proxyActivities<typeof activities>();
|
|
41
|
+
*
|
|
42
|
+
* const isValid = await validate(input);
|
|
43
|
+
* if (!isValid) {
|
|
44
|
+
* await Durable.workflow.interrupt(workflowId, {
|
|
45
|
+
* reason: 'Invalid input',
|
|
46
|
+
* });
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* @param {string} jobId - The ID of the workflow job to interrupt.
|
|
52
|
+
* @param {JobInterruptOptions} [options={}] - Interruption options (`reason`, `descend`, etc.).
|
|
53
|
+
* @returns {Promise<string | void>} The result of the interruption, if any.
|
|
54
|
+
*/
|
|
55
|
+
export declare function interrupt(jobId: string, options?: JobInterruptOptions): Promise<string | void>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.interrupt = void 0;
|
|
4
|
+
const common_1 = require("./common");
|
|
5
|
+
const context_1 = require("./context");
|
|
6
|
+
const isSideEffectAllowed_1 = require("./isSideEffectAllowed");
|
|
7
|
+
/**
|
|
8
|
+
* Terminates a running workflow job by its ID. The target job's status
|
|
9
|
+
* is set to an error code indicating abnormal termination, and any
|
|
10
|
+
* pending activities or timers are cancelled.
|
|
11
|
+
*
|
|
12
|
+
* This is the workflow-internal interrupt — it can only be called from
|
|
13
|
+
* within a workflow function. For external interruption, use
|
|
14
|
+
* `hotMesh.interrupt()` directly.
|
|
15
|
+
*
|
|
16
|
+
* The interrupt fires exactly once per workflow execution — the
|
|
17
|
+
* `isSideEffectAllowed` guard prevents re-interrupting on replay.
|
|
18
|
+
*
|
|
19
|
+
* ## Examples
|
|
20
|
+
*
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { Durable } from '@hotmeshio/hotmesh';
|
|
23
|
+
*
|
|
24
|
+
* // Cancel a child workflow from the parent
|
|
25
|
+
* export async function supervisorWorkflow(): Promise<void> {
|
|
26
|
+
* const childId = await Durable.workflow.startChild({
|
|
27
|
+
* taskQueue: 'workers',
|
|
28
|
+
* workflowName: 'longTask',
|
|
29
|
+
* args: [],
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Wait for a timeout, then cancel the child
|
|
33
|
+
* await Durable.workflow.sleepFor('5 minutes');
|
|
34
|
+
* await Durable.workflow.interrupt(childId, {
|
|
35
|
+
* reason: 'Timed out waiting for child',
|
|
36
|
+
* descend: true, // also interrupt any grandchild workflows
|
|
37
|
+
* });
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* ```typescript
|
|
42
|
+
* // Self-interrupt on validation failure
|
|
43
|
+
* export async function validatedWorkflow(input: string): Promise<void> {
|
|
44
|
+
* const { workflowId } = Durable.workflow.getContext();
|
|
45
|
+
* const { validate } = Durable.workflow.proxyActivities<typeof activities>();
|
|
46
|
+
*
|
|
47
|
+
* const isValid = await validate(input);
|
|
48
|
+
* if (!isValid) {
|
|
49
|
+
* await Durable.workflow.interrupt(workflowId, {
|
|
50
|
+
* reason: 'Invalid input',
|
|
51
|
+
* });
|
|
52
|
+
* }
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*
|
|
56
|
+
* @param {string} jobId - The ID of the workflow job to interrupt.
|
|
57
|
+
* @param {JobInterruptOptions} [options={}] - Interruption options (`reason`, `descend`, etc.).
|
|
58
|
+
* @returns {Promise<string | void>} The result of the interruption, if any.
|
|
59
|
+
*/
|
|
60
|
+
async function interrupt(jobId, options = {}) {
|
|
61
|
+
const { workflowTopic, connection, namespace } = (0, context_1.getContext)();
|
|
62
|
+
const hotMeshClient = await common_1.WorkerService.getHotMesh(workflowTopic, {
|
|
63
|
+
connection,
|
|
64
|
+
namespace,
|
|
65
|
+
});
|
|
66
|
+
if (await (0, isSideEffectAllowed_1.isSideEffectAllowed)(hotMeshClient, 'interrupt')) {
|
|
67
|
+
return await hotMeshClient.interrupt(`${hotMeshClient.appId}.execute`, jobId, options);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.interrupt = interrupt;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard that returns `true` if an error is a Durable engine
|
|
3
|
+
* control-flow signal rather than a genuine application error.
|
|
4
|
+
*
|
|
5
|
+
* Durable uses thrown errors internally to suspend workflow execution
|
|
6
|
+
* for durable operations like `sleepFor`, `waitFor`, `proxyActivities`,
|
|
7
|
+
* and `execChild`. These errors must be re-thrown (not swallowed) so
|
|
8
|
+
* the engine can persist state and schedule the next step.
|
|
9
|
+
*
|
|
10
|
+
* **Always use `didInterrupt` in `catch` blocks inside workflow
|
|
11
|
+
* functions** to avoid accidentally swallowing engine signals.
|
|
12
|
+
*
|
|
13
|
+
* ## Recognized Error Types
|
|
14
|
+
*
|
|
15
|
+
* `DurableChildError`, `DurableFatalError`, `DurableMaxedError`,
|
|
16
|
+
* `DurableProxyError`, `DurableRetryError`, `DurableSleepError`,
|
|
17
|
+
* `DurableTimeoutError`, `DurableWaitForError`, `DurableWaitForAllError`
|
|
18
|
+
*
|
|
19
|
+
* ## Examples
|
|
20
|
+
*
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { Durable } from '@hotmeshio/hotmesh';
|
|
23
|
+
*
|
|
24
|
+
* export async function safeWorkflow(): Promise<string> {
|
|
25
|
+
* const { riskyOperation } = Durable.workflow.proxyActivities<typeof activities>();
|
|
26
|
+
*
|
|
27
|
+
* try {
|
|
28
|
+
* return await riskyOperation();
|
|
29
|
+
* } catch (error) {
|
|
30
|
+
* // CRITICAL: re-throw engine signals
|
|
31
|
+
* if (Durable.workflow.didInterrupt(error)) {
|
|
32
|
+
* throw error;
|
|
33
|
+
* }
|
|
34
|
+
* // Handle real application errors
|
|
35
|
+
* return 'fallback-value';
|
|
36
|
+
* }
|
|
37
|
+
* }
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* ```typescript
|
|
41
|
+
* // Common pattern in interceptors
|
|
42
|
+
* const interceptor: WorkflowInterceptor = {
|
|
43
|
+
* async execute(ctx, next) {
|
|
44
|
+
* try {
|
|
45
|
+
* return await next();
|
|
46
|
+
* } catch (error) {
|
|
47
|
+
* if (Durable.workflow.didInterrupt(error)) {
|
|
48
|
+
* throw error; // always re-throw engine signals
|
|
49
|
+
* }
|
|
50
|
+
* // Log and re-throw application errors
|
|
51
|
+
* console.error('Workflow failed:', error);
|
|
52
|
+
* throw error;
|
|
53
|
+
* }
|
|
54
|
+
* },
|
|
55
|
+
* };
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @param {Error} error - The error to check.
|
|
59
|
+
* @returns {boolean} `true` if the error is a Durable engine interruption signal.
|
|
60
|
+
*/
|
|
61
|
+
export declare function didInterrupt(error: Error): boolean;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.didInterrupt = void 0;
|
|
4
|
+
const errors_1 = require("../../../modules/errors");
|
|
5
|
+
/**
|
|
6
|
+
* Type guard that returns `true` if an error is a Durable engine
|
|
7
|
+
* control-flow signal rather than a genuine application error.
|
|
8
|
+
*
|
|
9
|
+
* Durable uses thrown errors internally to suspend workflow execution
|
|
10
|
+
* for durable operations like `sleepFor`, `waitFor`, `proxyActivities`,
|
|
11
|
+
* and `execChild`. These errors must be re-thrown (not swallowed) so
|
|
12
|
+
* the engine can persist state and schedule the next step.
|
|
13
|
+
*
|
|
14
|
+
* **Always use `didInterrupt` in `catch` blocks inside workflow
|
|
15
|
+
* functions** to avoid accidentally swallowing engine signals.
|
|
16
|
+
*
|
|
17
|
+
* ## Recognized Error Types
|
|
18
|
+
*
|
|
19
|
+
* `DurableChildError`, `DurableFatalError`, `DurableMaxedError`,
|
|
20
|
+
* `DurableProxyError`, `DurableRetryError`, `DurableSleepError`,
|
|
21
|
+
* `DurableTimeoutError`, `DurableWaitForError`, `DurableWaitForAllError`
|
|
22
|
+
*
|
|
23
|
+
* ## Examples
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { Durable } from '@hotmeshio/hotmesh';
|
|
27
|
+
*
|
|
28
|
+
* export async function safeWorkflow(): Promise<string> {
|
|
29
|
+
* const { riskyOperation } = Durable.workflow.proxyActivities<typeof activities>();
|
|
30
|
+
*
|
|
31
|
+
* try {
|
|
32
|
+
* return await riskyOperation();
|
|
33
|
+
* } catch (error) {
|
|
34
|
+
* // CRITICAL: re-throw engine signals
|
|
35
|
+
* if (Durable.workflow.didInterrupt(error)) {
|
|
36
|
+
* throw error;
|
|
37
|
+
* }
|
|
38
|
+
* // Handle real application errors
|
|
39
|
+
* return 'fallback-value';
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* ```typescript
|
|
45
|
+
* // Common pattern in interceptors
|
|
46
|
+
* const interceptor: WorkflowInterceptor = {
|
|
47
|
+
* async execute(ctx, next) {
|
|
48
|
+
* try {
|
|
49
|
+
* return await next();
|
|
50
|
+
* } catch (error) {
|
|
51
|
+
* if (Durable.workflow.didInterrupt(error)) {
|
|
52
|
+
* throw error; // always re-throw engine signals
|
|
53
|
+
* }
|
|
54
|
+
* // Log and re-throw application errors
|
|
55
|
+
* console.error('Workflow failed:', error);
|
|
56
|
+
* throw error;
|
|
57
|
+
* }
|
|
58
|
+
* },
|
|
59
|
+
* };
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @param {Error} error - The error to check.
|
|
63
|
+
* @returns {boolean} `true` if the error is a Durable engine interruption signal.
|
|
64
|
+
*/
|
|
65
|
+
function didInterrupt(error) {
|
|
66
|
+
return (error instanceof errors_1.DurableChildError ||
|
|
67
|
+
error instanceof errors_1.DurableFatalError ||
|
|
68
|
+
error instanceof errors_1.DurableMaxedError ||
|
|
69
|
+
error instanceof errors_1.DurableProxyError ||
|
|
70
|
+
error instanceof errors_1.DurableRetryError ||
|
|
71
|
+
error instanceof errors_1.DurableSleepError ||
|
|
72
|
+
error instanceof errors_1.DurableTimeoutError ||
|
|
73
|
+
error instanceof errors_1.DurableWaitForError ||
|
|
74
|
+
error instanceof errors_1.DurableWaitForAllError);
|
|
75
|
+
}
|
|
76
|
+
exports.didInterrupt = didInterrupt;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { HotMesh } from './common';
|
|
2
|
+
/**
|
|
3
|
+
* Guards side-effectful operations (`emit`, `signal`, `hook`, `trace`,
|
|
4
|
+
* `interrupt`) against duplicate execution during replay. Unlike
|
|
5
|
+
* `didRun()` (which replays stored results), this guard is for
|
|
6
|
+
* operations that don't produce a return value to cache — they simply
|
|
7
|
+
* must not run twice.
|
|
8
|
+
*
|
|
9
|
+
* ## Mechanism
|
|
10
|
+
*
|
|
11
|
+
* 1. Increments the execution counter to produce a `sessionId`.
|
|
12
|
+
* 2. If that `sessionId` already exists in the `replay` hash, the
|
|
13
|
+
* operation already ran in a previous execution → return `false`.
|
|
14
|
+
* 3. Otherwise, atomically increments a field on the job's backend
|
|
15
|
+
* HASH via `incrementFieldByFloat`. If the result is exactly `1`,
|
|
16
|
+
* this is the first worker to reach this point → return `true`.
|
|
17
|
+
* If `> 1`, a concurrent worker already executed it → return `false`.
|
|
18
|
+
*
|
|
19
|
+
* This provides **distributed idempotency** for side effects across
|
|
20
|
+
* replays and concurrent worker instances.
|
|
21
|
+
*
|
|
22
|
+
* @private
|
|
23
|
+
* @param {HotMesh} hotMeshClient - The HotMesh client.
|
|
24
|
+
* @param {string} prefix - Operation type (`'trace'`, `'emit'`, `'signal'`, `'hook'`, `'interrupt'`).
|
|
25
|
+
* @returns {Promise<boolean>} `true` if the side effect should execute, `false` if it already ran.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isSideEffectAllowed(hotMeshClient: HotMesh, prefix: string): Promise<boolean>;
|
|
@@ -3,12 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.isSideEffectAllowed = void 0;
|
|
4
4
|
const common_1 = require("./common");
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Guards side-effectful operations (`emit`, `signal`, `hook`, `trace`,
|
|
7
|
+
* `interrupt`) against duplicate execution during replay. Unlike
|
|
8
|
+
* `didRun()` (which replays stored results), this guard is for
|
|
9
|
+
* operations that don't produce a return value to cache — they simply
|
|
10
|
+
* must not run twice.
|
|
11
|
+
*
|
|
12
|
+
* ## Mechanism
|
|
13
|
+
*
|
|
14
|
+
* 1. Increments the execution counter to produce a `sessionId`.
|
|
15
|
+
* 2. If that `sessionId` already exists in the `replay` hash, the
|
|
16
|
+
* operation already ran in a previous execution → return `false`.
|
|
17
|
+
* 3. Otherwise, atomically increments a field on the job's backend
|
|
18
|
+
* HASH via `incrementFieldByFloat`. If the result is exactly `1`,
|
|
19
|
+
* this is the first worker to reach this point → return `true`.
|
|
20
|
+
* If `> 1`, a concurrent worker already executed it → return `false`.
|
|
21
|
+
*
|
|
22
|
+
* This provides **distributed idempotency** for side effects across
|
|
23
|
+
* replays and concurrent worker instances.
|
|
24
|
+
*
|
|
8
25
|
* @private
|
|
9
26
|
* @param {HotMesh} hotMeshClient - The HotMesh client.
|
|
10
|
-
* @param {string} prefix -
|
|
11
|
-
* @returns {Promise<boolean>}
|
|
27
|
+
* @param {string} prefix - Operation type (`'trace'`, `'emit'`, `'signal'`, `'hook'`, `'interrupt'`).
|
|
28
|
+
* @returns {Promise<boolean>} `true` if the side effect should execute, `false` if it already ran.
|
|
12
29
|
*/
|
|
13
30
|
async function isSideEffectAllowed(hotMeshClient, prefix) {
|
|
14
31
|
const store = common_1.asyncLocalStorage.getStore();
|