@hotmeshio/hotmesh 0.14.3 → 0.14.5
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/build/modules/enums.d.ts +6 -0
- package/build/modules/enums.js +8 -2
- package/build/package.json +4 -3
- package/build/services/activities/hook.d.ts +10 -1
- package/build/services/activities/hook.js +45 -6
- package/build/services/dba/index.d.ts +1 -0
- package/build/services/dba/index.js +20 -3
- package/build/services/durable/client.js +13 -3
- package/build/services/durable/handle.d.ts +8 -1
- package/build/services/durable/handle.js +9 -1
- package/build/services/durable/worker.js +4 -0
- package/build/services/durable/workflow/signal.d.ts +1 -1
- package/build/services/durable/workflow/signal.js +2 -1
- package/build/services/mapper/index.d.ts +57 -2
- package/build/services/mapper/index.js +57 -2
- package/build/services/pipe/index.d.ts +444 -10
- package/build/services/pipe/index.js +444 -10
- package/build/services/store/index.d.ts +15 -2
- package/build/services/store/providers/postgres/kvtables.d.ts +1 -0
- package/build/services/store/providers/postgres/kvtables.js +46 -1
- package/build/services/store/providers/postgres/postgres.d.ts +25 -2
- package/build/services/store/providers/postgres/postgres.js +121 -4
- package/build/services/stream/registry.d.ts +1 -0
- package/build/services/stream/registry.js +12 -8
- package/build/services/task/index.d.ts +4 -1
- package/build/services/task/index.js +34 -6
- package/build/services/worker/index.js +2 -0
- package/build/types/dba.d.ts +11 -0
- package/build/types/hotmesh.d.ts +8 -0
- package/package.json +4 -3
- package/vitest.config.mts +1 -1
package/build/modules/enums.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export declare const HMSH_TELEMETRY: "debug" | "info";
|
|
|
55
55
|
* Default cleanup time for signal in the db when its associated job is completed.
|
|
56
56
|
*/
|
|
57
57
|
export declare const HMSH_SIGNAL_EXPIRE = 3600;
|
|
58
|
+
/**
|
|
59
|
+
* Default TTL for pending signals (signals that arrived before the hook registered).
|
|
60
|
+
* The signaler can override this via the `$expire` field in the signal data
|
|
61
|
+
* using a natural-language duration (e.g., '1h', '24h').
|
|
62
|
+
*/
|
|
63
|
+
export declare const HMSH_PENDING_SIGNAL_EXPIRE = 600;
|
|
58
64
|
export declare const HMSH_CODE_SUCCESS = 200;
|
|
59
65
|
export declare const HMSH_CODE_PENDING = 202;
|
|
60
66
|
export declare const HMSH_CODE_NOTFOUND = 404;
|
package/build/modules/enums.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = void 0;
|
|
3
|
+
exports.HMSH_ROUTER_SCOUT_INTERVAL_MS = exports.HMSH_ROUTER_SCOUT_INTERVAL_SECONDS = exports.HMSH_SCOUT_INTERVAL_SECONDS = exports.HMSH_FIDELITY_SECONDS = exports.HMSH_EXPIRE_DURATION = exports.HMSH_XPENDING_COUNT = exports.HMSH_XCLAIM_COUNT = exports.HMSH_XCLAIM_DELAY_MS = exports.HMSH_BLOCK_TIME_MS = exports.HMSH_DURABLE_INITIAL_INTERVAL = exports.HMSH_DURABLE_EXP_BACKOFF = exports.HMSH_DURABLE_MAX_INTERVAL = exports.HMSH_DURABLE_MAX_ATTEMPTS = exports.HMSH_GRADUATED_INTERVAL_MS = exports.HMSH_MAX_TIMEOUT_MS = exports.HMSH_POISON_MESSAGE_THRESHOLD = exports.HMSH_MAX_RETRIES = exports.MAX_DELAY = exports.MAX_STREAM_RETRIES = exports.INITIAL_STREAM_BACKOFF = exports.MAX_STREAM_BACKOFF = exports.HMSH_EXPIRE_JOB_SECONDS = exports.HMSH_OTT_WAIT_TIME = exports.HMSH_DEPLOYMENT_PAUSE = exports.HMSH_DEPLOYMENT_DELAY = exports.HMSH_ACTIVATION_MAX_RETRY = exports.HMSH_QUORUM_DELAY_MS = exports.HMSH_QUORUM_ROLLCALL_CYCLES = exports.HMSH_STATUS_UNKNOWN = exports.HMSH_CODE_DURABLE_RETRYABLE = exports.HMSH_CODE_DURABLE_FATAL = exports.HMSH_CODE_DURABLE_MAXED = exports.HMSH_CODE_DURABLE_TIMEOUT = exports.HMSH_CODE_DURABLE_WAIT = exports.HMSH_CODE_DURABLE_CONTINUE = exports.HMSH_CODE_DURABLE_PROXY = exports.HMSH_CODE_DURABLE_CHILD = exports.HMSH_CODE_DURABLE_ALL = exports.HMSH_CODE_DURABLE_SLEEP = exports.HMSH_CODE_UNACKED = exports.HMSH_CODE_TIMEOUT = exports.HMSH_CODE_UNKNOWN = exports.HMSH_CODE_INTERRUPT = exports.HMSH_CODE_NOTFOUND = exports.HMSH_CODE_PENDING = exports.HMSH_CODE_SUCCESS = exports.HMSH_PENDING_SIGNAL_EXPIRE = exports.HMSH_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
|
|
4
|
+
exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = void 0;
|
|
5
5
|
/**
|
|
6
6
|
* Determines the log level for the application. The default is 'info'.
|
|
7
7
|
*/
|
|
@@ -58,6 +58,12 @@ exports.HMSH_TELEMETRY = process.env.HMSH_TELEMETRY || 'info';
|
|
|
58
58
|
* Default cleanup time for signal in the db when its associated job is completed.
|
|
59
59
|
*/
|
|
60
60
|
exports.HMSH_SIGNAL_EXPIRE = 3600; //seconds
|
|
61
|
+
/**
|
|
62
|
+
* Default TTL for pending signals (signals that arrived before the hook registered).
|
|
63
|
+
* The signaler can override this via the `$expire` field in the signal data
|
|
64
|
+
* using a natural-language duration (e.g., '1h', '24h').
|
|
65
|
+
*/
|
|
66
|
+
exports.HMSH_PENDING_SIGNAL_EXPIRE = 600; //seconds (10 minutes)
|
|
61
67
|
// HOTMESH STATUS CODES
|
|
62
68
|
exports.HMSH_CODE_SUCCESS = 200;
|
|
63
69
|
exports.HMSH_CODE_PENDING = 202;
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
4
4
|
"description": "Durable Workflow",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"obfuscate": "ts-node scripts/obfuscate.ts",
|
|
15
15
|
"clean-build": "npm run clean && npm run build",
|
|
16
16
|
"clean-build-obfuscate": "npm run clean-build && npm run obfuscate",
|
|
17
|
-
"docs": "typedoc",
|
|
18
|
-
"docs:clean": "rimraf ./docs/hotmesh && typedoc",
|
|
17
|
+
"docs": "typedoc && cp -R docs/hotmesh/* docs/ && rm -rf docs/hotmesh",
|
|
18
|
+
"docs:clean": "rimraf ./docs/hotmesh && typedoc && cp -R docs/hotmesh/* docs/ && rm -rf docs/hotmesh",
|
|
19
19
|
"lint": "eslint . --ext .ts",
|
|
20
20
|
"lint:fix": "eslint . --fix --ext .ts",
|
|
21
21
|
"start": "ts-node src/index.ts",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"test:durable:retrypolicy": "vitest run tests/durable/retry-policy",
|
|
48
48
|
"test:durable:sleep": "vitest run tests/durable/sleep/postgres.test.ts",
|
|
49
49
|
"test:durable:signal": "vitest run tests/durable/signal/postgres.test.ts",
|
|
50
|
+
"test:durable:readonly": "docker compose --profile readonly up -d --build && docker compose exec hotmesh-readonly npx vitest run --config tests/durable/readonly/vitest.config.mts",
|
|
50
51
|
"test:durable:unknown": "vitest run tests/durable/unknown/postgres.test.ts",
|
|
51
52
|
"test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
|
|
52
53
|
"test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
|
|
@@ -152,9 +152,18 @@ declare class Hook extends Activity {
|
|
|
152
152
|
isConfiguredAsHook(): boolean;
|
|
153
153
|
doesHook(): boolean;
|
|
154
154
|
doHook(telemetry: TelemetryService): Promise<void>;
|
|
155
|
+
/**
|
|
156
|
+
* Re-publishes a pending signal as a WEBHOOK stream message so the
|
|
157
|
+
* normal leg2 dispatch path processes it. Called when leg1's
|
|
158
|
+
* setHookSignal atomically detected and consumed a pending signal.
|
|
159
|
+
*/
|
|
160
|
+
private redeliverPendingSignal;
|
|
155
161
|
doPassThrough(telemetry: TelemetryService): Promise<void>;
|
|
156
162
|
getHookRule(topic: string): Promise<HookRule | undefined>;
|
|
157
|
-
registerHook(transaction?: ProviderTransaction): Promise<
|
|
163
|
+
registerHook(transaction?: ProviderTransaction): Promise<{
|
|
164
|
+
jobId?: string;
|
|
165
|
+
pending?: string;
|
|
166
|
+
} | void>;
|
|
158
167
|
processWebHookEvent(status?: StreamStatus, code?: StreamCode): Promise<JobStatus | void>;
|
|
159
168
|
processTimeHookEvent(jobId: string): Promise<JobStatus | void>;
|
|
160
169
|
}
|
|
@@ -6,6 +6,7 @@ const pipe_1 = require("../pipe");
|
|
|
6
6
|
const task_1 = require("../task");
|
|
7
7
|
const telemetry_1 = require("../telemetry");
|
|
8
8
|
const stream_1 = require("../../types/stream");
|
|
9
|
+
const utils_1 = require("../../modules/utils");
|
|
9
10
|
const activity_1 = require("./activity");
|
|
10
11
|
/**
|
|
11
12
|
* A versatile pause/resume activity that supports three distinct patterns:
|
|
@@ -203,7 +204,7 @@ class Hook extends activity_1.Activity {
|
|
|
203
204
|
}
|
|
204
205
|
async doHook(telemetry) {
|
|
205
206
|
const transaction = this.store.transact();
|
|
206
|
-
await this.registerHook(transaction);
|
|
207
|
+
const hookResult = await this.registerHook(transaction);
|
|
207
208
|
this.mapOutputData();
|
|
208
209
|
this.mapJobData();
|
|
209
210
|
await this.setState(transaction);
|
|
@@ -211,6 +212,38 @@ class Hook extends activity_1.Activity {
|
|
|
211
212
|
await this.setStatus(0, transaction);
|
|
212
213
|
await transaction.exec();
|
|
213
214
|
telemetry.mapActivityAttributes();
|
|
215
|
+
//if a pending signal was detected (signal arrived before hook
|
|
216
|
+
//registered), re-publish the WEBHOOK so leg2 processes it
|
|
217
|
+
//now that the hook signal is committed and state is saved
|
|
218
|
+
if (hookResult && hookResult.pending) {
|
|
219
|
+
await this.redeliverPendingSignal(hookResult.pending);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Re-publishes a pending signal as a WEBHOOK stream message so the
|
|
224
|
+
* normal leg2 dispatch path processes it. Called when leg1's
|
|
225
|
+
* setHookSignal atomically detected and consumed a pending signal.
|
|
226
|
+
*/
|
|
227
|
+
async redeliverPendingSignal(pendingJson) {
|
|
228
|
+
const data = JSON.parse(pendingJson);
|
|
229
|
+
const hookRule = await this.getHookRule(this.config.hook.topic);
|
|
230
|
+
this.logger.warn('hook-pending-signal-redelivery', {
|
|
231
|
+
topic: this.config.hook.topic,
|
|
232
|
+
aid: hookRule?.to || this.metadata.aid,
|
|
233
|
+
jid: this.context.metadata.jid,
|
|
234
|
+
});
|
|
235
|
+
const streamData = {
|
|
236
|
+
type: stream_1.StreamDataType.WEBHOOK,
|
|
237
|
+
status: stream_1.StreamStatus.SUCCESS,
|
|
238
|
+
code: 200,
|
|
239
|
+
metadata: {
|
|
240
|
+
guid: (0, utils_1.guid)(),
|
|
241
|
+
aid: hookRule?.to || this.metadata.aid,
|
|
242
|
+
topic: this.config.hook.topic,
|
|
243
|
+
},
|
|
244
|
+
data,
|
|
245
|
+
};
|
|
246
|
+
await this.engine.router?.publishMessage(null, streamData);
|
|
214
247
|
}
|
|
215
248
|
async doPassThrough(telemetry) {
|
|
216
249
|
this.adjacencyList = await this.filterAdjacent();
|
|
@@ -225,19 +258,25 @@ class Hook extends activity_1.Activity {
|
|
|
225
258
|
return rules?.[topic]?.[0];
|
|
226
259
|
}
|
|
227
260
|
async registerHook(transaction) {
|
|
228
|
-
let
|
|
261
|
+
let jobId;
|
|
262
|
+
let pending;
|
|
229
263
|
if (this.config.hook?.topic) {
|
|
230
|
-
|
|
264
|
+
//hook signal is set standalone (not in the transaction) so the
|
|
265
|
+
//single CTE query can atomically detect a pending signal collision
|
|
266
|
+
const hookResult = await this.engine.taskService.registerWebHook(this.config.hook.topic, this.context, this.resolveDad(), this.context.metadata.expire);
|
|
267
|
+
jobId = hookResult.jobId;
|
|
268
|
+
pending = hookResult.pending;
|
|
231
269
|
}
|
|
232
270
|
if (this.config.sleep) {
|
|
233
271
|
const duration = pipe_1.Pipe.resolve(this.config.sleep, this.context);
|
|
234
272
|
if (!isNaN(duration) && Number(duration) > 0) {
|
|
235
273
|
await this.engine.taskService.registerTimeHook(this.context.metadata.jid, this.context.metadata.gid, `${this.metadata.aid}${this.metadata.dad || ''}`, 'sleep', duration, this.metadata.dad || '', transaction);
|
|
236
|
-
if (!
|
|
237
|
-
|
|
274
|
+
if (!jobId)
|
|
275
|
+
jobId = this.context.metadata.jid;
|
|
238
276
|
}
|
|
239
277
|
}
|
|
240
|
-
|
|
278
|
+
if (jobId)
|
|
279
|
+
return { jobId, pending };
|
|
241
280
|
}
|
|
242
281
|
async processWebHookEvent(status = stream_1.StreamStatus.SUCCESS, code = 200) {
|
|
243
282
|
this.logger.debug('hook-process-web-hook-event', {
|
|
@@ -14,6 +14,7 @@ import { PostgresClientType } from '../../types/postgres';
|
|
|
14
14
|
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
|
|
15
15
|
* | `{appId}.engine_streams` | Processed engine stream messages with `expired_at` set |
|
|
16
16
|
* | `{appId}.worker_streams` | Processed worker stream messages with `expired_at` set |
|
|
17
|
+
* | `{appId}.signal_registry` | Consumed hook signals and stale pending signals with `expiry` set |
|
|
17
18
|
*
|
|
18
19
|
* The `DBA` service addresses this with two methods:
|
|
19
20
|
*
|
|
@@ -17,6 +17,7 @@ const postgres_1 = require("../connector/providers/postgres");
|
|
|
17
17
|
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
|
|
18
18
|
* | `{appId}.engine_streams` | Processed engine stream messages with `expired_at` set |
|
|
19
19
|
* | `{appId}.worker_streams` | Processed worker stream messages with `expired_at` set |
|
|
20
|
+
* | `{appId}.signal_registry` | Consumed hook signals and stale pending signals with `expiry` set |
|
|
20
21
|
*
|
|
21
22
|
* The `DBA` service addresses this with two methods:
|
|
22
23
|
*
|
|
@@ -186,7 +187,8 @@ class DBA {
|
|
|
186
187
|
prune_engine_streams BOOLEAN DEFAULT NULL,
|
|
187
188
|
prune_worker_streams BOOLEAN DEFAULT NULL,
|
|
188
189
|
engine_streams_retention INTERVAL DEFAULT NULL,
|
|
189
|
-
worker_streams_retention INTERVAL DEFAULT NULL
|
|
190
|
+
worker_streams_retention INTERVAL DEFAULT NULL,
|
|
191
|
+
prune_signals BOOLEAN DEFAULT TRUE
|
|
190
192
|
)
|
|
191
193
|
RETURNS TABLE(
|
|
192
194
|
deleted_jobs BIGINT,
|
|
@@ -195,7 +197,8 @@ class DBA {
|
|
|
195
197
|
deleted_worker_streams BIGINT,
|
|
196
198
|
stripped_attributes BIGINT,
|
|
197
199
|
deleted_transient BIGINT,
|
|
198
|
-
marked_pruned BIGINT
|
|
200
|
+
marked_pruned BIGINT,
|
|
201
|
+
deleted_signals BIGINT
|
|
199
202
|
)
|
|
200
203
|
LANGUAGE plpgsql
|
|
201
204
|
AS $$
|
|
@@ -206,6 +209,7 @@ class DBA {
|
|
|
206
209
|
v_stripped_attributes BIGINT := 0;
|
|
207
210
|
v_deleted_transient BIGINT := 0;
|
|
208
211
|
v_marked_pruned BIGINT := 0;
|
|
212
|
+
v_deleted_signals BIGINT := 0;
|
|
209
213
|
v_do_engine BOOLEAN;
|
|
210
214
|
v_do_worker BOOLEAN;
|
|
211
215
|
v_engine_retention INTERVAL;
|
|
@@ -287,6 +291,15 @@ class DBA {
|
|
|
287
291
|
GET DIAGNOSTICS v_marked_pruned = ROW_COUNT;
|
|
288
292
|
END IF;
|
|
289
293
|
|
|
294
|
+
-- 6. Hard-delete expired signal_registry rows.
|
|
295
|
+
-- Includes consumed hook signals and stale pending signals.
|
|
296
|
+
IF prune_signals THEN
|
|
297
|
+
DELETE FROM ${schema}.signal_registry
|
|
298
|
+
WHERE expiry IS NOT NULL
|
|
299
|
+
AND expiry <= NOW();
|
|
300
|
+
GET DIAGNOSTICS v_deleted_signals = ROW_COUNT;
|
|
301
|
+
END IF;
|
|
302
|
+
|
|
290
303
|
deleted_jobs := v_deleted_jobs;
|
|
291
304
|
deleted_streams := v_deleted_engine_streams + v_deleted_worker_streams;
|
|
292
305
|
deleted_engine_streams := v_deleted_engine_streams;
|
|
@@ -294,6 +307,7 @@ class DBA {
|
|
|
294
307
|
stripped_attributes := v_stripped_attributes;
|
|
295
308
|
deleted_transient := v_deleted_transient;
|
|
296
309
|
marked_pruned := v_marked_pruned;
|
|
310
|
+
deleted_signals := v_deleted_signals;
|
|
297
311
|
RETURN NEXT;
|
|
298
312
|
END;
|
|
299
313
|
$$;
|
|
@@ -391,12 +405,14 @@ class DBA {
|
|
|
391
405
|
const workerStreams = options.workerStreams ?? null;
|
|
392
406
|
const engineStreamsExpire = options.engineStreamsExpire ?? null;
|
|
393
407
|
const workerStreamsExpire = options.workerStreamsExpire ?? null;
|
|
408
|
+
const signals = options.signals ?? true;
|
|
394
409
|
await DBA.deploy(options.connection, options.appId);
|
|
395
410
|
const { client, release } = await DBA.getClient(options.connection);
|
|
396
411
|
try {
|
|
397
|
-
const result = await client.query(`SELECT * FROM ${schema}.prune($1::interval, $2::boolean, $3::boolean, $4::boolean, $5::text[], $6::boolean, $7::boolean, $8::boolean, $9::boolean, $10::interval, $11::interval)`, [
|
|
412
|
+
const result = await client.query(`SELECT * FROM ${schema}.prune($1::interval, $2::boolean, $3::boolean, $4::boolean, $5::text[], $6::boolean, $7::boolean, $8::boolean, $9::boolean, $10::interval, $11::interval, $12::boolean)`, [
|
|
398
413
|
expire, jobs, streams, attributes, entities, pruneTransient, keepHmark,
|
|
399
414
|
engineStreams, workerStreams, engineStreamsExpire, workerStreamsExpire,
|
|
415
|
+
signals,
|
|
400
416
|
]);
|
|
401
417
|
const row = result.rows[0];
|
|
402
418
|
return {
|
|
@@ -407,6 +423,7 @@ class DBA {
|
|
|
407
423
|
attributes: Number(row.stripped_attributes),
|
|
408
424
|
transient: Number(row.deleted_transient),
|
|
409
425
|
marked: Number(row.marked_pruned),
|
|
426
|
+
signals: Number(row.deleted_signals),
|
|
410
427
|
};
|
|
411
428
|
}
|
|
412
429
|
finally {
|
|
@@ -156,11 +156,21 @@ class ClientService {
|
|
|
156
156
|
return new handle_1.WorkflowHandleService(hotMeshClient, workflowTopic, jobId);
|
|
157
157
|
},
|
|
158
158
|
/**
|
|
159
|
-
* Sends a message payload to a running workflow that is paused and awaiting the signal
|
|
159
|
+
* Sends a message payload to a running workflow that is paused and awaiting the signal.
|
|
160
|
+
*
|
|
161
|
+
* If the signal arrives before the workflow has registered its hook
|
|
162
|
+
* (race condition under load), it is buffered as a pending signal
|
|
163
|
+
* for up to `expire` (default 10 minutes). Use a longer duration
|
|
164
|
+
* when signaling "early on purpose" (e.g., depositing a payload
|
|
165
|
+
* hours before the workflow starts).
|
|
160
166
|
*/
|
|
161
|
-
signal: async (signalId, data, namespace) => {
|
|
167
|
+
signal: async (signalId, data, namespace, expire) => {
|
|
162
168
|
const topic = `${namespace ?? factory_1.APP_ID}.wfs.signal`;
|
|
163
|
-
return await (await this.getHotMeshClient(topic, namespace)).signal(topic, {
|
|
169
|
+
return await (await this.getHotMeshClient(topic, namespace)).signal(topic, {
|
|
170
|
+
id: signalId,
|
|
171
|
+
data,
|
|
172
|
+
...(expire ? { $expire: expire } : {}),
|
|
173
|
+
});
|
|
164
174
|
},
|
|
165
175
|
/**
|
|
166
176
|
* Spawns an a new, isolated execution cycle within the same job.
|
|
@@ -57,10 +57,17 @@ export declare class WorkflowHandleService {
|
|
|
57
57
|
* on `Durable.workflow.condition(signalId)`, it resumes with the
|
|
58
58
|
* provided data.
|
|
59
59
|
*
|
|
60
|
+
* If the signal arrives before the workflow has registered its hook
|
|
61
|
+
* (race condition under load), it is buffered as a pending signal
|
|
62
|
+
* for up to `expire` (default 10 minutes). Use a longer duration
|
|
63
|
+
* when signaling "early on purpose" (e.g., depositing a payload
|
|
64
|
+
* hours before the workflow starts).
|
|
65
|
+
*
|
|
60
66
|
* @param signalId - Matches the `signalId` passed to `condition()`.
|
|
61
67
|
* @param data - Payload delivered to the waiting workflow.
|
|
68
|
+
* @param expire - Optional pending signal TTL (e.g., '1h', '30d'). Default '10m'.
|
|
62
69
|
*/
|
|
63
|
-
signal(signalId: string, data: Record<any, any
|
|
70
|
+
signal(signalId: string, data: Record<any, any>, expire?: string): Promise<void>;
|
|
64
71
|
/**
|
|
65
72
|
* Returns the current workflow state. For a completed workflow this
|
|
66
73
|
* is the final output; for a running workflow it reflects the latest
|
|
@@ -58,13 +58,21 @@ class WorkflowHandleService {
|
|
|
58
58
|
* on `Durable.workflow.condition(signalId)`, it resumes with the
|
|
59
59
|
* provided data.
|
|
60
60
|
*
|
|
61
|
+
* If the signal arrives before the workflow has registered its hook
|
|
62
|
+
* (race condition under load), it is buffered as a pending signal
|
|
63
|
+
* for up to `expire` (default 10 minutes). Use a longer duration
|
|
64
|
+
* when signaling "early on purpose" (e.g., depositing a payload
|
|
65
|
+
* hours before the workflow starts).
|
|
66
|
+
*
|
|
61
67
|
* @param signalId - Matches the `signalId` passed to `condition()`.
|
|
62
68
|
* @param data - Payload delivered to the waiting workflow.
|
|
69
|
+
* @param expire - Optional pending signal TTL (e.g., '1h', '30d'). Default '10m'.
|
|
63
70
|
*/
|
|
64
|
-
async signal(signalId, data) {
|
|
71
|
+
async signal(signalId, data, expire) {
|
|
65
72
|
await this.hotMesh.signal(`${this.hotMesh.appId}.wfs.signal`, {
|
|
66
73
|
id: signalId,
|
|
67
74
|
data,
|
|
75
|
+
...(expire ? { $expire: expire } : {}),
|
|
68
76
|
});
|
|
69
77
|
}
|
|
70
78
|
/**
|
|
@@ -516,10 +516,12 @@ class WorkerService {
|
|
|
516
516
|
if (WorkerService.instances.has(targetTopic)) {
|
|
517
517
|
return await WorkerService.instances.get(targetTopic);
|
|
518
518
|
}
|
|
519
|
+
const readonly = providerConfig.readonly ?? false;
|
|
519
520
|
const workerEntry = {
|
|
520
521
|
topic: activityTopic,
|
|
521
522
|
connection: providerConfig,
|
|
522
523
|
callback: this.wrapActivityFunctions().bind(this),
|
|
524
|
+
readonly,
|
|
523
525
|
};
|
|
524
526
|
if (config.workerCredentials) {
|
|
525
527
|
workerEntry.workerCredentials = config.workerCredentials;
|
|
@@ -644,10 +646,12 @@ class WorkerService {
|
|
|
644
646
|
const targetNamespace = config?.namespace ?? factory_1.APP_ID;
|
|
645
647
|
const optionsHash = WorkerService.hashOptions(config?.connection);
|
|
646
648
|
const targetTopic = `${optionsHash}.${targetNamespace}.${workflowTopic}`;
|
|
649
|
+
const readonly = providerConfig.readonly ?? false;
|
|
647
650
|
const workerEntry = {
|
|
648
651
|
topic: taskQueue,
|
|
649
652
|
workflowName: workflowFunctionName,
|
|
650
653
|
connection: providerConfig,
|
|
654
|
+
readonly,
|
|
651
655
|
callback: this.wrapWorkflowFunction(workflowFunction, workflowTopic, workflowFunctionName, config).bind(this),
|
|
652
656
|
};
|
|
653
657
|
if (config.workerCredentials) {
|
|
@@ -55,4 +55,4 @@
|
|
|
55
55
|
* @param {Record<any, any>} data - The payload to deliver to the waiting workflow.
|
|
56
56
|
* @returns {Promise<string>} The resulting hook/stream ID.
|
|
57
57
|
*/
|
|
58
|
-
export declare function signal(signalId: string, data: Record<any, any
|
|
58
|
+
export declare function signal(signalId: string, data: Record<any, any>, expire?: string): Promise<string>;
|
|
@@ -60,7 +60,7 @@ const isSideEffectAllowed_1 = require("./isSideEffectAllowed");
|
|
|
60
60
|
* @param {Record<any, any>} data - The payload to deliver to the waiting workflow.
|
|
61
61
|
* @returns {Promise<string>} The resulting hook/stream ID.
|
|
62
62
|
*/
|
|
63
|
-
async function signal(signalId, data) {
|
|
63
|
+
async function signal(signalId, data, expire) {
|
|
64
64
|
const store = common_1.asyncLocalStorage.getStore();
|
|
65
65
|
const workflowTopic = store.get('workflowTopic');
|
|
66
66
|
const connection = store.get('connection');
|
|
@@ -73,6 +73,7 @@ async function signal(signalId, data) {
|
|
|
73
73
|
return await hotMeshClient.signal(`${namespace}.wfs.signal`, {
|
|
74
74
|
id: signalId,
|
|
75
75
|
data,
|
|
76
|
+
...(expire ? { $expire: expire } : {}),
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
import { JobState } from '../../types/job';
|
|
2
2
|
import { TransitionRule } from '../../types/transition';
|
|
3
3
|
import { StreamCode } from '../../types';
|
|
4
|
+
/**
|
|
5
|
+
* Evaluates and transforms data-mapping rules against live job state.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* MapperService is the bridge between a workflow's declarative mapping
|
|
9
|
+
* rules (including `@pipe` expressions) and the runtime job data. It
|
|
10
|
+
* recursively walks a rule tree, delegating leaf-level resolution to
|
|
11
|
+
* the {@link Pipe} engine. Static helpers such as {@link evaluate}
|
|
12
|
+
* also power transition-condition checks during workflow execution.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* const mapper = new MapperService(
|
|
17
|
+
* { greeting: '{data.name}', score: { '@pipe': [['{data.x}', '{data.y}'], ['{@math.add}']] } },
|
|
18
|
+
* jobState,
|
|
19
|
+
* );
|
|
20
|
+
* const result = mapper.mapRules();
|
|
21
|
+
* // => { greeting: 'Alice', score: 7 }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
4
24
|
declare class MapperService {
|
|
5
25
|
private rules;
|
|
6
26
|
private data;
|
|
27
|
+
/**
|
|
28
|
+
* @param rules - The mapping rule tree to evaluate. May contain
|
|
29
|
+
* literal values, `{data.*}` references, or nested `@pipe` objects.
|
|
30
|
+
* @param data - The current {@link JobState} used to resolve references.
|
|
31
|
+
*/
|
|
7
32
|
constructor(rules: Record<string, unknown>, data: JobState);
|
|
33
|
+
/**
|
|
34
|
+
* Recursively resolves every rule in the tree against the current job
|
|
35
|
+
* state and returns a fully-evaluated copy.
|
|
36
|
+
*
|
|
37
|
+
* @returns A plain object mirroring the rule structure with all
|
|
38
|
+
* expressions replaced by their resolved values.
|
|
39
|
+
*/
|
|
8
40
|
mapRules(): Record<string, unknown>;
|
|
9
41
|
private traverseRules;
|
|
10
42
|
/**
|
|
@@ -20,8 +52,31 @@ declare class MapperService {
|
|
|
20
52
|
*/
|
|
21
53
|
private resolve;
|
|
22
54
|
/**
|
|
23
|
-
* Evaluates a transition rule against the current job state and
|
|
24
|
-
*
|
|
55
|
+
* Evaluates a transition rule against the current job state and an
|
|
56
|
+
* incoming Stream message code to decide whether the transition fires.
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* Supports both simple boolean rules (`true` / `false`) and compound
|
|
60
|
+
* match rules with optional AND / OR gating. When the rule includes
|
|
61
|
+
* a `match` array, each entry's `actual` expression is resolved via
|
|
62
|
+
* {@link Pipe.resolve} and compared to the `expected` value.
|
|
63
|
+
*
|
|
64
|
+
* @param transitionRule - A boolean shorthand or a {@link TransitionRule}
|
|
65
|
+
* containing `code`, optional `gate`, and optional `match` conditions.
|
|
66
|
+
* @param context - The current {@link JobState} used to resolve
|
|
67
|
+
* `actual` expressions inside match entries.
|
|
68
|
+
* @param code - The {@link StreamCode} returned by the preceding
|
|
69
|
+
* activity (e.g. `200`).
|
|
70
|
+
* @returns `true` if the transition should be taken, `false` otherwise.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```typescript
|
|
74
|
+
* const shouldTransition = MapperService.evaluate(
|
|
75
|
+
* { code: 200, match: [{ expected: true, actual: '{data.isReady}' }] },
|
|
76
|
+
* jobState,
|
|
77
|
+
* 200,
|
|
78
|
+
* );
|
|
79
|
+
* ```
|
|
25
80
|
*/
|
|
26
81
|
static evaluate(transitionRule: TransitionRule | boolean, context: JobState, code: StreamCode): boolean;
|
|
27
82
|
}
|
|
@@ -2,11 +2,43 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MapperService = void 0;
|
|
4
4
|
const pipe_1 = require("../pipe");
|
|
5
|
+
/**
|
|
6
|
+
* Evaluates and transforms data-mapping rules against live job state.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* MapperService is the bridge between a workflow's declarative mapping
|
|
10
|
+
* rules (including `@pipe` expressions) and the runtime job data. It
|
|
11
|
+
* recursively walks a rule tree, delegating leaf-level resolution to
|
|
12
|
+
* the {@link Pipe} engine. Static helpers such as {@link evaluate}
|
|
13
|
+
* also power transition-condition checks during workflow execution.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const mapper = new MapperService(
|
|
18
|
+
* { greeting: '{data.name}', score: { '@pipe': [['{data.x}', '{data.y}'], ['{@math.add}']] } },
|
|
19
|
+
* jobState,
|
|
20
|
+
* );
|
|
21
|
+
* const result = mapper.mapRules();
|
|
22
|
+
* // => { greeting: 'Alice', score: 7 }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
5
25
|
class MapperService {
|
|
26
|
+
/**
|
|
27
|
+
* @param rules - The mapping rule tree to evaluate. May contain
|
|
28
|
+
* literal values, `{data.*}` references, or nested `@pipe` objects.
|
|
29
|
+
* @param data - The current {@link JobState} used to resolve references.
|
|
30
|
+
*/
|
|
6
31
|
constructor(rules, data) {
|
|
7
32
|
this.rules = rules;
|
|
8
33
|
this.data = data;
|
|
9
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Recursively resolves every rule in the tree against the current job
|
|
37
|
+
* state and returns a fully-evaluated copy.
|
|
38
|
+
*
|
|
39
|
+
* @returns A plain object mirroring the rule structure with all
|
|
40
|
+
* expressions replaced by their resolved values.
|
|
41
|
+
*/
|
|
10
42
|
mapRules() {
|
|
11
43
|
return this.traverseRules(this.rules);
|
|
12
44
|
}
|
|
@@ -46,8 +78,31 @@ class MapperService {
|
|
|
46
78
|
return pipe.process();
|
|
47
79
|
}
|
|
48
80
|
/**
|
|
49
|
-
* Evaluates a transition rule against the current job state and
|
|
50
|
-
*
|
|
81
|
+
* Evaluates a transition rule against the current job state and an
|
|
82
|
+
* incoming Stream message code to decide whether the transition fires.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* Supports both simple boolean rules (`true` / `false`) and compound
|
|
86
|
+
* match rules with optional AND / OR gating. When the rule includes
|
|
87
|
+
* a `match` array, each entry's `actual` expression is resolved via
|
|
88
|
+
* {@link Pipe.resolve} and compared to the `expected` value.
|
|
89
|
+
*
|
|
90
|
+
* @param transitionRule - A boolean shorthand or a {@link TransitionRule}
|
|
91
|
+
* containing `code`, optional `gate`, and optional `match` conditions.
|
|
92
|
+
* @param context - The current {@link JobState} used to resolve
|
|
93
|
+
* `actual` expressions inside match entries.
|
|
94
|
+
* @param code - The {@link StreamCode} returned by the preceding
|
|
95
|
+
* activity (e.g. `200`).
|
|
96
|
+
* @returns `true` if the transition should be taken, `false` otherwise.
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const shouldTransition = MapperService.evaluate(
|
|
101
|
+
* { code: 200, match: [{ expected: true, actual: '{data.isReady}' }] },
|
|
102
|
+
* jobState,
|
|
103
|
+
* 200,
|
|
104
|
+
* );
|
|
105
|
+
* ```
|
|
51
106
|
*/
|
|
52
107
|
static evaluate(transitionRule, context, code) {
|
|
53
108
|
if (typeof transitionRule === 'boolean') {
|