@hotmeshio/hotmesh 0.10.1 → 0.10.2
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/package.json +3 -1
- package/build/services/dba/index.d.ts +52 -18
- package/build/services/dba/index.js +118 -29
- package/build/services/durable/exporter.d.ts +60 -3
- package/build/services/durable/exporter.js +430 -2
- package/build/services/durable/handle.d.ts +12 -1
- package/build/services/durable/handle.js +13 -0
- package/build/services/store/providers/postgres/kvtables.js +6 -1
- package/build/types/dba.d.ts +31 -5
- package/build/types/exporter.d.ts +127 -0
- package/build/types/index.d.ts +1 -1
- package/package.json +3 -1
package/build/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Permanent-Memory Workflows & AI Agents",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -46,6 +46,8 @@
|
|
|
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/exporter.test.ts",
|
|
50
|
+
"test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
|
|
49
51
|
"test:dba": "vitest run tests/dba",
|
|
50
52
|
"test:cycle": "vitest run tests/functional/cycle",
|
|
51
53
|
"test:functional": "vitest run tests/functional",
|
|
@@ -11,7 +11,7 @@ import { PostgresClientType } from '../../types/postgres';
|
|
|
11
11
|
* | Table | What accumulates |
|
|
12
12
|
* |---|---|
|
|
13
13
|
* | `{appId}.jobs` | Completed/expired jobs with `expired_at` set |
|
|
14
|
-
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `
|
|
14
|
+
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
|
|
15
15
|
* | `{appId}.streams` | Processed stream messages with `expired_at` set |
|
|
16
16
|
*
|
|
17
17
|
* The `DBA` service addresses this with two methods:
|
|
@@ -22,10 +22,29 @@ import { PostgresClientType } from '../../types/postgres';
|
|
|
22
22
|
* - {@link DBA.deploy | deploy()} — Pre-deploys the Postgres function
|
|
23
23
|
* (e.g., during CI/CD migrations) without running a prune.
|
|
24
24
|
*
|
|
25
|
-
* ##
|
|
25
|
+
* ## Attribute stripping preserves export history
|
|
26
|
+
*
|
|
27
|
+
* Stripping removes `adata`, `hmark`, `status`, and `other` attributes
|
|
28
|
+
* from completed jobs while preserving:
|
|
29
|
+
* - `jdata` — workflow return data
|
|
30
|
+
* - `udata` — user-searchable data
|
|
31
|
+
* - `jmark` — timeline markers needed for Temporal-compatible export
|
|
32
|
+
*
|
|
33
|
+
* Set `keepHmark: true` to also preserve `hmark` (activity state markers).
|
|
34
|
+
*
|
|
35
|
+
* ## Entity-scoped pruning
|
|
26
36
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
37
|
+
* Use `entities` to restrict pruning/stripping to specific entity types
|
|
38
|
+
* (e.g., `['book', 'author']`). Use `pruneTransient` to delete expired
|
|
39
|
+
* jobs with no entity (`entity IS NULL`).
|
|
40
|
+
*
|
|
41
|
+
* ## Idempotent stripping with `pruned_at`
|
|
42
|
+
*
|
|
43
|
+
* After stripping, jobs are marked with `pruned_at = NOW()`. Subsequent
|
|
44
|
+
* prune calls skip already-pruned jobs, making the operation idempotent
|
|
45
|
+
* and efficient for repeated scheduling.
|
|
46
|
+
*
|
|
47
|
+
* ## Independent cron schedules (TypeScript)
|
|
29
48
|
*
|
|
30
49
|
* @example
|
|
31
50
|
* ```typescript
|
|
@@ -38,7 +57,6 @@ import { PostgresClientType } from '../../types/postgres';
|
|
|
38
57
|
* };
|
|
39
58
|
*
|
|
40
59
|
* // Cron 1 — Nightly: strip execution artifacts from completed jobs
|
|
41
|
-
* // Keeps all jobs and their jdata/udata; keeps all streams.
|
|
42
60
|
* await DBA.prune({
|
|
43
61
|
* appId: 'myapp', connection,
|
|
44
62
|
* jobs: false, streams: false, attributes: true,
|
|
@@ -51,29 +69,34 @@ import { PostgresClientType } from '../../types/postgres';
|
|
|
51
69
|
* jobs: false, streams: true,
|
|
52
70
|
* });
|
|
53
71
|
*
|
|
54
|
-
* // Cron 3 — Weekly: remove expired jobs older than 30 days
|
|
72
|
+
* // Cron 3 — Weekly: remove expired 'book' jobs older than 30 days
|
|
55
73
|
* await DBA.prune({
|
|
56
74
|
* appId: 'myapp', connection,
|
|
57
75
|
* expire: '30 days',
|
|
58
76
|
* jobs: true, streams: false,
|
|
77
|
+
* entities: ['book'],
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* // Cron 4 — Weekly: remove transient (no entity) expired jobs
|
|
81
|
+
* await DBA.prune({
|
|
82
|
+
* appId: 'myapp', connection,
|
|
83
|
+
* expire: '7 days',
|
|
84
|
+
* jobs: false, streams: false,
|
|
85
|
+
* pruneTransient: true,
|
|
59
86
|
* });
|
|
60
87
|
* ```
|
|
61
88
|
*
|
|
62
89
|
* ## Direct SQL (schedulable via pg_cron)
|
|
63
90
|
*
|
|
64
91
|
* The underlying Postgres function can be called directly, without
|
|
65
|
-
* the TypeScript SDK.
|
|
66
|
-
* SQL client:
|
|
92
|
+
* the TypeScript SDK. The first 4 parameters are backwards-compatible:
|
|
67
93
|
*
|
|
68
94
|
* ```sql
|
|
69
95
|
* -- Strip attributes only (keep all jobs and streams)
|
|
70
96
|
* SELECT * FROM myapp.prune('0 seconds', false, false, true);
|
|
71
97
|
*
|
|
72
|
-
* -- Prune
|
|
73
|
-
* SELECT * FROM myapp.prune('
|
|
74
|
-
*
|
|
75
|
-
* -- Prune expired jobs older than 30 days (keep streams)
|
|
76
|
-
* SELECT * FROM myapp.prune('30 days', true, false, false);
|
|
98
|
+
* -- Prune only 'book' entity jobs older than 30 days
|
|
99
|
+
* SELECT * FROM myapp.prune('30 days', true, false, false, ARRAY['book']);
|
|
77
100
|
*
|
|
78
101
|
* -- Prune everything older than 7 days and strip attributes
|
|
79
102
|
* SELECT * FROM myapp.prune('7 days', true, true, true);
|
|
@@ -98,6 +121,12 @@ declare class DBA {
|
|
|
98
121
|
client: PostgresClientType;
|
|
99
122
|
release: () => Promise<void>;
|
|
100
123
|
}>;
|
|
124
|
+
/**
|
|
125
|
+
* Returns migration SQL for the `pruned_at` column.
|
|
126
|
+
* Handles existing deployments that lack the column.
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
static getMigrationSQL(schema: string): string;
|
|
101
130
|
/**
|
|
102
131
|
* Returns the SQL for the server-side `prune()` function.
|
|
103
132
|
* @private
|
|
@@ -105,7 +134,8 @@ declare class DBA {
|
|
|
105
134
|
static getPruneFunctionSQL(schema: string): string;
|
|
106
135
|
/**
|
|
107
136
|
* Deploys the `prune()` Postgres function into the target schema.
|
|
108
|
-
*
|
|
137
|
+
* Also runs schema migrations (e.g., adding `pruned_at` column).
|
|
138
|
+
* Idempotent — uses `CREATE OR REPLACE` and `IF NOT EXISTS`.
|
|
109
139
|
*
|
|
110
140
|
* The function is automatically deployed when {@link DBA.prune} is called,
|
|
111
141
|
* but this method is exposed for explicit control (e.g., CI/CD
|
|
@@ -138,12 +168,15 @@ declare class DBA {
|
|
|
138
168
|
*
|
|
139
169
|
* Operations (each enabled individually):
|
|
140
170
|
* 1. **jobs** — Hard-deletes expired jobs older than the retention
|
|
141
|
-
* window (FK CASCADE removes their attributes automatically)
|
|
171
|
+
* window (FK CASCADE removes their attributes automatically).
|
|
172
|
+
* Scoped by `entities` when set.
|
|
142
173
|
* 2. **streams** — Hard-deletes expired stream messages older than
|
|
143
174
|
* the retention window
|
|
144
175
|
* 3. **attributes** — Strips non-essential attributes (`adata`,
|
|
145
|
-
* `hmark`, `
|
|
146
|
-
*
|
|
176
|
+
* `hmark`, `status`, `other`) from completed, un-pruned jobs.
|
|
177
|
+
* Preserves `jdata`, `udata`, and `jmark`. Marks stripped
|
|
178
|
+
* jobs with `pruned_at` for idempotency.
|
|
179
|
+
* 4. **pruneTransient** — Deletes expired jobs with `entity IS NULL`
|
|
147
180
|
*
|
|
148
181
|
* @param options - Prune configuration
|
|
149
182
|
* @returns Counts of deleted/stripped rows
|
|
@@ -153,7 +186,7 @@ declare class DBA {
|
|
|
153
186
|
* import { Client as Postgres } from 'pg';
|
|
154
187
|
* import { DBA } from '@hotmeshio/hotmesh';
|
|
155
188
|
*
|
|
156
|
-
* // Strip attributes
|
|
189
|
+
* // Strip attributes from 'book' entities only
|
|
157
190
|
* await DBA.prune({
|
|
158
191
|
* appId: 'myapp',
|
|
159
192
|
* connection: {
|
|
@@ -163,6 +196,7 @@ declare class DBA {
|
|
|
163
196
|
* jobs: false,
|
|
164
197
|
* streams: false,
|
|
165
198
|
* attributes: true,
|
|
199
|
+
* entities: ['book'],
|
|
166
200
|
* });
|
|
167
201
|
* ```
|
|
168
202
|
*/
|
|
@@ -14,7 +14,7 @@ const postgres_1 = require("../connector/providers/postgres");
|
|
|
14
14
|
* | Table | What accumulates |
|
|
15
15
|
* |---|---|
|
|
16
16
|
* | `{appId}.jobs` | Completed/expired jobs with `expired_at` set |
|
|
17
|
-
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `
|
|
17
|
+
* | `{appId}.jobs_attributes` | Execution artifacts (`adata`, `hmark`, `status`, `other`) that are only needed during workflow execution |
|
|
18
18
|
* | `{appId}.streams` | Processed stream messages with `expired_at` set |
|
|
19
19
|
*
|
|
20
20
|
* The `DBA` service addresses this with two methods:
|
|
@@ -25,10 +25,29 @@ const postgres_1 = require("../connector/providers/postgres");
|
|
|
25
25
|
* - {@link DBA.deploy | deploy()} — Pre-deploys the Postgres function
|
|
26
26
|
* (e.g., during CI/CD migrations) without running a prune.
|
|
27
27
|
*
|
|
28
|
-
* ##
|
|
28
|
+
* ## Attribute stripping preserves export history
|
|
29
|
+
*
|
|
30
|
+
* Stripping removes `adata`, `hmark`, `status`, and `other` attributes
|
|
31
|
+
* from completed jobs while preserving:
|
|
32
|
+
* - `jdata` — workflow return data
|
|
33
|
+
* - `udata` — user-searchable data
|
|
34
|
+
* - `jmark` — timeline markers needed for Temporal-compatible export
|
|
35
|
+
*
|
|
36
|
+
* Set `keepHmark: true` to also preserve `hmark` (activity state markers).
|
|
37
|
+
*
|
|
38
|
+
* ## Entity-scoped pruning
|
|
29
39
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
40
|
+
* Use `entities` to restrict pruning/stripping to specific entity types
|
|
41
|
+
* (e.g., `['book', 'author']`). Use `pruneTransient` to delete expired
|
|
42
|
+
* jobs with no entity (`entity IS NULL`).
|
|
43
|
+
*
|
|
44
|
+
* ## Idempotent stripping with `pruned_at`
|
|
45
|
+
*
|
|
46
|
+
* After stripping, jobs are marked with `pruned_at = NOW()`. Subsequent
|
|
47
|
+
* prune calls skip already-pruned jobs, making the operation idempotent
|
|
48
|
+
* and efficient for repeated scheduling.
|
|
49
|
+
*
|
|
50
|
+
* ## Independent cron schedules (TypeScript)
|
|
32
51
|
*
|
|
33
52
|
* @example
|
|
34
53
|
* ```typescript
|
|
@@ -41,7 +60,6 @@ const postgres_1 = require("../connector/providers/postgres");
|
|
|
41
60
|
* };
|
|
42
61
|
*
|
|
43
62
|
* // Cron 1 — Nightly: strip execution artifacts from completed jobs
|
|
44
|
-
* // Keeps all jobs and their jdata/udata; keeps all streams.
|
|
45
63
|
* await DBA.prune({
|
|
46
64
|
* appId: 'myapp', connection,
|
|
47
65
|
* jobs: false, streams: false, attributes: true,
|
|
@@ -54,29 +72,34 @@ const postgres_1 = require("../connector/providers/postgres");
|
|
|
54
72
|
* jobs: false, streams: true,
|
|
55
73
|
* });
|
|
56
74
|
*
|
|
57
|
-
* // Cron 3 — Weekly: remove expired jobs older than 30 days
|
|
75
|
+
* // Cron 3 — Weekly: remove expired 'book' jobs older than 30 days
|
|
58
76
|
* await DBA.prune({
|
|
59
77
|
* appId: 'myapp', connection,
|
|
60
78
|
* expire: '30 days',
|
|
61
79
|
* jobs: true, streams: false,
|
|
80
|
+
* entities: ['book'],
|
|
81
|
+
* });
|
|
82
|
+
*
|
|
83
|
+
* // Cron 4 — Weekly: remove transient (no entity) expired jobs
|
|
84
|
+
* await DBA.prune({
|
|
85
|
+
* appId: 'myapp', connection,
|
|
86
|
+
* expire: '7 days',
|
|
87
|
+
* jobs: false, streams: false,
|
|
88
|
+
* pruneTransient: true,
|
|
62
89
|
* });
|
|
63
90
|
* ```
|
|
64
91
|
*
|
|
65
92
|
* ## Direct SQL (schedulable via pg_cron)
|
|
66
93
|
*
|
|
67
94
|
* The underlying Postgres function can be called directly, without
|
|
68
|
-
* the TypeScript SDK.
|
|
69
|
-
* SQL client:
|
|
95
|
+
* the TypeScript SDK. The first 4 parameters are backwards-compatible:
|
|
70
96
|
*
|
|
71
97
|
* ```sql
|
|
72
98
|
* -- Strip attributes only (keep all jobs and streams)
|
|
73
99
|
* SELECT * FROM myapp.prune('0 seconds', false, false, true);
|
|
74
100
|
*
|
|
75
|
-
* -- Prune
|
|
76
|
-
* SELECT * FROM myapp.prune('
|
|
77
|
-
*
|
|
78
|
-
* -- Prune expired jobs older than 30 days (keep streams)
|
|
79
|
-
* SELECT * FROM myapp.prune('30 days', true, false, false);
|
|
101
|
+
* -- Prune only 'book' entity jobs older than 30 days
|
|
102
|
+
* SELECT * FROM myapp.prune('30 days', true, false, false, ARRAY['book']);
|
|
80
103
|
*
|
|
81
104
|
* -- Prune everything older than 7 days and strip attributes
|
|
82
105
|
* SELECT * FROM myapp.prune('7 days', true, true, true);
|
|
@@ -121,6 +144,20 @@ class DBA {
|
|
|
121
144
|
release: async () => { },
|
|
122
145
|
};
|
|
123
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns migration SQL for the `pruned_at` column.
|
|
149
|
+
* Handles existing deployments that lack the column.
|
|
150
|
+
* @private
|
|
151
|
+
*/
|
|
152
|
+
static getMigrationSQL(schema) {
|
|
153
|
+
return `
|
|
154
|
+
ALTER TABLE ${schema}.jobs
|
|
155
|
+
ADD COLUMN IF NOT EXISTS pruned_at TIMESTAMP WITH TIME ZONE;
|
|
156
|
+
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_pruned_at
|
|
158
|
+
ON ${schema}.jobs (pruned_at) WHERE pruned_at IS NULL;
|
|
159
|
+
`;
|
|
160
|
+
}
|
|
124
161
|
/**
|
|
125
162
|
* Returns the SQL for the server-side `prune()` function.
|
|
126
163
|
* @private
|
|
@@ -131,12 +168,17 @@ class DBA {
|
|
|
131
168
|
retention INTERVAL DEFAULT INTERVAL '7 days',
|
|
132
169
|
prune_jobs BOOLEAN DEFAULT TRUE,
|
|
133
170
|
prune_streams BOOLEAN DEFAULT TRUE,
|
|
134
|
-
strip_attributes BOOLEAN DEFAULT FALSE
|
|
171
|
+
strip_attributes BOOLEAN DEFAULT FALSE,
|
|
172
|
+
entity_list TEXT[] DEFAULT NULL,
|
|
173
|
+
prune_transient BOOLEAN DEFAULT FALSE,
|
|
174
|
+
keep_hmark BOOLEAN DEFAULT FALSE
|
|
135
175
|
)
|
|
136
176
|
RETURNS TABLE(
|
|
137
177
|
deleted_jobs BIGINT,
|
|
138
178
|
deleted_streams BIGINT,
|
|
139
|
-
stripped_attributes BIGINT
|
|
179
|
+
stripped_attributes BIGINT,
|
|
180
|
+
deleted_transient BIGINT,
|
|
181
|
+
marked_pruned BIGINT
|
|
140
182
|
)
|
|
141
183
|
LANGUAGE plpgsql
|
|
142
184
|
AS $$
|
|
@@ -144,17 +186,30 @@ class DBA {
|
|
|
144
186
|
v_deleted_jobs BIGINT := 0;
|
|
145
187
|
v_deleted_streams BIGINT := 0;
|
|
146
188
|
v_stripped_attributes BIGINT := 0;
|
|
189
|
+
v_deleted_transient BIGINT := 0;
|
|
190
|
+
v_marked_pruned BIGINT := 0;
|
|
147
191
|
BEGIN
|
|
148
192
|
-- 1. Hard-delete expired jobs older than the retention window.
|
|
149
193
|
-- FK CASCADE on jobs_attributes handles attribute cleanup.
|
|
194
|
+
-- Optionally scoped to an entity allowlist.
|
|
150
195
|
IF prune_jobs THEN
|
|
151
196
|
DELETE FROM ${schema}.jobs
|
|
152
197
|
WHERE expired_at IS NOT NULL
|
|
153
|
-
AND expired_at < NOW() - retention
|
|
198
|
+
AND expired_at < NOW() - retention
|
|
199
|
+
AND (entity_list IS NULL OR entity = ANY(entity_list));
|
|
154
200
|
GET DIAGNOSTICS v_deleted_jobs = ROW_COUNT;
|
|
155
201
|
END IF;
|
|
156
202
|
|
|
157
|
-
-- 2. Hard-delete
|
|
203
|
+
-- 2. Hard-delete transient (entity IS NULL) expired jobs.
|
|
204
|
+
IF prune_transient THEN
|
|
205
|
+
DELETE FROM ${schema}.jobs
|
|
206
|
+
WHERE entity IS NULL
|
|
207
|
+
AND expired_at IS NOT NULL
|
|
208
|
+
AND expired_at < NOW() - retention;
|
|
209
|
+
GET DIAGNOSTICS v_deleted_transient = ROW_COUNT;
|
|
210
|
+
END IF;
|
|
211
|
+
|
|
212
|
+
-- 3. Hard-delete expired stream messages older than the retention window.
|
|
158
213
|
IF prune_streams THEN
|
|
159
214
|
DELETE FROM ${schema}.streams
|
|
160
215
|
WHERE expired_at IS NOT NULL
|
|
@@ -162,22 +217,45 @@ class DBA {
|
|
|
162
217
|
GET DIAGNOSTICS v_deleted_streams = ROW_COUNT;
|
|
163
218
|
END IF;
|
|
164
219
|
|
|
165
|
-
--
|
|
166
|
-
--
|
|
220
|
+
-- 4. Strip execution artifacts from completed, live, un-pruned jobs.
|
|
221
|
+
-- Always preserves: jdata, udata, jmark (timeline/export history).
|
|
222
|
+
-- Optionally preserves: hmark (when keep_hmark is true).
|
|
167
223
|
IF strip_attributes THEN
|
|
168
|
-
|
|
169
|
-
|
|
224
|
+
WITH target_jobs AS (
|
|
225
|
+
SELECT id FROM ${schema}.jobs
|
|
226
|
+
WHERE status = 0
|
|
227
|
+
AND is_live = TRUE
|
|
228
|
+
AND pruned_at IS NULL
|
|
229
|
+
AND (entity_list IS NULL OR entity = ANY(entity_list))
|
|
230
|
+
),
|
|
231
|
+
deleted AS (
|
|
232
|
+
DELETE FROM ${schema}.jobs_attributes
|
|
233
|
+
WHERE job_id IN (SELECT id FROM target_jobs)
|
|
234
|
+
AND type NOT IN ('jdata', 'udata', 'jmark')
|
|
235
|
+
AND (keep_hmark = FALSE OR type <> 'hmark')
|
|
236
|
+
RETURNING job_id
|
|
237
|
+
)
|
|
238
|
+
SELECT COUNT(*) INTO v_stripped_attributes FROM deleted;
|
|
239
|
+
|
|
240
|
+
-- Mark pruned jobs so they are skipped on future runs.
|
|
241
|
+
WITH target_jobs AS (
|
|
170
242
|
SELECT id FROM ${schema}.jobs
|
|
171
243
|
WHERE status = 0
|
|
172
244
|
AND is_live = TRUE
|
|
245
|
+
AND pruned_at IS NULL
|
|
246
|
+
AND (entity_list IS NULL OR entity = ANY(entity_list))
|
|
173
247
|
)
|
|
174
|
-
|
|
175
|
-
|
|
248
|
+
UPDATE ${schema}.jobs
|
|
249
|
+
SET pruned_at = NOW()
|
|
250
|
+
WHERE id IN (SELECT id FROM target_jobs);
|
|
251
|
+
GET DIAGNOSTICS v_marked_pruned = ROW_COUNT;
|
|
176
252
|
END IF;
|
|
177
253
|
|
|
178
254
|
deleted_jobs := v_deleted_jobs;
|
|
179
255
|
deleted_streams := v_deleted_streams;
|
|
180
256
|
stripped_attributes := v_stripped_attributes;
|
|
257
|
+
deleted_transient := v_deleted_transient;
|
|
258
|
+
marked_pruned := v_marked_pruned;
|
|
181
259
|
RETURN NEXT;
|
|
182
260
|
END;
|
|
183
261
|
$$;
|
|
@@ -185,7 +263,8 @@ class DBA {
|
|
|
185
263
|
}
|
|
186
264
|
/**
|
|
187
265
|
* Deploys the `prune()` Postgres function into the target schema.
|
|
188
|
-
*
|
|
266
|
+
* Also runs schema migrations (e.g., adding `pruned_at` column).
|
|
267
|
+
* Idempotent — uses `CREATE OR REPLACE` and `IF NOT EXISTS`.
|
|
189
268
|
*
|
|
190
269
|
* The function is automatically deployed when {@link DBA.prune} is called,
|
|
191
270
|
* but this method is exposed for explicit control (e.g., CI/CD
|
|
@@ -214,6 +293,7 @@ class DBA {
|
|
|
214
293
|
const schema = DBA.safeName(appId);
|
|
215
294
|
const { client, release } = await DBA.getClient(connection);
|
|
216
295
|
try {
|
|
296
|
+
await client.query(DBA.getMigrationSQL(schema));
|
|
217
297
|
await client.query(DBA.getPruneFunctionSQL(schema));
|
|
218
298
|
}
|
|
219
299
|
finally {
|
|
@@ -227,12 +307,15 @@ class DBA {
|
|
|
227
307
|
*
|
|
228
308
|
* Operations (each enabled individually):
|
|
229
309
|
* 1. **jobs** — Hard-deletes expired jobs older than the retention
|
|
230
|
-
* window (FK CASCADE removes their attributes automatically)
|
|
310
|
+
* window (FK CASCADE removes their attributes automatically).
|
|
311
|
+
* Scoped by `entities` when set.
|
|
231
312
|
* 2. **streams** — Hard-deletes expired stream messages older than
|
|
232
313
|
* the retention window
|
|
233
314
|
* 3. **attributes** — Strips non-essential attributes (`adata`,
|
|
234
|
-
* `hmark`, `
|
|
235
|
-
*
|
|
315
|
+
* `hmark`, `status`, `other`) from completed, un-pruned jobs.
|
|
316
|
+
* Preserves `jdata`, `udata`, and `jmark`. Marks stripped
|
|
317
|
+
* jobs with `pruned_at` for idempotency.
|
|
318
|
+
* 4. **pruneTransient** — Deletes expired jobs with `entity IS NULL`
|
|
236
319
|
*
|
|
237
320
|
* @param options - Prune configuration
|
|
238
321
|
* @returns Counts of deleted/stripped rows
|
|
@@ -242,7 +325,7 @@ class DBA {
|
|
|
242
325
|
* import { Client as Postgres } from 'pg';
|
|
243
326
|
* import { DBA } from '@hotmeshio/hotmesh';
|
|
244
327
|
*
|
|
245
|
-
* // Strip attributes
|
|
328
|
+
* // Strip attributes from 'book' entities only
|
|
246
329
|
* await DBA.prune({
|
|
247
330
|
* appId: 'myapp',
|
|
248
331
|
* connection: {
|
|
@@ -252,6 +335,7 @@ class DBA {
|
|
|
252
335
|
* jobs: false,
|
|
253
336
|
* streams: false,
|
|
254
337
|
* attributes: true,
|
|
338
|
+
* entities: ['book'],
|
|
255
339
|
* });
|
|
256
340
|
* ```
|
|
257
341
|
*/
|
|
@@ -261,15 +345,20 @@ class DBA {
|
|
|
261
345
|
const jobs = options.jobs ?? true;
|
|
262
346
|
const streams = options.streams ?? true;
|
|
263
347
|
const attributes = options.attributes ?? false;
|
|
348
|
+
const entities = options.entities ?? null;
|
|
349
|
+
const pruneTransient = options.pruneTransient ?? false;
|
|
350
|
+
const keepHmark = options.keepHmark ?? false;
|
|
264
351
|
await DBA.deploy(options.connection, options.appId);
|
|
265
352
|
const { client, release } = await DBA.getClient(options.connection);
|
|
266
353
|
try {
|
|
267
|
-
const result = await client.query(`SELECT * FROM ${schema}.prune($1::interval, $2::boolean, $3::boolean, $4::boolean)`, [expire, jobs, streams, attributes]);
|
|
354
|
+
const result = await client.query(`SELECT * FROM ${schema}.prune($1::interval, $2::boolean, $3::boolean, $4::boolean, $5::text[], $6::boolean, $7::boolean)`, [expire, jobs, streams, attributes, entities, pruneTransient, keepHmark]);
|
|
268
355
|
const row = result.rows[0];
|
|
269
356
|
return {
|
|
270
357
|
jobs: Number(row.deleted_jobs),
|
|
271
358
|
streams: Number(row.deleted_streams),
|
|
272
359
|
attributes: Number(row.stripped_attributes),
|
|
360
|
+
transient: Number(row.deleted_transient),
|
|
361
|
+
marked: Number(row.marked_pruned),
|
|
273
362
|
};
|
|
274
363
|
}
|
|
275
364
|
finally {
|
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
import { ILogger } from '../logger';
|
|
2
2
|
import { StoreService } from '../store';
|
|
3
|
-
import { ExportOptions, DurableJobExport, TimelineType, TransitionType, ExportFields } from '../../types/exporter';
|
|
3
|
+
import { ExportOptions, DurableJobExport, TimelineType, TransitionType, ExportFields, ExecutionExportOptions, WorkflowExecution, WorkflowExecutionStatus } from '../../types/exporter';
|
|
4
4
|
import { ProviderClient, ProviderTransaction } from '../../types/provider';
|
|
5
5
|
import { StringStringType, Symbols } from '../../types/serializer';
|
|
6
|
+
/**
|
|
7
|
+
* Parse a HotMesh compact timestamp (YYYYMMDDHHmmss.mmm) into ISO 8601.
|
|
8
|
+
* Also accepts ISO 8601 strings directly.
|
|
9
|
+
*/
|
|
10
|
+
declare function parseTimestamp(raw: string | undefined | null): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* Compute duration in milliseconds between two HotMesh timestamps.
|
|
13
|
+
*/
|
|
14
|
+
declare function computeDuration(ac: string | undefined, au: string | undefined): number | null;
|
|
15
|
+
/**
|
|
16
|
+
* Extract the operation type (proxy, child, start, wait, sleep, hook)
|
|
17
|
+
* from a timeline key like `-proxy,0,0-1-`.
|
|
18
|
+
*/
|
|
19
|
+
declare function extractOperation(key: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Extract the activity name from a timeline entry value's job_id.
|
|
22
|
+
*
|
|
23
|
+
* Job ID format: `-{workflowId}-$${activityName}{dimension}-{execIndex}`
|
|
24
|
+
* Examples:
|
|
25
|
+
* `-wfId-$analyzeContent-5` → `'analyzeContent'`
|
|
26
|
+
* `-wfId-$processOrder,0,0-3` → `'processOrder'`
|
|
27
|
+
*/
|
|
28
|
+
declare function extractActivityName(value: Record<string, any> | null): string;
|
|
29
|
+
/**
|
|
30
|
+
* Check if an activity name is a system (interceptor) operation.
|
|
31
|
+
*/
|
|
32
|
+
declare function isSystemActivity(name: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Map HotMesh job state to a human-readable execution status.
|
|
35
|
+
*
|
|
36
|
+
* HotMesh semaphore: `0` = idle, `> 0` = pending activities,
|
|
37
|
+
* `< 0` = failed / interrupted.
|
|
38
|
+
*
|
|
39
|
+
* A workflow can be "done" (`state.data.done === true`) while the
|
|
40
|
+
* semaphore is still > 0 (cleanup activities pending). We check
|
|
41
|
+
* both the `done` flag and the semaphore to determine status.
|
|
42
|
+
*/
|
|
43
|
+
declare function mapStatus(rawStatus: number | undefined, isDone?: boolean, hasError?: boolean): WorkflowExecutionStatus;
|
|
6
44
|
declare class ExporterService {
|
|
7
45
|
appId: string;
|
|
8
46
|
logger: ILogger;
|
|
@@ -11,10 +49,29 @@ declare class ExporterService {
|
|
|
11
49
|
private static symbols;
|
|
12
50
|
constructor(appId: string, store: StoreService<ProviderClient, ProviderTransaction>, logger: ILogger);
|
|
13
51
|
/**
|
|
14
|
-
* Convert the job hash from its
|
|
52
|
+
* Convert the job hash from its compiled format into a DurableJobExport object with
|
|
15
53
|
* facets that describe the workflow in terms relevant to narrative storytelling.
|
|
16
54
|
*/
|
|
17
55
|
export(jobId: string, options?: ExportOptions): Promise<DurableJobExport>;
|
|
56
|
+
/**
|
|
57
|
+
* Export a workflow execution as a Temporal-compatible event history.
|
|
58
|
+
*
|
|
59
|
+
* **Sparse mode** (default): transforms the main workflow's timeline
|
|
60
|
+
* into a flat event list. No additional I/O beyond the initial export.
|
|
61
|
+
*
|
|
62
|
+
* **Verbose mode**: recursively fetches child workflow jobs and attaches
|
|
63
|
+
* their executions as nested `children`.
|
|
64
|
+
*/
|
|
65
|
+
exportExecution(jobId: string, workflowTopic: string, options?: ExecutionExportOptions): Promise<WorkflowExecution>;
|
|
66
|
+
/**
|
|
67
|
+
* Pure transformation: convert a raw DurableJobExport into a
|
|
68
|
+
* Temporal-compatible WorkflowExecution event history.
|
|
69
|
+
*/
|
|
70
|
+
transformToExecution(raw: DurableJobExport, workflowId: string, workflowTopic: string, options: ExecutionExportOptions): WorkflowExecution;
|
|
71
|
+
/**
|
|
72
|
+
* Recursively fetch child workflow executions for verbose mode.
|
|
73
|
+
*/
|
|
74
|
+
private fetchChildren;
|
|
18
75
|
/**
|
|
19
76
|
* Inflates the job data into a DurableJobExport object
|
|
20
77
|
* @param jobHash - the job data
|
|
@@ -48,4 +105,4 @@ declare class ExporterService {
|
|
|
48
105
|
*/
|
|
49
106
|
sortParts(parts: TimelineType[]): TimelineType[];
|
|
50
107
|
}
|
|
51
|
-
export { ExporterService };
|
|
108
|
+
export { ExporterService, parseTimestamp, computeDuration, extractOperation, extractActivityName, isSystemActivity, mapStatus, };
|
|
@@ -1,8 +1,158 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ExporterService = void 0;
|
|
3
|
+
exports.mapStatus = exports.isSystemActivity = exports.extractActivityName = exports.extractOperation = exports.computeDuration = exports.parseTimestamp = exports.ExporterService = void 0;
|
|
4
4
|
const utils_1 = require("../../modules/utils");
|
|
5
5
|
const serializer_1 = require("../serializer");
|
|
6
|
+
// ── Timestamp helpers ────────────────────────────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Parse a HotMesh compact timestamp (YYYYMMDDHHmmss.mmm) into ISO 8601.
|
|
9
|
+
* Also accepts ISO 8601 strings directly.
|
|
10
|
+
*/
|
|
11
|
+
function parseTimestamp(raw) {
|
|
12
|
+
if (!raw || typeof raw !== 'string')
|
|
13
|
+
return null;
|
|
14
|
+
const m = raw.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(?:\.?(\d+))?$/);
|
|
15
|
+
if (m) {
|
|
16
|
+
const [, yr, mo, dy, hr, mi, sc, ms] = m;
|
|
17
|
+
const frac = ms ? `.${ms.padEnd(3, '0').slice(0, 3)}` : '.000';
|
|
18
|
+
return `${yr}-${mo}-${dy}T${hr}:${mi}:${sc}${frac}Z`;
|
|
19
|
+
}
|
|
20
|
+
// Try ISO 8601
|
|
21
|
+
if (raw.includes('T') || raw.includes('-')) {
|
|
22
|
+
const d = new Date(raw);
|
|
23
|
+
if (!isNaN(d.getTime()))
|
|
24
|
+
return d.toISOString();
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
exports.parseTimestamp = parseTimestamp;
|
|
29
|
+
/**
|
|
30
|
+
* Compute duration in milliseconds between two HotMesh timestamps.
|
|
31
|
+
*/
|
|
32
|
+
function computeDuration(ac, au) {
|
|
33
|
+
const start = parseTimestamp(ac);
|
|
34
|
+
const end = parseTimestamp(au);
|
|
35
|
+
if (!start || !end)
|
|
36
|
+
return null;
|
|
37
|
+
return new Date(end).getTime() - new Date(start).getTime();
|
|
38
|
+
}
|
|
39
|
+
exports.computeDuration = computeDuration;
|
|
40
|
+
// ── Timeline key parsing ─────────────────────────────────────────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Extract the operation type (proxy, child, start, wait, sleep, hook)
|
|
43
|
+
* from a timeline key like `-proxy,0,0-1-`.
|
|
44
|
+
*/
|
|
45
|
+
function extractOperation(key) {
|
|
46
|
+
const parts = key.split('-').filter(Boolean);
|
|
47
|
+
return parts[0]?.split(',')[0] || 'unknown';
|
|
48
|
+
}
|
|
49
|
+
exports.extractOperation = extractOperation;
|
|
50
|
+
// ── Name extraction ──────────────────────────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Extract the activity name from a timeline entry value's job_id.
|
|
53
|
+
*
|
|
54
|
+
* Job ID format: `-{workflowId}-$${activityName}{dimension}-{execIndex}`
|
|
55
|
+
* Examples:
|
|
56
|
+
* `-wfId-$analyzeContent-5` → `'analyzeContent'`
|
|
57
|
+
* `-wfId-$processOrder,0,0-3` → `'processOrder'`
|
|
58
|
+
*/
|
|
59
|
+
function extractActivityName(value) {
|
|
60
|
+
const jobId = value?.job_id;
|
|
61
|
+
if (!jobId || typeof jobId !== 'string')
|
|
62
|
+
return 'unknown';
|
|
63
|
+
const dollarIdx = jobId.lastIndexOf('$');
|
|
64
|
+
if (dollarIdx === -1)
|
|
65
|
+
return jobId;
|
|
66
|
+
const afterDollar = jobId.substring(dollarIdx + 1);
|
|
67
|
+
const dashIdx = afterDollar.lastIndexOf('-');
|
|
68
|
+
const nameWithDim = dashIdx > 0 ? afterDollar.substring(0, dashIdx) : afterDollar;
|
|
69
|
+
// Strip dimension suffix (,N,N...)
|
|
70
|
+
const commaIdx = nameWithDim.indexOf(',');
|
|
71
|
+
return (commaIdx > 0 ? nameWithDim.substring(0, commaIdx) : nameWithDim) || 'unknown';
|
|
72
|
+
}
|
|
73
|
+
exports.extractActivityName = extractActivityName;
|
|
74
|
+
/**
|
|
75
|
+
* Check if an activity name is a system (interceptor) operation.
|
|
76
|
+
*/
|
|
77
|
+
function isSystemActivity(name) {
|
|
78
|
+
return name.startsWith('lt');
|
|
79
|
+
}
|
|
80
|
+
exports.isSystemActivity = isSystemActivity;
|
|
81
|
+
/**
|
|
82
|
+
* Extract a child workflow ID from a child/start timeline value.
|
|
83
|
+
*/
|
|
84
|
+
function extractChildWorkflowId(value) {
|
|
85
|
+
return value?.job_id || 'unknown';
|
|
86
|
+
}
|
|
87
|
+
// ── Status mapping ───────────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Map HotMesh job state to a human-readable execution status.
|
|
90
|
+
*
|
|
91
|
+
* HotMesh semaphore: `0` = idle, `> 0` = pending activities,
|
|
92
|
+
* `< 0` = failed / interrupted.
|
|
93
|
+
*
|
|
94
|
+
* A workflow can be "done" (`state.data.done === true`) while the
|
|
95
|
+
* semaphore is still > 0 (cleanup activities pending). We check
|
|
96
|
+
* both the `done` flag and the semaphore to determine status.
|
|
97
|
+
*/
|
|
98
|
+
function mapStatus(rawStatus, isDone, hasError) {
|
|
99
|
+
if (hasError || (rawStatus !== undefined && !isNaN(rawStatus) && rawStatus < 0)) {
|
|
100
|
+
return 'failed';
|
|
101
|
+
}
|
|
102
|
+
if (isDone || rawStatus === 0)
|
|
103
|
+
return 'completed';
|
|
104
|
+
if (rawStatus === undefined || isNaN(rawStatus))
|
|
105
|
+
return 'running';
|
|
106
|
+
return 'running';
|
|
107
|
+
}
|
|
108
|
+
exports.mapStatus = mapStatus;
|
|
109
|
+
// ── Event construction ───────────────────────────────────────────────────────
|
|
110
|
+
function makeEvent(event_id, event_type, category, event_time, duration_ms, is_system, attributes) {
|
|
111
|
+
return { event_id, event_type, category, event_time, duration_ms, is_system, attributes };
|
|
112
|
+
}
|
|
113
|
+
function computeSummary(events) {
|
|
114
|
+
const summary = {
|
|
115
|
+
total_events: events.length,
|
|
116
|
+
activities: { total: 0, completed: 0, failed: 0, system: 0, user: 0 },
|
|
117
|
+
child_workflows: { total: 0, completed: 0, failed: 0 },
|
|
118
|
+
timers: 0,
|
|
119
|
+
signals: 0,
|
|
120
|
+
};
|
|
121
|
+
for (const e of events) {
|
|
122
|
+
switch (e.event_type) {
|
|
123
|
+
case 'activity_task_scheduled':
|
|
124
|
+
summary.activities.total++;
|
|
125
|
+
if (e.is_system)
|
|
126
|
+
summary.activities.system++;
|
|
127
|
+
else
|
|
128
|
+
summary.activities.user++;
|
|
129
|
+
break;
|
|
130
|
+
case 'activity_task_completed':
|
|
131
|
+
summary.activities.completed++;
|
|
132
|
+
break;
|
|
133
|
+
case 'activity_task_failed':
|
|
134
|
+
summary.activities.failed++;
|
|
135
|
+
break;
|
|
136
|
+
case 'child_workflow_execution_started':
|
|
137
|
+
summary.child_workflows.total++;
|
|
138
|
+
break;
|
|
139
|
+
case 'child_workflow_execution_completed':
|
|
140
|
+
summary.child_workflows.completed++;
|
|
141
|
+
break;
|
|
142
|
+
case 'child_workflow_execution_failed':
|
|
143
|
+
summary.child_workflows.failed++;
|
|
144
|
+
break;
|
|
145
|
+
case 'timer_started':
|
|
146
|
+
summary.timers++;
|
|
147
|
+
break;
|
|
148
|
+
case 'workflow_execution_signaled':
|
|
149
|
+
summary.signals++;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return summary;
|
|
154
|
+
}
|
|
155
|
+
// ── Exporter Service ─────────────────────────────────────────────────────────
|
|
6
156
|
class ExporterService {
|
|
7
157
|
constructor(appId, store, logger) {
|
|
8
158
|
this.appId = appId;
|
|
@@ -10,7 +160,7 @@ class ExporterService {
|
|
|
10
160
|
this.store = store;
|
|
11
161
|
}
|
|
12
162
|
/**
|
|
13
|
-
* Convert the job hash from its
|
|
163
|
+
* Convert the job hash from its compiled format into a DurableJobExport object with
|
|
14
164
|
* facets that describe the workflow in terms relevant to narrative storytelling.
|
|
15
165
|
*/
|
|
16
166
|
async export(jobId, options = {}) {
|
|
@@ -22,6 +172,284 @@ class ExporterService {
|
|
|
22
172
|
const jobExport = this.inflate(jobData, options);
|
|
23
173
|
return jobExport;
|
|
24
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Export a workflow execution as a Temporal-compatible event history.
|
|
177
|
+
*
|
|
178
|
+
* **Sparse mode** (default): transforms the main workflow's timeline
|
|
179
|
+
* into a flat event list. No additional I/O beyond the initial export.
|
|
180
|
+
*
|
|
181
|
+
* **Verbose mode**: recursively fetches child workflow jobs and attaches
|
|
182
|
+
* their executions as nested `children`.
|
|
183
|
+
*/
|
|
184
|
+
async exportExecution(jobId, workflowTopic, options = {}) {
|
|
185
|
+
const raw = await this.export(jobId);
|
|
186
|
+
const execution = this.transformToExecution(raw, jobId, workflowTopic, options);
|
|
187
|
+
if (options.mode === 'verbose') {
|
|
188
|
+
const maxDepth = options.max_depth ?? 5;
|
|
189
|
+
execution.children = await this.fetchChildren(raw, workflowTopic, options, 1, maxDepth);
|
|
190
|
+
}
|
|
191
|
+
return execution;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Pure transformation: convert a raw DurableJobExport into a
|
|
195
|
+
* Temporal-compatible WorkflowExecution event history.
|
|
196
|
+
*/
|
|
197
|
+
transformToExecution(raw, workflowId, workflowTopic, options) {
|
|
198
|
+
const events = [];
|
|
199
|
+
let nextId = 1;
|
|
200
|
+
// ── Extract timing from state metadata ─────────────────────────
|
|
201
|
+
const state = raw.state;
|
|
202
|
+
const metadata = state?.output?.metadata ?? state?.metadata;
|
|
203
|
+
const stateData = state?.output?.data ?? state?.data;
|
|
204
|
+
const jobCreated = metadata?.jc ?? stateData?.jc ?? metadata?.ac;
|
|
205
|
+
const jobUpdated = metadata?.ju ?? stateData?.ju ?? metadata?.au;
|
|
206
|
+
const startTime = parseTimestamp(jobCreated);
|
|
207
|
+
const closeTime = parseTimestamp(jobUpdated);
|
|
208
|
+
// ── Synthetic workflow_execution_started ─────────────────────────
|
|
209
|
+
if (startTime) {
|
|
210
|
+
events.push(makeEvent(nextId++, 'workflow_execution_started', 'workflow', startTime, null, false, {
|
|
211
|
+
kind: 'workflow_execution_started',
|
|
212
|
+
workflow_type: workflowTopic,
|
|
213
|
+
task_queue: workflowTopic,
|
|
214
|
+
input: options.omit_results ? undefined : raw.data,
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
// ── Transform timeline entries ───────────────────────────────
|
|
218
|
+
const timeline = raw.timeline || [];
|
|
219
|
+
for (const entry of timeline) {
|
|
220
|
+
const operation = extractOperation(entry.key);
|
|
221
|
+
const val = (typeof entry.value === 'object' && entry.value !== null)
|
|
222
|
+
? entry.value
|
|
223
|
+
: null;
|
|
224
|
+
const ac = val?.ac;
|
|
225
|
+
const au = val?.au;
|
|
226
|
+
const acIso = parseTimestamp(ac);
|
|
227
|
+
const auIso = parseTimestamp(au);
|
|
228
|
+
const dur = computeDuration(ac, au);
|
|
229
|
+
const hasError = val != null && '$error' in val;
|
|
230
|
+
switch (operation) {
|
|
231
|
+
case 'proxy': {
|
|
232
|
+
const name = extractActivityName(val);
|
|
233
|
+
const isSys = isSystemActivity(name);
|
|
234
|
+
if (options.exclude_system && isSys)
|
|
235
|
+
break;
|
|
236
|
+
if (acIso) {
|
|
237
|
+
events.push(makeEvent(nextId++, 'activity_task_scheduled', 'activity', acIso, null, isSys, {
|
|
238
|
+
kind: 'activity_task_scheduled',
|
|
239
|
+
activity_type: name,
|
|
240
|
+
timeline_key: entry.key,
|
|
241
|
+
execution_index: entry.index,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
if (auIso) {
|
|
245
|
+
if (hasError) {
|
|
246
|
+
events.push(makeEvent(nextId++, 'activity_task_failed', 'activity', auIso, dur, isSys, {
|
|
247
|
+
kind: 'activity_task_failed',
|
|
248
|
+
activity_type: name,
|
|
249
|
+
failure: val?.$error,
|
|
250
|
+
timeline_key: entry.key,
|
|
251
|
+
execution_index: entry.index,
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
events.push(makeEvent(nextId++, 'activity_task_completed', 'activity', auIso, dur, isSys, {
|
|
256
|
+
kind: 'activity_task_completed',
|
|
257
|
+
activity_type: name,
|
|
258
|
+
result: options.omit_results ? undefined : val?.data,
|
|
259
|
+
timeline_key: entry.key,
|
|
260
|
+
execution_index: entry.index,
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
case 'child': {
|
|
267
|
+
const childId = extractChildWorkflowId(val);
|
|
268
|
+
if (acIso) {
|
|
269
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_started', 'child_workflow', acIso, null, false, {
|
|
270
|
+
kind: 'child_workflow_execution_started',
|
|
271
|
+
child_workflow_id: childId,
|
|
272
|
+
awaited: true,
|
|
273
|
+
timeline_key: entry.key,
|
|
274
|
+
execution_index: entry.index,
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
if (auIso) {
|
|
278
|
+
if (hasError) {
|
|
279
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_failed', 'child_workflow', auIso, dur, false, {
|
|
280
|
+
kind: 'child_workflow_execution_failed',
|
|
281
|
+
child_workflow_id: childId,
|
|
282
|
+
failure: val?.$error,
|
|
283
|
+
timeline_key: entry.key,
|
|
284
|
+
execution_index: entry.index,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_completed', 'child_workflow', auIso, dur, false, {
|
|
289
|
+
kind: 'child_workflow_execution_completed',
|
|
290
|
+
child_workflow_id: childId,
|
|
291
|
+
result: options.omit_results ? undefined : val?.data,
|
|
292
|
+
timeline_key: entry.key,
|
|
293
|
+
execution_index: entry.index,
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
case 'start': {
|
|
300
|
+
const childId = extractChildWorkflowId(val);
|
|
301
|
+
const ts = acIso || auIso;
|
|
302
|
+
if (ts) {
|
|
303
|
+
events.push(makeEvent(nextId++, 'child_workflow_execution_started', 'child_workflow', ts, null, false, {
|
|
304
|
+
kind: 'child_workflow_execution_started',
|
|
305
|
+
child_workflow_id: childId,
|
|
306
|
+
awaited: false,
|
|
307
|
+
timeline_key: entry.key,
|
|
308
|
+
execution_index: entry.index,
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'wait': {
|
|
314
|
+
const signalName = val?.id
|
|
315
|
+
|| val?.data?.id
|
|
316
|
+
|| val?.data?.data?.id
|
|
317
|
+
|| `signal-${entry.index}`;
|
|
318
|
+
const ts = auIso || acIso;
|
|
319
|
+
if (ts) {
|
|
320
|
+
events.push(makeEvent(nextId++, 'workflow_execution_signaled', 'signal', ts, dur, false, {
|
|
321
|
+
kind: 'workflow_execution_signaled',
|
|
322
|
+
signal_name: signalName,
|
|
323
|
+
input: options.omit_results ? undefined : val?.data?.data,
|
|
324
|
+
timeline_key: entry.key,
|
|
325
|
+
execution_index: entry.index,
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case 'sleep': {
|
|
331
|
+
if (acIso) {
|
|
332
|
+
events.push(makeEvent(nextId++, 'timer_started', 'timer', acIso, null, false, {
|
|
333
|
+
kind: 'timer_started',
|
|
334
|
+
duration_ms: dur ?? undefined,
|
|
335
|
+
timeline_key: entry.key,
|
|
336
|
+
execution_index: entry.index,
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
if (auIso) {
|
|
340
|
+
events.push(makeEvent(nextId++, 'timer_fired', 'timer', auIso, dur, false, {
|
|
341
|
+
kind: 'timer_fired',
|
|
342
|
+
timeline_key: entry.key,
|
|
343
|
+
execution_index: entry.index,
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
// Unknown operation types are silently skipped (forward-compatible)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
// ── Determine status ─────────────────────────────────────────
|
|
352
|
+
const isDone = stateData?.done === true;
|
|
353
|
+
const hasError = !!stateData?.$error;
|
|
354
|
+
const status = mapStatus(raw.status, isDone, hasError);
|
|
355
|
+
// ── Extract workflow result ──────────────────────────────────
|
|
356
|
+
const result = stateData?.response ?? (raw.data && Object.keys(raw.data).length > 0 ? raw.data : null);
|
|
357
|
+
// ── Synthetic workflow_execution_completed / failed ───────────
|
|
358
|
+
if (status === 'completed' && closeTime) {
|
|
359
|
+
const totalDur = startTime
|
|
360
|
+
? new Date(closeTime).getTime() - new Date(startTime).getTime()
|
|
361
|
+
: null;
|
|
362
|
+
events.push(makeEvent(nextId++, 'workflow_execution_completed', 'workflow', closeTime, totalDur, false, {
|
|
363
|
+
kind: 'workflow_execution_completed',
|
|
364
|
+
result: options.omit_results ? undefined : result,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
else if (status === 'failed' && closeTime) {
|
|
368
|
+
const totalDur = startTime
|
|
369
|
+
? new Date(closeTime).getTime() - new Date(startTime).getTime()
|
|
370
|
+
: null;
|
|
371
|
+
events.push(makeEvent(nextId++, 'workflow_execution_failed', 'workflow', closeTime, totalDur, false, {
|
|
372
|
+
kind: 'workflow_execution_failed',
|
|
373
|
+
failure: stateData?.err,
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
// ── Sort chronologically ─────────────────────────────────────
|
|
377
|
+
events.sort((a, b) => {
|
|
378
|
+
const cmp = a.event_time.localeCompare(b.event_time);
|
|
379
|
+
return cmp !== 0 ? cmp : a.event_id - b.event_id;
|
|
380
|
+
});
|
|
381
|
+
// ── Re-number event IDs after sort ───────────────────────────
|
|
382
|
+
for (let i = 0; i < events.length; i++) {
|
|
383
|
+
events[i].event_id = i + 1;
|
|
384
|
+
}
|
|
385
|
+
// ── Back-references (Temporal-compatible) ────────────────────
|
|
386
|
+
const scheduledMap = new Map();
|
|
387
|
+
const initiatedMap = new Map();
|
|
388
|
+
for (const e of events) {
|
|
389
|
+
const attrs = e.attributes;
|
|
390
|
+
if (e.event_type === 'activity_task_scheduled' && attrs.timeline_key) {
|
|
391
|
+
scheduledMap.set(attrs.timeline_key, e.event_id);
|
|
392
|
+
}
|
|
393
|
+
if (e.event_type === 'child_workflow_execution_started' && attrs.timeline_key) {
|
|
394
|
+
initiatedMap.set(attrs.timeline_key, e.event_id);
|
|
395
|
+
}
|
|
396
|
+
if ((e.event_type === 'activity_task_completed' || e.event_type === 'activity_task_failed') && attrs.timeline_key) {
|
|
397
|
+
attrs.scheduled_event_id = scheduledMap.get(attrs.timeline_key) ?? null;
|
|
398
|
+
}
|
|
399
|
+
if ((e.event_type === 'child_workflow_execution_completed' || e.event_type === 'child_workflow_execution_failed') && attrs.timeline_key) {
|
|
400
|
+
attrs.initiated_event_id = initiatedMap.get(attrs.timeline_key) ?? null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// ── Compute total duration ───────────────────────────────────
|
|
404
|
+
const totalDuration = (startTime && closeTime)
|
|
405
|
+
? new Date(closeTime).getTime() - new Date(startTime).getTime()
|
|
406
|
+
: null;
|
|
407
|
+
return {
|
|
408
|
+
workflow_id: workflowId,
|
|
409
|
+
workflow_type: workflowTopic,
|
|
410
|
+
task_queue: workflowTopic,
|
|
411
|
+
status,
|
|
412
|
+
start_time: startTime,
|
|
413
|
+
close_time: (status !== 'running') ? closeTime : null,
|
|
414
|
+
duration_ms: totalDuration,
|
|
415
|
+
result,
|
|
416
|
+
events,
|
|
417
|
+
summary: computeSummary(events),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Recursively fetch child workflow executions for verbose mode.
|
|
422
|
+
*/
|
|
423
|
+
async fetchChildren(raw, workflowTopic, options, depth, maxDepth) {
|
|
424
|
+
if (depth >= maxDepth)
|
|
425
|
+
return [];
|
|
426
|
+
const children = [];
|
|
427
|
+
const timeline = raw.timeline || [];
|
|
428
|
+
for (const entry of timeline) {
|
|
429
|
+
const operation = extractOperation(entry.key);
|
|
430
|
+
if (operation !== 'child' && operation !== 'start')
|
|
431
|
+
continue;
|
|
432
|
+
const val = (typeof entry.value === 'object' && entry.value !== null)
|
|
433
|
+
? entry.value
|
|
434
|
+
: null;
|
|
435
|
+
const childJobId = val?.job_id;
|
|
436
|
+
if (!childJobId || typeof childJobId !== 'string')
|
|
437
|
+
continue;
|
|
438
|
+
try {
|
|
439
|
+
const childRaw = await this.export(childJobId);
|
|
440
|
+
const childTopic = childRaw.data?.workflowTopic ?? workflowTopic;
|
|
441
|
+
const childExecution = this.transformToExecution(childRaw, childJobId, childTopic, options);
|
|
442
|
+
if (options.mode === 'verbose') {
|
|
443
|
+
childExecution.children = await this.fetchChildren(childRaw, childTopic, options, depth + 1, maxDepth);
|
|
444
|
+
}
|
|
445
|
+
children.push(childExecution);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// Child job may have expired or been cleaned up
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return children;
|
|
452
|
+
}
|
|
25
453
|
/**
|
|
26
454
|
* Inflates the job data into a DurableJobExport object
|
|
27
455
|
* @param jobHash - the job data
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { HotMesh } from '../hotmesh';
|
|
2
|
-
import { DurableJobExport, ExportOptions } from '../../types/exporter';
|
|
2
|
+
import { DurableJobExport, ExportOptions, ExecutionExportOptions, WorkflowExecution } from '../../types/exporter';
|
|
3
3
|
import { JobInterruptOptions } from '../../types/job';
|
|
4
4
|
import { StreamError } from '../../types/stream';
|
|
5
5
|
import { ExporterService } from './exporter';
|
|
@@ -44,6 +44,17 @@ export declare class WorkflowHandleService {
|
|
|
44
44
|
* Exports the workflow state to a JSON object.
|
|
45
45
|
*/
|
|
46
46
|
export(options?: ExportOptions): Promise<DurableJobExport>;
|
|
47
|
+
/**
|
|
48
|
+
* Exports the workflow as a Temporal-like execution event history.
|
|
49
|
+
*
|
|
50
|
+
* **Sparse mode** (default): transforms the main workflow's timeline
|
|
51
|
+
* into a flat event list with workflow lifecycle, activity, child workflow,
|
|
52
|
+
* timer, and signal events.
|
|
53
|
+
*
|
|
54
|
+
* **Verbose mode**: recursively fetches child workflow jobs and attaches
|
|
55
|
+
* their full execution histories as nested `children`.
|
|
56
|
+
*/
|
|
57
|
+
exportExecution(options?: ExecutionExportOptions): Promise<WorkflowExecution>;
|
|
47
58
|
/**
|
|
48
59
|
* Sends a signal to the workflow. This is a way to send
|
|
49
60
|
* a message to a workflow that is paused due to having
|
|
@@ -43,6 +43,19 @@ class WorkflowHandleService {
|
|
|
43
43
|
async export(options) {
|
|
44
44
|
return this.exporter.export(this.workflowId, options);
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Exports the workflow as a Temporal-like execution event history.
|
|
48
|
+
*
|
|
49
|
+
* **Sparse mode** (default): transforms the main workflow's timeline
|
|
50
|
+
* into a flat event list with workflow lifecycle, activity, child workflow,
|
|
51
|
+
* timer, and signal events.
|
|
52
|
+
*
|
|
53
|
+
* **Verbose mode**: recursively fetches child workflow jobs and attaches
|
|
54
|
+
* their full execution histories as nested `children`.
|
|
55
|
+
*/
|
|
56
|
+
async exportExecution(options) {
|
|
57
|
+
return this.exporter.exportExecution(this.workflowId, this.workflowTopic, options);
|
|
58
|
+
}
|
|
46
59
|
/**
|
|
47
60
|
* Sends a signal to the workflow. This is a way to send
|
|
48
61
|
* a message to a workflow that is paused due to having
|
|
@@ -182,6 +182,7 @@ const KVTables = (context) => ({
|
|
|
182
182
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
183
183
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
184
184
|
expired_at TIMESTAMP WITH TIME ZONE,
|
|
185
|
+
pruned_at TIMESTAMP WITH TIME ZONE,
|
|
185
186
|
is_live BOOLEAN DEFAULT TRUE,
|
|
186
187
|
PRIMARY KEY (id)
|
|
187
188
|
) PARTITION BY HASH (id);
|
|
@@ -214,8 +215,12 @@ const KVTables = (context) => ({
|
|
|
214
215
|
ON ${fullTableName} (entity, status);
|
|
215
216
|
`);
|
|
216
217
|
await client.query(`
|
|
217
|
-
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_expired_at
|
|
218
|
+
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_expired_at
|
|
218
219
|
ON ${fullTableName} (expired_at);
|
|
220
|
+
`);
|
|
221
|
+
await client.query(`
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_${tableDef.name}_pruned_at
|
|
223
|
+
ON ${fullTableName} (pruned_at) WHERE pruned_at IS NULL;
|
|
219
224
|
`);
|
|
220
225
|
// Create function to update is_live flag in the schema
|
|
221
226
|
await client.query(`
|
package/build/types/dba.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export interface PruneOptions {
|
|
|
31
31
|
/**
|
|
32
32
|
* If true, hard-deletes expired jobs older than the retention window.
|
|
33
33
|
* FK CASCADE on `jobs_attributes` automatically removes associated
|
|
34
|
-
* attribute rows.
|
|
34
|
+
* attribute rows. When `entities` is set, only matching jobs are deleted.
|
|
35
35
|
* @default true
|
|
36
36
|
*/
|
|
37
37
|
jobs?: boolean;
|
|
@@ -42,13 +42,35 @@ export interface PruneOptions {
|
|
|
42
42
|
*/
|
|
43
43
|
streams?: boolean;
|
|
44
44
|
/**
|
|
45
|
-
* If true, strips execution-artifact attributes
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
45
|
+
* If true, strips execution-artifact attributes from completed,
|
|
46
|
+
* un-pruned jobs. Preserves `jdata` (return data), `udata`
|
|
47
|
+
* (searchable data), and `jmark` (timeline/event history for
|
|
48
|
+
* Temporal-compatible export). See `keepHmark` for `hmark`.
|
|
49
49
|
* @default false
|
|
50
50
|
*/
|
|
51
51
|
attributes?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Entity allowlist. When provided, only jobs whose `entity` column
|
|
54
|
+
* matches one of these values are eligible for pruning/stripping.
|
|
55
|
+
* Jobs with `entity IS NULL` are excluded unless `pruneTransient`
|
|
56
|
+
* is also true.
|
|
57
|
+
* @default undefined (all entities)
|
|
58
|
+
*/
|
|
59
|
+
entities?: string[];
|
|
60
|
+
/**
|
|
61
|
+
* If true, hard-deletes expired jobs where `entity IS NULL`
|
|
62
|
+
* (transient workflow runs). Must also satisfy the retention
|
|
63
|
+
* window (`expire`).
|
|
64
|
+
* @default false
|
|
65
|
+
*/
|
|
66
|
+
pruneTransient?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* If true, `hmark` attributes are preserved during stripping
|
|
69
|
+
* (along with `jdata`, `udata`, and `jmark`). If false, `hmark`
|
|
70
|
+
* rows are stripped.
|
|
71
|
+
* @default false
|
|
72
|
+
*/
|
|
73
|
+
keepHmark?: boolean;
|
|
52
74
|
}
|
|
53
75
|
/**
|
|
54
76
|
* Result returned by `DBA.prune()`, providing deletion
|
|
@@ -61,4 +83,8 @@ export interface PruneResult {
|
|
|
61
83
|
streams: number;
|
|
62
84
|
/** Number of execution-artifact attribute rows stripped from completed jobs */
|
|
63
85
|
attributes: number;
|
|
86
|
+
/** Number of transient (entity IS NULL) job rows hard-deleted */
|
|
87
|
+
transient: number;
|
|
88
|
+
/** Number of jobs marked as pruned (pruned_at set) */
|
|
89
|
+
marked: number;
|
|
64
90
|
}
|
|
@@ -79,3 +79,130 @@ export interface JobExport {
|
|
|
79
79
|
process: StringAnyType;
|
|
80
80
|
status: string;
|
|
81
81
|
}
|
|
82
|
+
export type ExportMode = 'sparse' | 'verbose';
|
|
83
|
+
export type WorkflowEventType = 'workflow_execution_started' | 'workflow_execution_completed' | 'workflow_execution_failed' | 'activity_task_scheduled' | 'activity_task_completed' | 'activity_task_failed' | 'child_workflow_execution_started' | 'child_workflow_execution_completed' | 'child_workflow_execution_failed' | 'timer_started' | 'timer_fired' | 'workflow_execution_signaled';
|
|
84
|
+
export type WorkflowEventCategory = 'workflow' | 'activity' | 'child_workflow' | 'timer' | 'signal';
|
|
85
|
+
export interface WorkflowExecutionStartedAttributes {
|
|
86
|
+
kind: 'workflow_execution_started';
|
|
87
|
+
workflow_type: string;
|
|
88
|
+
task_queue: string;
|
|
89
|
+
input?: any;
|
|
90
|
+
}
|
|
91
|
+
export interface WorkflowExecutionCompletedAttributes {
|
|
92
|
+
kind: 'workflow_execution_completed';
|
|
93
|
+
result?: any;
|
|
94
|
+
}
|
|
95
|
+
export interface WorkflowExecutionFailedAttributes {
|
|
96
|
+
kind: 'workflow_execution_failed';
|
|
97
|
+
failure?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface ActivityTaskScheduledAttributes {
|
|
100
|
+
kind: 'activity_task_scheduled';
|
|
101
|
+
activity_type: string;
|
|
102
|
+
timeline_key: string;
|
|
103
|
+
execution_index: number;
|
|
104
|
+
}
|
|
105
|
+
export interface ActivityTaskCompletedAttributes {
|
|
106
|
+
kind: 'activity_task_completed';
|
|
107
|
+
activity_type: string;
|
|
108
|
+
result?: any;
|
|
109
|
+
scheduled_event_id?: number;
|
|
110
|
+
timeline_key: string;
|
|
111
|
+
execution_index: number;
|
|
112
|
+
}
|
|
113
|
+
export interface ActivityTaskFailedAttributes {
|
|
114
|
+
kind: 'activity_task_failed';
|
|
115
|
+
activity_type: string;
|
|
116
|
+
failure?: any;
|
|
117
|
+
scheduled_event_id?: number;
|
|
118
|
+
timeline_key: string;
|
|
119
|
+
execution_index: number;
|
|
120
|
+
}
|
|
121
|
+
export interface ChildWorkflowExecutionStartedAttributes {
|
|
122
|
+
kind: 'child_workflow_execution_started';
|
|
123
|
+
child_workflow_id: string;
|
|
124
|
+
awaited: boolean;
|
|
125
|
+
timeline_key: string;
|
|
126
|
+
execution_index: number;
|
|
127
|
+
}
|
|
128
|
+
export interface ChildWorkflowExecutionCompletedAttributes {
|
|
129
|
+
kind: 'child_workflow_execution_completed';
|
|
130
|
+
child_workflow_id: string;
|
|
131
|
+
result?: any;
|
|
132
|
+
initiated_event_id?: number;
|
|
133
|
+
timeline_key: string;
|
|
134
|
+
execution_index: number;
|
|
135
|
+
}
|
|
136
|
+
export interface ChildWorkflowExecutionFailedAttributes {
|
|
137
|
+
kind: 'child_workflow_execution_failed';
|
|
138
|
+
child_workflow_id: string;
|
|
139
|
+
failure?: any;
|
|
140
|
+
initiated_event_id?: number;
|
|
141
|
+
timeline_key: string;
|
|
142
|
+
execution_index: number;
|
|
143
|
+
}
|
|
144
|
+
export interface TimerStartedAttributes {
|
|
145
|
+
kind: 'timer_started';
|
|
146
|
+
duration_ms?: number;
|
|
147
|
+
timeline_key: string;
|
|
148
|
+
execution_index: number;
|
|
149
|
+
}
|
|
150
|
+
export interface TimerFiredAttributes {
|
|
151
|
+
kind: 'timer_fired';
|
|
152
|
+
timeline_key: string;
|
|
153
|
+
execution_index: number;
|
|
154
|
+
}
|
|
155
|
+
export interface WorkflowExecutionSignaledAttributes {
|
|
156
|
+
kind: 'workflow_execution_signaled';
|
|
157
|
+
signal_name: string;
|
|
158
|
+
input?: any;
|
|
159
|
+
timeline_key: string;
|
|
160
|
+
execution_index: number;
|
|
161
|
+
}
|
|
162
|
+
export type WorkflowEventAttributes = WorkflowExecutionStartedAttributes | WorkflowExecutionCompletedAttributes | WorkflowExecutionFailedAttributes | ActivityTaskScheduledAttributes | ActivityTaskCompletedAttributes | ActivityTaskFailedAttributes | ChildWorkflowExecutionStartedAttributes | ChildWorkflowExecutionCompletedAttributes | ChildWorkflowExecutionFailedAttributes | TimerStartedAttributes | TimerFiredAttributes | WorkflowExecutionSignaledAttributes;
|
|
163
|
+
export interface WorkflowExecutionEvent {
|
|
164
|
+
event_id: number;
|
|
165
|
+
event_type: WorkflowEventType;
|
|
166
|
+
category: WorkflowEventCategory;
|
|
167
|
+
event_time: string;
|
|
168
|
+
duration_ms: number | null;
|
|
169
|
+
is_system: boolean;
|
|
170
|
+
attributes: WorkflowEventAttributes;
|
|
171
|
+
}
|
|
172
|
+
export interface WorkflowExecutionSummary {
|
|
173
|
+
total_events: number;
|
|
174
|
+
activities: {
|
|
175
|
+
total: number;
|
|
176
|
+
completed: number;
|
|
177
|
+
failed: number;
|
|
178
|
+
system: number;
|
|
179
|
+
user: number;
|
|
180
|
+
};
|
|
181
|
+
child_workflows: {
|
|
182
|
+
total: number;
|
|
183
|
+
completed: number;
|
|
184
|
+
failed: number;
|
|
185
|
+
};
|
|
186
|
+
timers: number;
|
|
187
|
+
signals: number;
|
|
188
|
+
}
|
|
189
|
+
export type WorkflowExecutionStatus = 'running' | 'completed' | 'failed';
|
|
190
|
+
export interface WorkflowExecution {
|
|
191
|
+
workflow_id: string;
|
|
192
|
+
workflow_type: string;
|
|
193
|
+
task_queue: string;
|
|
194
|
+
status: WorkflowExecutionStatus;
|
|
195
|
+
start_time: string | null;
|
|
196
|
+
close_time: string | null;
|
|
197
|
+
duration_ms: number | null;
|
|
198
|
+
result: any;
|
|
199
|
+
events: WorkflowExecutionEvent[];
|
|
200
|
+
summary: WorkflowExecutionSummary;
|
|
201
|
+
children?: WorkflowExecution[];
|
|
202
|
+
}
|
|
203
|
+
export interface ExecutionExportOptions {
|
|
204
|
+
mode?: ExportMode;
|
|
205
|
+
exclude_system?: boolean;
|
|
206
|
+
omit_results?: boolean;
|
|
207
|
+
max_depth?: number;
|
|
208
|
+
}
|
package/build/types/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { CollationFaultType, CollationStage } from './collator';
|
|
|
6
6
|
export { ActivityConfig, ActivityInterceptor, ActivityInterceptorContext, ActivityWorkflowDataType, ChildResponseType, ClientConfig, ClientWorkflow, ContextType, Connection, ProxyResponseType, ProxyType, Registry, SignalOptions, FindJobsOptions, FindOptions, FindWhereOptions, FindWhereQuery, HookOptions, SearchResults, WorkflowConfig, WorkerConfig, WorkerOptions, WorkflowContext, WorkflowSearchOptions, WorkflowSearchSchema, WorkflowDataType, WorkflowOptions, WorkflowInterceptor, InterceptorRegistry, } from './durable';
|
|
7
7
|
export { PruneOptions, PruneResult, } from './dba';
|
|
8
8
|
export { DurableChildErrorType, DurableProxyErrorType, DurableSleepErrorType, DurableWaitForAllErrorType, DurableWaitForErrorType, } from './error';
|
|
9
|
-
export { ActivityAction, DependencyExport, DurableJobExport, ExportCycles, ExportItem, ExportOptions, ExportTransitions, JobAction, JobExport, JobActionExport, JobTimeline, } from './exporter';
|
|
9
|
+
export { ActivityAction, DependencyExport, DurableJobExport, ExecutionExportOptions, ExportCycles, ExportItem, ExportMode, ExportOptions, ExportTransitions, JobAction, JobExport, JobActionExport, JobTimeline, WorkflowEventAttributes, WorkflowEventCategory, WorkflowEventType, WorkflowExecution, WorkflowExecutionEvent, WorkflowExecutionStatus, WorkflowExecutionSummary, } from './exporter';
|
|
10
10
|
export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal, } from './hook';
|
|
11
11
|
export { HotMesh, HotMeshEngine, HotMeshWorker, HotMeshSettings, HotMeshApp, HotMeshApps, HotMeshConfig, HotMeshManifest, HotMeshGraph, KeyType, KeyStoreParams, ScoutType, } from './hotmesh';
|
|
12
12
|
export { ILogger, LogLevel } from './logger';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hotmeshio/hotmesh",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Permanent-Memory Workflows & AI Agents",
|
|
5
5
|
"main": "./build/index.js",
|
|
6
6
|
"types": "./build/index.d.ts",
|
|
@@ -46,6 +46,8 @@
|
|
|
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/exporter.test.ts",
|
|
50
|
+
"test:durable:exporter:debug": "EXPORT_DEBUG=1 HMSH_LOGLEVEL=error vitest run tests/durable/basic/postgres.test.ts",
|
|
49
51
|
"test:dba": "vitest run tests/dba",
|
|
50
52
|
"test:cycle": "vitest run tests/functional/cycle",
|
|
51
53
|
"test:functional": "vitest run tests/functional",
|