@hotmeshio/hotmesh 0.10.2 → 0.12.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 +1 -1
- package/build/modules/enums.d.ts +1 -0
- package/build/modules/enums.js +3 -1
- package/build/modules/errors.d.ts +2 -0
- package/build/modules/errors.js +2 -0
- package/build/modules/key.js +3 -2
- package/build/package.json +2 -2
- package/build/services/activities/worker.js +10 -0
- package/build/services/dba/index.d.ts +2 -1
- package/build/services/dba/index.js +11 -2
- package/build/services/durable/client.js +6 -1
- package/build/services/durable/exporter.d.ts +15 -0
- package/build/services/durable/exporter.js +384 -5
- package/build/services/durable/schemas/factory.d.ts +1 -1
- package/build/services/durable/schemas/factory.js +27 -4
- package/build/services/durable/worker.d.ts +2 -2
- package/build/services/durable/worker.js +15 -9
- package/build/services/durable/workflow/context.js +2 -0
- package/build/services/durable/workflow/execChild.js +5 -2
- package/build/services/durable/workflow/hook.js +6 -0
- package/build/services/durable/workflow/proxyActivities.js +3 -4
- package/build/services/engine/index.d.ts +2 -2
- package/build/services/engine/index.js +10 -5
- package/build/services/exporter/index.d.ts +16 -2
- package/build/services/exporter/index.js +76 -0
- package/build/services/hotmesh/index.d.ts +2 -2
- package/build/services/hotmesh/index.js +2 -2
- package/build/services/router/config/index.d.ts +2 -2
- package/build/services/router/config/index.js +2 -1
- package/build/services/router/consumption/index.js +80 -5
- package/build/services/store/index.d.ts +52 -0
- package/build/services/store/providers/postgres/exporter-sql.d.ts +40 -0
- package/build/services/store/providers/postgres/exporter-sql.js +92 -0
- package/build/services/store/providers/postgres/kvtables.js +6 -0
- package/build/services/store/providers/postgres/postgres.d.ts +42 -0
- package/build/services/store/providers/postgres/postgres.js +151 -0
- package/build/services/stream/index.d.ts +1 -0
- package/build/services/stream/providers/postgres/kvtables.d.ts +1 -1
- package/build/services/stream/providers/postgres/kvtables.js +235 -82
- package/build/services/stream/providers/postgres/lifecycle.d.ts +4 -3
- package/build/services/stream/providers/postgres/lifecycle.js +6 -5
- package/build/services/stream/providers/postgres/messages.d.ts +14 -6
- package/build/services/stream/providers/postgres/messages.js +153 -76
- package/build/services/stream/providers/postgres/notifications.d.ts +5 -2
- package/build/services/stream/providers/postgres/notifications.js +39 -35
- package/build/services/stream/providers/postgres/postgres.d.ts +21 -118
- package/build/services/stream/providers/postgres/postgres.js +87 -140
- package/build/services/stream/providers/postgres/scout.js +2 -2
- package/build/services/stream/providers/postgres/stats.js +3 -2
- package/build/services/stream/registry.d.ts +62 -0
- package/build/services/stream/registry.js +198 -0
- package/build/services/worker/index.js +20 -6
- package/build/types/durable.d.ts +6 -1
- package/build/types/error.d.ts +2 -0
- package/build/types/exporter.d.ts +84 -0
- package/build/types/hotmesh.d.ts +7 -1
- package/build/types/index.d.ts +1 -1
- package/build/types/stream.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install @hotmeshio/hotmesh
|
|
|
11
11
|
## Use HotMesh for
|
|
12
12
|
|
|
13
13
|
- **Durable pipelines** — Orchestrate long-running, multi-step pipelines transactionally.
|
|
14
|
-
- **Temporal
|
|
14
|
+
- **Familiar Temporal syntax** — The `Durable` module provides a Temporal-compatible API (`Client`, `Worker`, `proxyActivities`, `sleepFor`, `startChild`, signals) that runs directly on Postgres. No app server required.
|
|
15
15
|
- **Distributed state machines** — Build stateful applications where every component can [fail and recover](https://github.com/hotmeshio/sdk-typescript/blob/main/services/collator/README.md).
|
|
16
16
|
- **AI and training pipelines** — Multi-step AI workloads where each stage is expensive and must not be repeated on failure. A crashed pipeline resumes from the last committed step, not from the beginning.
|
|
17
17
|
|
package/build/modules/enums.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export declare const INITIAL_STREAM_BACKOFF: number;
|
|
|
78
78
|
export declare const MAX_STREAM_RETRIES: number;
|
|
79
79
|
export declare const MAX_DELAY = 2147483647;
|
|
80
80
|
export declare const HMSH_MAX_RETRIES: number;
|
|
81
|
+
export declare const HMSH_POISON_MESSAGE_THRESHOLD: number;
|
|
81
82
|
export declare const HMSH_MAX_TIMEOUT_MS: number;
|
|
82
83
|
export declare const HMSH_GRADUATED_INTERVAL_MS: number;
|
|
83
84
|
/**
|
package/build/modules/enums.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.HMSH_NOTIFY_PAYLOAD_LIMIT = exports.DEFAULT_TASK_QUEUE = exports.HMSH_GUID_SIZE = 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_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_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_SIGNAL_EXPIRE = exports.HMSH_TELEMETRY = exports.HMSH_LOGLEVEL = void 0;
|
|
4
|
+
exports.HMSH_ROUTER_POLL_FALLBACK_INTERVAL = void 0;
|
|
4
5
|
/**
|
|
5
6
|
* Determines the log level for the application. The default is 'info'.
|
|
6
7
|
*/
|
|
@@ -87,6 +88,7 @@ exports.INITIAL_STREAM_BACKOFF = parseInt(process.env.INITIAL_STREAM_BACKOFF, 10
|
|
|
87
88
|
exports.MAX_STREAM_RETRIES = parseInt(process.env.MAX_STREAM_RETRIES, 10) || 2;
|
|
88
89
|
exports.MAX_DELAY = 2147483647; // Maximum allowed delay in milliseconds for setTimeout
|
|
89
90
|
exports.HMSH_MAX_RETRIES = parseInt(process.env.HMSH_MAX_RETRIES, 10) || 3;
|
|
91
|
+
exports.HMSH_POISON_MESSAGE_THRESHOLD = parseInt(process.env.HMSH_POISON_MESSAGE_THRESHOLD, 10) || 5;
|
|
90
92
|
exports.HMSH_MAX_TIMEOUT_MS = parseInt(process.env.HMSH_MAX_TIMEOUT_MS, 10) || 60000;
|
|
91
93
|
exports.HMSH_GRADUATED_INTERVAL_MS = parseInt(process.env.HMSH_GRADUATED_INTERVAL_MS, 10) || 5000;
|
|
92
94
|
// DURABLE
|
package/build/modules/errors.js
CHANGED
|
@@ -54,6 +54,8 @@ class DurableChildError extends Error {
|
|
|
54
54
|
this.arguments = params.arguments;
|
|
55
55
|
this.workflowId = params.workflowId;
|
|
56
56
|
this.workflowTopic = params.workflowTopic;
|
|
57
|
+
this.taskQueue = params.taskQueue;
|
|
58
|
+
this.workflowName = params.workflowName;
|
|
57
59
|
this.parentWorkflowId = params.parentWorkflowId;
|
|
58
60
|
this.expire = params.expire;
|
|
59
61
|
this.persistent = params.persistent;
|
package/build/modules/key.js
CHANGED
|
@@ -143,7 +143,7 @@ class KeyService {
|
|
|
143
143
|
case 'v':
|
|
144
144
|
return 'versions';
|
|
145
145
|
case 'x':
|
|
146
|
-
return id === '' ? '
|
|
146
|
+
return id === '' ? 'engine_streams' : 'worker_streams';
|
|
147
147
|
case 'hooks':
|
|
148
148
|
return 'signal_patterns';
|
|
149
149
|
case 'signals':
|
|
@@ -174,7 +174,8 @@ class KeyService {
|
|
|
174
174
|
return 's';
|
|
175
175
|
case 'versions':
|
|
176
176
|
return 'v';
|
|
177
|
-
case '
|
|
177
|
+
case 'engine_streams':
|
|
178
|
+
case 'worker_streams':
|
|
178
179
|
return 'x';
|
|
179
180
|
case 'signal_patterns':
|
|
180
181
|
return 'hooks';
|
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Permanent-Memory Workflows & AI Agents",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"test:durable:sleep": "vitest run tests/durable/sleep/postgres.test.ts",
|
|
47
47
|
"test:durable:signal": "vitest run tests/durable/signal/postgres.test.ts",
|
|
48
48
|
"test:durable:unknown": "vitest run tests/durable/unknown/postgres.test.ts",
|
|
49
|
-
"test:durable:exporter": "vitest run tests/durable/exporter
|
|
49
|
+
"test:durable:exporter": "HMSH_LOGLEVEL=info vitest run tests/durable/exporter",
|
|
50
50
|
"test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
|
|
51
51
|
"test:dba": "vitest run tests/dba",
|
|
52
52
|
"test:cycle": "vitest run tests/functional/cycle",
|
|
@@ -189,6 +189,15 @@ class Worker extends activity_1.Activity {
|
|
|
189
189
|
}
|
|
190
190
|
async execActivity(transaction) {
|
|
191
191
|
const topic = pipe_1.Pipe.resolve(this.config.subtype, this.context);
|
|
192
|
+
// Extract workflow name from job data (set by durable client) or derive from subscribes
|
|
193
|
+
const jobData = this.context.data;
|
|
194
|
+
let wfn = jobData?.workflowName || '';
|
|
195
|
+
if (!wfn && this.config.subscribes) {
|
|
196
|
+
// Fallback: derive from subscribes by removing topic prefix
|
|
197
|
+
wfn = this.config.subscribes.startsWith(`${topic}-`)
|
|
198
|
+
? this.config.subscribes.substring(topic.length + 1)
|
|
199
|
+
: this.config.subscribes;
|
|
200
|
+
}
|
|
192
201
|
const streamData = {
|
|
193
202
|
metadata: {
|
|
194
203
|
guid: (0, utils_1.guid)(),
|
|
@@ -197,6 +206,7 @@ class Worker extends activity_1.Activity {
|
|
|
197
206
|
dad: this.metadata.dad,
|
|
198
207
|
aid: this.metadata.aid,
|
|
199
208
|
topic,
|
|
209
|
+
wfn,
|
|
200
210
|
spn: this.context['$self'].output.metadata.l1s,
|
|
201
211
|
trc: this.context.metadata.trc,
|
|
202
212
|
},
|
|
@@ -12,7 +12,8 @@ import { PostgresClientType } from '../../types/postgres';
|
|
|
12
12
|
* |---|---|
|
|
13
13
|
* | `{appId}.jobs` | Completed/expired jobs with `expired_at` set |
|
|
14
14
|
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
|
|
15
|
-
* | `{appId}.
|
|
15
|
+
* | `{appId}.engine_streams` | Processed engine stream messages with `expired_at` set |
|
|
16
|
+
* | `{appId}.worker_streams` | Processed worker stream messages with `expired_at` set |
|
|
16
17
|
*
|
|
17
18
|
* The `DBA` service addresses this with two methods:
|
|
18
19
|
*
|
|
@@ -15,7 +15,8 @@ const postgres_1 = require("../connector/providers/postgres");
|
|
|
15
15
|
* |---|---|
|
|
16
16
|
* | `{appId}.jobs` | Completed/expired jobs with `expired_at` set |
|
|
17
17
|
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
|
|
18
|
-
* | `{appId}.
|
|
18
|
+
* | `{appId}.engine_streams` | Processed engine stream messages with `expired_at` set |
|
|
19
|
+
* | `{appId}.worker_streams` | Processed worker stream messages with `expired_at` set |
|
|
19
20
|
*
|
|
20
21
|
* The `DBA` service addresses this with two methods:
|
|
21
22
|
*
|
|
@@ -188,6 +189,7 @@ class DBA {
|
|
|
188
189
|
v_stripped_attributes BIGINT := 0;
|
|
189
190
|
v_deleted_transient BIGINT := 0;
|
|
190
191
|
v_marked_pruned BIGINT := 0;
|
|
192
|
+
v_temp_count BIGINT := 0;
|
|
191
193
|
BEGIN
|
|
192
194
|
-- 1. Hard-delete expired jobs older than the retention window.
|
|
193
195
|
-- FK CASCADE on jobs_attributes handles attribute cleanup.
|
|
@@ -210,11 +212,18 @@ class DBA {
|
|
|
210
212
|
END IF;
|
|
211
213
|
|
|
212
214
|
-- 3. Hard-delete expired stream messages older than the retention window.
|
|
215
|
+
-- Deletes from both engine_streams and worker_streams tables.
|
|
213
216
|
IF prune_streams THEN
|
|
214
|
-
DELETE FROM ${schema}.
|
|
217
|
+
DELETE FROM ${schema}.engine_streams
|
|
215
218
|
WHERE expired_at IS NOT NULL
|
|
216
219
|
AND expired_at < NOW() - retention;
|
|
217
220
|
GET DIAGNOSTICS v_deleted_streams = ROW_COUNT;
|
|
221
|
+
|
|
222
|
+
DELETE FROM ${schema}.worker_streams
|
|
223
|
+
WHERE expired_at IS NOT NULL
|
|
224
|
+
AND expired_at < NOW() - retention;
|
|
225
|
+
GET DIAGNOSTICS v_temp_count = ROW_COUNT;
|
|
226
|
+
v_deleted_streams := v_deleted_streams + v_temp_count;
|
|
218
227
|
END IF;
|
|
219
228
|
|
|
220
229
|
-- 4. Strip execution artifacts from completed, live, un-pruned jobs.
|
|
@@ -145,6 +145,8 @@ class ClientService {
|
|
|
145
145
|
parentWorkflowId: options.parentWorkflowId,
|
|
146
146
|
workflowId: options.workflowId || hotmesh_1.HotMesh.guid(),
|
|
147
147
|
workflowTopic: workflowTopic,
|
|
148
|
+
taskQueue: taskQueueName,
|
|
149
|
+
workflowName: workflowName,
|
|
148
150
|
backoffCoefficient: options.config?.backoffCoefficient || enums_1.HMSH_DURABLE_EXP_BACKOFF,
|
|
149
151
|
maximumAttempts: options.config?.maximumAttempts || enums_1.HMSH_DURABLE_MAX_ATTEMPTS,
|
|
150
152
|
maximumInterval: (0, utils_1.s)(options.config?.maximumInterval || enums_1.HMSH_DURABLE_MAX_INTERVAL),
|
|
@@ -186,11 +188,14 @@ class ClientService {
|
|
|
186
188
|
*/
|
|
187
189
|
hook: async (options) => {
|
|
188
190
|
const taskQueue = options.taskQueue ?? options.entity;
|
|
189
|
-
const
|
|
191
|
+
const hookWorkflowName = options.entity ?? options.workflowName;
|
|
192
|
+
const workflowTopic = `${taskQueue}-${hookWorkflowName}`;
|
|
190
193
|
const payload = {
|
|
191
194
|
arguments: [...options.args],
|
|
192
195
|
id: options.workflowId,
|
|
193
196
|
workflowTopic,
|
|
197
|
+
taskQueue,
|
|
198
|
+
workflowName: hookWorkflowName,
|
|
194
199
|
backoffCoefficient: options.config?.backoffCoefficient || enums_1.HMSH_DURABLE_EXP_BACKOFF,
|
|
195
200
|
maximumAttempts: options.config?.maximumAttempts || enums_1.HMSH_DURABLE_MAX_ATTEMPTS,
|
|
196
201
|
maximumInterval: (0, utils_1.s)(options.config?.maximumInterval || enums_1.HMSH_DURABLE_MAX_INTERVAL),
|
|
@@ -63,6 +63,21 @@ declare class ExporterService {
|
|
|
63
63
|
* their executions as nested `children`.
|
|
64
64
|
*/
|
|
65
65
|
exportExecution(jobId: string, workflowTopic: string, options?: ExecutionExportOptions): Promise<WorkflowExecution>;
|
|
66
|
+
/**
|
|
67
|
+
* Reconstruct a WorkflowExecution from raw database rows when the job
|
|
68
|
+
* handle has expired or been pruned. Only available if the store provider
|
|
69
|
+
* implements getJobByKeyDirect.
|
|
70
|
+
*/
|
|
71
|
+
private exportExecutionDirect;
|
|
72
|
+
/**
|
|
73
|
+
* Enrich execution events with activity and child workflow inputs.
|
|
74
|
+
* Queries the store for activity arguments and child workflow arguments.
|
|
75
|
+
*/
|
|
76
|
+
private enrichExecutionInputs;
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a symbol field from stable JSON path using the symbol registry.
|
|
79
|
+
*/
|
|
80
|
+
private resolveSymbolField;
|
|
66
81
|
/**
|
|
67
82
|
* Pure transformation: convert a raw DurableJobExport into a
|
|
68
83
|
* Temporal-compatible WorkflowExecution event history.
|
|
@@ -182,14 +182,393 @@ class ExporterService {
|
|
|
182
182
|
* their executions as nested `children`.
|
|
183
183
|
*/
|
|
184
184
|
async exportExecution(jobId, workflowTopic, options = {}) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
let execution;
|
|
186
|
+
try {
|
|
187
|
+
const raw = await this.export(jobId);
|
|
188
|
+
execution = this.transformToExecution(raw, jobId, workflowTopic, options);
|
|
189
|
+
if (options.mode === 'verbose') {
|
|
190
|
+
const maxDepth = options.max_depth ?? 5;
|
|
191
|
+
execution.children = await this.fetchChildren(raw, workflowTopic, options, 1, maxDepth);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// Fallback to direct query for expired/pruned jobs
|
|
196
|
+
if (options.allow_direct_query && this.store.getJobByKeyDirect) {
|
|
197
|
+
this.logger.debug('Job export failed, attempting direct query', { jobId, error: error.message });
|
|
198
|
+
execution = await this.exportExecutionDirect(jobId, workflowTopic, options);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Enrich with activity/child workflow inputs if requested
|
|
205
|
+
if (options.enrich_inputs) {
|
|
206
|
+
await this.enrichExecutionInputs(execution, jobId);
|
|
190
207
|
}
|
|
191
208
|
return execution;
|
|
192
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Reconstruct a WorkflowExecution from raw database rows when the job
|
|
212
|
+
* handle has expired or been pruned. Only available if the store provider
|
|
213
|
+
* implements getJobByKeyDirect.
|
|
214
|
+
*/
|
|
215
|
+
async exportExecutionDirect(workflowId, workflowTopic, options) {
|
|
216
|
+
if (!this.store.getJobByKeyDirect) {
|
|
217
|
+
throw new Error('Direct query not supported by this store provider');
|
|
218
|
+
}
|
|
219
|
+
const jobKey = `hmsh:${this.appId}:j:${workflowId}`;
|
|
220
|
+
const { job, attributes } = await this.store.getJobByKeyDirect(jobKey);
|
|
221
|
+
// Parse metadata for timing
|
|
222
|
+
const startTime = attributes['aoa'] ? parseTimestamp(attributes['aoa']) : job.created_at?.toISOString();
|
|
223
|
+
const closeTime = attributes['apa'] ? parseTimestamp(attributes['apa']) : job.updated_at?.toISOString();
|
|
224
|
+
// Parse workflow result
|
|
225
|
+
let workflowResult;
|
|
226
|
+
if (attributes['aBa']) {
|
|
227
|
+
const raw = attributes['aBa'].startsWith('/s') ? attributes['aBa'].slice(2) : attributes['aBa'];
|
|
228
|
+
try {
|
|
229
|
+
workflowResult = JSON.parse(raw);
|
|
230
|
+
}
|
|
231
|
+
catch { /* ignore */ }
|
|
232
|
+
}
|
|
233
|
+
// Build events from timeline operations
|
|
234
|
+
const events = [];
|
|
235
|
+
let nextId = 1;
|
|
236
|
+
// Helper to extract operation entries from attributes
|
|
237
|
+
const getOperationKeys = (prefix) => {
|
|
238
|
+
return Object.keys(attributes)
|
|
239
|
+
.filter((k) => k.startsWith(prefix))
|
|
240
|
+
.sort((a, b) => {
|
|
241
|
+
const numA = parseInt(a.replace(new RegExp(`${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|-`, 'g'), ''));
|
|
242
|
+
const numB = parseInt(b.replace(new RegExp(`${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}|-`, 'g'), ''));
|
|
243
|
+
return numA - numB;
|
|
244
|
+
})
|
|
245
|
+
.map((key) => {
|
|
246
|
+
const raw = attributes[key].startsWith('/s') ? attributes[key].slice(2) : attributes[key];
|
|
247
|
+
try {
|
|
248
|
+
return { key, index: parseInt(key.replace(/[^0-9]/g, '')), val: JSON.parse(raw) };
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
.filter(Boolean);
|
|
255
|
+
};
|
|
256
|
+
let systemCount = 0;
|
|
257
|
+
let userCount = 0;
|
|
258
|
+
let activityCompleted = 0;
|
|
259
|
+
let activityFailed = 0;
|
|
260
|
+
let childTotal = 0;
|
|
261
|
+
let childCompleted = 0;
|
|
262
|
+
let childFailed = 0;
|
|
263
|
+
let timerCount = 0;
|
|
264
|
+
let signalCount = 0;
|
|
265
|
+
// Process proxy (activities)
|
|
266
|
+
for (const { key, index, val } of getOperationKeys('-proxy-')) {
|
|
267
|
+
const activityName = extractActivityName(val);
|
|
268
|
+
const isSystem = isSystemActivity(activityName);
|
|
269
|
+
const ac = val.ac;
|
|
270
|
+
const au = val.au;
|
|
271
|
+
const dur = computeDuration(ac, au);
|
|
272
|
+
const hasError = '$error' in val;
|
|
273
|
+
if (isSystem)
|
|
274
|
+
systemCount++;
|
|
275
|
+
else
|
|
276
|
+
userCount++;
|
|
277
|
+
if (options.exclude_system && isSystem)
|
|
278
|
+
continue;
|
|
279
|
+
if (ac) {
|
|
280
|
+
events.push(makeEvent(nextId++, 'activity_task_scheduled', 'activity', parseTimestamp(ac), null, isSystem, {
|
|
281
|
+
kind: 'activity_task_scheduled',
|
|
282
|
+
activity_type: activityName,
|
|
283
|
+
timeline_key: val.job_id || key,
|
|
284
|
+
execution_index: index,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
if (au) {
|
|
288
|
+
if (hasError) {
|
|
289
|
+
activityFailed++;
|
|
290
|
+
events.push(makeEvent(nextId++, 'activity_task_failed', 'activity', parseTimestamp(au), dur, isSystem, {
|
|
291
|
+
kind: 'activity_task_failed',
|
|
292
|
+
activity_type: activityName,
|
|
293
|
+
failure: val.$error,
|
|
294
|
+
timeline_key: val.job_id || key,
|
|
295
|
+
execution_index: index,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
activityCompleted++;
|
|
300
|
+
events.push(makeEvent(nextId++, 'activity_task_completed', 'activity', parseTimestamp(au), dur, isSystem, {
|
|
301
|
+
kind: 'activity_task_completed',
|
|
302
|
+
activity_type: activityName,
|
|
303
|
+
result: options.omit_results ? undefined : val.data,
|
|
304
|
+
timeline_key: val.job_id || key,
|
|
305
|
+
execution_index: index,
|
|
306
|
+
}));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Process wait (signals)
|
|
311
|
+
for (const { key, index, val } of getOperationKeys('-wait-')) {
|
|
312
|
+
const signalName = val.id || val.data?.id || val.data?.data?.id || `signal-${index}`;
|
|
313
|
+
const ac = val.ac;
|
|
314
|
+
const au = val.au;
|
|
315
|
+
const dur = computeDuration(ac, au);
|
|
316
|
+
signalCount++;
|
|
317
|
+
const ts = au ? parseTimestamp(au) : ac ? parseTimestamp(ac) : startTime;
|
|
318
|
+
events.push(makeEvent(nextId++, 'workflow_execution_signaled', 'signal', ts, dur, false, {
|
|
319
|
+
kind: 'workflow_execution_signaled',
|
|
320
|
+
signal_name: signalName,
|
|
321
|
+
input: options.omit_results ? undefined : val.data?.data,
|
|
322
|
+
timeline_key: val.job_id || key,
|
|
323
|
+
execution_index: index,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
// Process sleep (timers)
|
|
327
|
+
for (const { key, index, val } of getOperationKeys('-sleep-')) {
|
|
328
|
+
const ac = val.ac;
|
|
329
|
+
const au = val.au;
|
|
330
|
+
const dur = computeDuration(ac, au);
|
|
331
|
+
timerCount++;
|
|
332
|
+
if (ac) {
|
|
333
|
+
events.push(makeEvent(nextId++, 'timer_started', 'timer', parseTimestamp(ac), null, false, {
|
|
334
|
+
kind: 'timer_started',
|
|
335
|
+
duration_ms: dur ?? undefined,
|
|
336
|
+
timeline_key: val.job_id || key,
|
|
337
|
+
execution_index: index,
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
if (au) {
|
|
341
|
+
events.push(makeEvent(nextId++, 'timer_fired', 'timer', parseTimestamp(au), dur, false, {
|
|
342
|
+
kind: 'timer_fired',
|
|
343
|
+
timeline_key: val.job_id || key,
|
|
344
|
+
execution_index: index,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Process child (awaited child workflows)
|
|
349
|
+
for (const { key, index, val } of getOperationKeys('-child-')) {
|
|
350
|
+
const childId = val.job_id || key;
|
|
351
|
+
const ac = val.ac;
|
|
352
|
+
const au = val.au;
|
|
353
|
+
const dur = computeDuration(ac, au);
|
|
354
|
+
const hasError = '$error' in val;
|
|
355
|
+
childTotal++;
|
|
356
|
+
if (ac) {
|
|
357
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_started', 'child_workflow', parseTimestamp(ac), null, false, {
|
|
358
|
+
kind: 'child_workflow_execution_started',
|
|
359
|
+
child_workflow_id: childId,
|
|
360
|
+
awaited: true,
|
|
361
|
+
timeline_key: childId,
|
|
362
|
+
execution_index: index,
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
if (au) {
|
|
366
|
+
if (hasError) {
|
|
367
|
+
childFailed++;
|
|
368
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_failed', 'child_workflow', parseTimestamp(au), dur, false, {
|
|
369
|
+
kind: 'child_workflow_execution_failed',
|
|
370
|
+
child_workflow_id: childId,
|
|
371
|
+
failure: val.$error,
|
|
372
|
+
timeline_key: childId,
|
|
373
|
+
execution_index: index,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
childCompleted++;
|
|
378
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_completed', 'child_workflow', parseTimestamp(au), dur, false, {
|
|
379
|
+
kind: 'child_workflow_execution_completed',
|
|
380
|
+
child_workflow_id: childId,
|
|
381
|
+
result: options.omit_results ? undefined : val.data,
|
|
382
|
+
timeline_key: childId,
|
|
383
|
+
execution_index: index,
|
|
384
|
+
}));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Process start (fire-and-forget child workflows)
|
|
389
|
+
for (const { key, index, val } of getOperationKeys('-start-')) {
|
|
390
|
+
const childId = val.job_id || key;
|
|
391
|
+
const ac = val.ac;
|
|
392
|
+
const au = val.au;
|
|
393
|
+
const ts = ac ? parseTimestamp(ac) : au ? parseTimestamp(au) : startTime;
|
|
394
|
+
childTotal++;
|
|
395
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_started', 'child_workflow', ts, null, false, {
|
|
396
|
+
kind: 'child_workflow_execution_started',
|
|
397
|
+
child_workflow_id: childId,
|
|
398
|
+
awaited: false,
|
|
399
|
+
timeline_key: childId,
|
|
400
|
+
execution_index: index,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
// Sort chronologically and re-number
|
|
404
|
+
events.sort((a, b) => {
|
|
405
|
+
const cmp = a.event_time.localeCompare(b.event_time);
|
|
406
|
+
return cmp !== 0 ? cmp : a.event_id - b.event_id;
|
|
407
|
+
});
|
|
408
|
+
for (let i = 0; i < events.length; i++) {
|
|
409
|
+
events[i].event_id = i + 1;
|
|
410
|
+
}
|
|
411
|
+
// Back-references
|
|
412
|
+
const scheduledMap = new Map();
|
|
413
|
+
const initiatedMap = new Map();
|
|
414
|
+
for (const e of events) {
|
|
415
|
+
const attrs = e.attributes;
|
|
416
|
+
if (e.event_type === 'activity_task_scheduled' && attrs.timeline_key) {
|
|
417
|
+
scheduledMap.set(attrs.timeline_key, e.event_id);
|
|
418
|
+
}
|
|
419
|
+
if (e.event_type === 'child_workflow_execution_started' && attrs.timeline_key) {
|
|
420
|
+
initiatedMap.set(attrs.timeline_key, e.event_id);
|
|
421
|
+
}
|
|
422
|
+
if ((e.event_type === 'activity_task_completed' || e.event_type === 'activity_task_failed') && attrs.timeline_key) {
|
|
423
|
+
attrs.scheduled_event_id = scheduledMap.get(attrs.timeline_key) ?? null;
|
|
424
|
+
}
|
|
425
|
+
if ((e.event_type === 'child_workflow_execution_completed' || e.event_type === 'child_workflow_execution_failed') && attrs.timeline_key) {
|
|
426
|
+
attrs.initiated_event_id = initiatedMap.get(attrs.timeline_key) ?? null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// Compute total duration
|
|
430
|
+
let totalDurationMs = null;
|
|
431
|
+
if (startTime && closeTime) {
|
|
432
|
+
const diffMs = new Date(closeTime).getTime() - new Date(startTime).getTime();
|
|
433
|
+
if (diffMs >= 0)
|
|
434
|
+
totalDurationMs = diffMs;
|
|
435
|
+
}
|
|
436
|
+
const proxyTotal = systemCount + userCount;
|
|
437
|
+
return {
|
|
438
|
+
workflow_id: workflowId,
|
|
439
|
+
workflow_type: workflowTopic,
|
|
440
|
+
task_queue: workflowTopic,
|
|
441
|
+
status: 'completed',
|
|
442
|
+
start_time: startTime || null,
|
|
443
|
+
close_time: closeTime || null,
|
|
444
|
+
duration_ms: totalDurationMs,
|
|
445
|
+
result: workflowResult,
|
|
446
|
+
events,
|
|
447
|
+
summary: {
|
|
448
|
+
total_events: events.length,
|
|
449
|
+
activities: {
|
|
450
|
+
total: proxyTotal,
|
|
451
|
+
completed: activityCompleted,
|
|
452
|
+
failed: activityFailed,
|
|
453
|
+
system: systemCount,
|
|
454
|
+
user: userCount,
|
|
455
|
+
},
|
|
456
|
+
child_workflows: { total: childTotal, completed: childCompleted, failed: childFailed },
|
|
457
|
+
timers: timerCount,
|
|
458
|
+
signals: signalCount,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Enrich execution events with activity and child workflow inputs.
|
|
464
|
+
* Queries the store for activity arguments and child workflow arguments.
|
|
465
|
+
*/
|
|
466
|
+
async enrichExecutionInputs(execution, workflowId) {
|
|
467
|
+
// Check if store supports exporter queries
|
|
468
|
+
if (!this.store.getActivityInputs || !this.store.getChildWorkflowInputs) {
|
|
469
|
+
this.logger.warn('Store does not support input enrichment (provider may not implement getActivityInputs/getChildWorkflowInputs)');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
// Resolve symbol fields for activity and workflow arguments using symbol keys
|
|
473
|
+
const symbolSets = await this.store.getSymbolKeys(['activity_trigger', 'trigger']);
|
|
474
|
+
const activityArgsField = this.resolveSymbolField(symbolSets, 'activity_trigger', 'activity_trigger/output/data/arguments');
|
|
475
|
+
const workflowArgsField = this.resolveSymbolField(symbolSets, 'trigger', 'trigger/output/data/arguments');
|
|
476
|
+
// ── 1. Enrich activity inputs ──
|
|
477
|
+
if (activityArgsField) {
|
|
478
|
+
const activityEvents = execution.events.filter((e) => e.event_type === 'activity_task_scheduled' || e.event_type === 'activity_task_completed' || e.event_type === 'activity_task_failed');
|
|
479
|
+
if (activityEvents.length > 0) {
|
|
480
|
+
const { byJobId, byNameIndex } = await this.store.getActivityInputs(workflowId, activityArgsField);
|
|
481
|
+
for (const evt of activityEvents) {
|
|
482
|
+
const attrs = evt.attributes;
|
|
483
|
+
let input = attrs.timeline_key ? byJobId.get(attrs.timeline_key) : undefined;
|
|
484
|
+
if (input === undefined && attrs.activity_type && attrs.execution_index !== undefined) {
|
|
485
|
+
input = byNameIndex.get(`${attrs.activity_type}:${attrs.execution_index}`);
|
|
486
|
+
}
|
|
487
|
+
if (input !== undefined) {
|
|
488
|
+
attrs.input = input;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
// ── 2. Enrich child workflow inputs ──
|
|
494
|
+
if (workflowArgsField) {
|
|
495
|
+
const childEvents = execution.events.filter((e) => e.event_type === 'child_workflow_execution_started');
|
|
496
|
+
if (childEvents.length > 0) {
|
|
497
|
+
const childIds = [...new Set(childEvents
|
|
498
|
+
.map((e) => e.attributes.child_workflow_id)
|
|
499
|
+
.filter(Boolean))];
|
|
500
|
+
if (childIds.length > 0) {
|
|
501
|
+
const childJobKeys = childIds.map((id) => `hmsh:${this.appId}:j:${id}`);
|
|
502
|
+
const childInputMap = await this.store.getChildWorkflowInputs(childJobKeys, workflowArgsField);
|
|
503
|
+
for (const evt of childEvents) {
|
|
504
|
+
const attrs = evt.attributes;
|
|
505
|
+
const input = childInputMap.get(attrs.child_workflow_id);
|
|
506
|
+
if (input !== undefined) {
|
|
507
|
+
attrs.input = input;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// ── 3. Stream-based fallback for unenriched activity events ──
|
|
514
|
+
// When job attributes have been pruned, recover inputs from worker_streams
|
|
515
|
+
if (this.store.getStreamHistory) {
|
|
516
|
+
const unenrichedEvents = execution.events.filter((e) => (e.event_type === 'activity_task_scheduled' ||
|
|
517
|
+
e.event_type === 'activity_task_completed' ||
|
|
518
|
+
e.event_type === 'activity_task_failed') &&
|
|
519
|
+
e.attributes.input === undefined);
|
|
520
|
+
if (unenrichedEvents.length > 0) {
|
|
521
|
+
const streamHistory = await this.store.getStreamHistory(workflowId, {
|
|
522
|
+
types: ['worker'],
|
|
523
|
+
});
|
|
524
|
+
// Build a map of aid -> stream message data (the worker invocation inputs)
|
|
525
|
+
const streamInputsByAid = new Map();
|
|
526
|
+
for (const entry of streamHistory) {
|
|
527
|
+
if (entry.msg_type === 'worker' && entry.data) {
|
|
528
|
+
const key = `${entry.aid}:${entry.dad || ''}`;
|
|
529
|
+
if (!streamInputsByAid.has(key)) {
|
|
530
|
+
streamInputsByAid.set(key, entry.data);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
for (const evt of unenrichedEvents) {
|
|
535
|
+
const attrs = evt.attributes;
|
|
536
|
+
// Try matching by activity_type + dimensional address
|
|
537
|
+
const key = `${attrs.activity_type}:${attrs.timeline_key || ''}`;
|
|
538
|
+
let input = streamInputsByAid.get(key);
|
|
539
|
+
if (input === undefined) {
|
|
540
|
+
// Fallback: match by activity name alone (first occurrence)
|
|
541
|
+
for (const [k, v] of streamInputsByAid) {
|
|
542
|
+
if (k.startsWith(`${attrs.activity_type}:`)) {
|
|
543
|
+
input = v;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (input !== undefined) {
|
|
549
|
+
attrs.input = input;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Resolve a symbol field from stable JSON path using the symbol registry.
|
|
557
|
+
*/
|
|
558
|
+
resolveSymbolField(symbolSets, range, path) {
|
|
559
|
+
// Get the symbol map for this range
|
|
560
|
+
const symbolMap = symbolSets[range];
|
|
561
|
+
if (!symbolMap) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
// Look up the symbol code for this path
|
|
565
|
+
const symbolCode = symbolMap[path];
|
|
566
|
+
if (!symbolCode) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
// Return with dimension suffix
|
|
570
|
+
return `${symbolCode},0`;
|
|
571
|
+
}
|
|
193
572
|
/**
|
|
194
573
|
* Pure transformation: convert a raw DurableJobExport into a
|
|
195
574
|
* Temporal-compatible WorkflowExecution event history.
|