bunqueue-dashboard 0.0.35 → 0.0.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/README.md +18 -11
  2. package/agent/backup/processExecutor.ts +90 -0
  3. package/agent/backup/resultValidation.ts +85 -0
  4. package/agent/backup/routes.ts +160 -0
  5. package/agent/backup/runner.ts +91 -0
  6. package/agent/backup/standaloneWorker.ts +23 -0
  7. package/agent/backup/workerExecutor.ts +40 -0
  8. package/agent/backup/workerFactory.ts +21 -0
  9. package/agent/backup/workerProtocol.ts +10 -0
  10. package/agent/db/core.ts +141 -0
  11. package/agent/db/csvExport.ts +172 -0
  12. package/agent/db/exportTimeout.ts +190 -0
  13. package/agent/db/query.ts +82 -0
  14. package/agent/db/queryTimeout.ts +76 -0
  15. package/agent/db/schema.ts +79 -0
  16. package/agent/db/tables.ts +157 -0
  17. package/agent/db/types.ts +118 -0
  18. package/agent/db/workerFactory.ts +25 -0
  19. package/agent/db.ts +43 -1099
  20. package/agent/errorMessage.ts +9 -0
  21. package/agent/flow/routes.ts +250 -0
  22. package/agent/flow/service.ts +266 -0
  23. package/agent/flow/types.ts +34 -0
  24. package/agent/flow/validation.ts +230 -0
  25. package/agent/index.ts +5 -17
  26. package/agent/logger.ts +121 -17
  27. package/agent/managedTarget.ts +43 -0
  28. package/agent/manager/config.ts +84 -0
  29. package/agent/manager/logs.ts +83 -0
  30. package/agent/manager/process.ts +233 -0
  31. package/agent/manager/storage.ts +28 -0
  32. package/agent/manager/types.ts +38 -0
  33. package/agent/manager.ts +10 -490
  34. package/agent/queue/routes.ts +128 -0
  35. package/agent/queue/runtime.ts +119 -0
  36. package/agent/queue/types.ts +34 -0
  37. package/agent/queue/validation.ts +100 -0
  38. package/agent/server/controlRoutes.ts +153 -0
  39. package/agent/server/databaseRoutes.ts +165 -0
  40. package/agent/server/errors.ts +48 -0
  41. package/agent/server/handler.ts +146 -0
  42. package/agent/server/jsonBody.ts +109 -0
  43. package/agent/server/lifecycle.ts +72 -0
  44. package/agent/server/managedRuntime.ts +41 -0
  45. package/agent/server/policy.ts +60 -0
  46. package/agent/server/router.ts +150 -0
  47. package/agent/server/types.ts +39 -0
  48. package/agent/server/workflowReadRoutes.ts +80 -0
  49. package/agent/server.ts +16 -441
  50. package/agent/shutdown.ts +156 -0
  51. package/agent/workflow/codec.ts +78 -0
  52. package/agent/workflow/detail.ts +63 -0
  53. package/agent/workflow/list.ts +87 -0
  54. package/agent/workflow/runtime.ts +274 -0
  55. package/agent/workflow/runtimeConfig.ts +103 -0
  56. package/agent/workflow/runtimeRoutes.ts +162 -0
  57. package/agent/workflow/sqlite.ts +52 -0
  58. package/agent/workflow/stats.ts +79 -0
  59. package/agent/workflow/types.ts +81 -0
  60. package/agent/workflow/validation.ts +114 -0
  61. package/agent/workflows.ts +14 -489
  62. package/dist/.vite/manifest.json +595 -234
  63. package/dist/assets/AddJob-UUa2fSXE.js +3 -0
  64. package/dist/assets/Alerts-7oGYfQMb.js +1 -0
  65. package/dist/assets/{AreaChart-REK4ZsTC.js → AreaChart-w0ruozXH.js} +1 -1
  66. package/dist/assets/Benchmark-C2NFp_kj.js +1 -0
  67. package/dist/assets/BulkAddJobs-CL4yBae-.js +5 -0
  68. package/dist/assets/Button-BPqpRZJD.js +1 -0
  69. package/dist/assets/Card-C7NMhqUt.js +1 -0
  70. package/dist/assets/{CopilotPanel-COLw-atS.js → CopilotPanel-BQ2Cgo9p.js} +3 -3
  71. package/dist/assets/Cron-DfWBhoBf.js +1 -0
  72. package/dist/assets/CronManager-DAK7RSOX.js +1 -0
  73. package/dist/assets/Database-DqL7giEj.js +3 -0
  74. package/dist/assets/Diagnostics-D98YwN-6.js +1 -0
  75. package/dist/assets/Dlq-BHmA5PTI.js +1 -0
  76. package/dist/assets/DlqControl-DihxvP-i.js +1 -0
  77. package/dist/assets/DlqPro-ncgnDtpE.js +1 -0
  78. package/dist/assets/Flows-B5N5Cil0.js +2 -0
  79. package/dist/assets/JobInspector-B40nFiGh.js +2 -0
  80. package/dist/assets/Jobs-Bla3IEef.js +1 -0
  81. package/dist/assets/JobsPro-CnczVx7x.js +1 -0
  82. package/dist/assets/Logs-T91Q3cH0.js +1 -0
  83. package/dist/assets/LogsPro-CMn6-Wwv.js +1 -0
  84. package/dist/assets/{McpServer-CbaU52l2.js → McpServer-BQBabwW-.js} +2 -2
  85. package/dist/assets/Metrics-DxT_RI2B.js +1 -0
  86. package/dist/assets/MetricsPro-Cjen-nAP.js +1 -0
  87. package/dist/assets/Overview-XN2ej8ph.js +1 -0
  88. package/dist/assets/PageHeader-BXlZSBcc.js +1 -0
  89. package/dist/assets/Pagination-Q-vf4hsX.js +1 -0
  90. package/dist/assets/QueueActions-C-Aferd6.js +1 -0
  91. package/dist/assets/QueueControl-B8Q9NX6t.js +1 -0
  92. package/dist/assets/QueueDetail-BMhG0jUB.js +1 -0
  93. package/dist/assets/QueueDetailPro-BKTTyLR4.js +1 -0
  94. package/dist/assets/Queues-COqPMoY9.js +1 -0
  95. package/dist/assets/QueuesOverview-pWdrPy8Z.js +1 -0
  96. package/dist/assets/S3Backup-BmsZ8xtq.js +1 -0
  97. package/dist/assets/S3BackupPro-AtlsPUJL.js +3 -0
  98. package/dist/assets/ServerControl-D3XwHr72.js +3 -0
  99. package/dist/assets/Settings-BofKZoHn.js +1 -0
  100. package/dist/assets/StatusBadge-B0I7hNBX.js +1 -0
  101. package/dist/assets/Usage-DAR9E22v.js +1 -0
  102. package/dist/assets/UsagePro-Bc30N2Lo.js +1 -0
  103. package/dist/assets/Webhooks-Bg2k1PU4.js +1 -0
  104. package/dist/assets/Workers-BqUZMJVQ.js +1 -0
  105. package/dist/assets/WorkersPro-C9aSadUh.js +1 -0
  106. package/dist/assets/Workflows-BEFMfHmo.js +1 -0
  107. package/dist/assets/api-CQ5Z7xR7.js +1 -0
  108. package/dist/assets/bq-BeBxa743.js +1 -0
  109. package/dist/assets/cn-zFEt5SO8.js +1 -0
  110. package/dist/assets/connectionStore-DbBYqHEY.js +1 -0
  111. package/dist/assets/controlConnectionEpoch-CFSO6237.js +1 -0
  112. package/dist/assets/feedback-BnGE2oW2.js +1 -0
  113. package/dist/assets/{form-DsUq0w8s.js → form-tC20r4Ma.js} +1 -1
  114. package/dist/assets/format-Cg7VCIZW.js +1 -0
  115. package/dist/assets/icons-CIvFfU2b.js +1 -0
  116. package/dist/assets/index-Cui9IwWy.js +8 -0
  117. package/dist/assets/index-DOze4TKg.css +1 -0
  118. package/dist/assets/install-Cd9Is10G.js +3 -0
  119. package/dist/assets/{react-vendor-Bjui4wS4.js → react-vendor-CyjgmGTq.js} +3 -3
  120. package/dist/assets/responses-kPO4rSu5.js +1 -0
  121. package/dist/assets/toastStore-CS_9Xndn.js +1 -0
  122. package/dist/assets/usePolledData-DzlUpBOO.js +1 -0
  123. package/dist/assets/{useServerActionGuard-CItj3hvh.js → useServerActionGuard-BlH-Kso1.js} +1 -1
  124. package/dist/assets/useThroughputSeries-C2pa1Wd_.js +1 -0
  125. package/dist/index.html +16 -5
  126. package/package.json +12 -3
  127. package/scripts/embedded.gen.ts +152 -126
  128. package/scripts/serve.ts +96 -484
  129. package/scripts/serveHandler.ts +217 -0
  130. package/scripts/servePolicy.ts +133 -0
  131. package/dist/assets/AddJob-CG28MSxU.js +0 -3
  132. package/dist/assets/Alerts-D9JojJrj.js +0 -1
  133. package/dist/assets/Benchmark-BVjUH9dG.js +0 -1
  134. package/dist/assets/BulkAddJobs-BHzUCMj_.js +0 -5
  135. package/dist/assets/Cron-DoEHDHwy.js +0 -1
  136. package/dist/assets/CronManager-vulcesqJ.js +0 -1
  137. package/dist/assets/Database-DlB_t04Z.js +0 -3
  138. package/dist/assets/Diagnostics-DddCUgeR.js +0 -1
  139. package/dist/assets/Dlq-DMKrsSMP.js +0 -1
  140. package/dist/assets/DlqControl-9m0Re_TH.js +0 -1
  141. package/dist/assets/DlqPro-Dmw-0wta.js +0 -1
  142. package/dist/assets/Flows-C3-9nXW4.js +0 -1
  143. package/dist/assets/JobInspector-DwIfABmI.js +0 -2
  144. package/dist/assets/Jobs-j8tTS3NC.js +0 -1
  145. package/dist/assets/JobsPro-CS__CJJl.js +0 -1
  146. package/dist/assets/Logs-BB-VDPA1.js +0 -1
  147. package/dist/assets/LogsPro-RNtSQZRT.js +0 -1
  148. package/dist/assets/Metrics-ivcddPwT.js +0 -1
  149. package/dist/assets/MetricsPro-xjZno4vg.js +0 -1
  150. package/dist/assets/Overview-D84aD2ph.js +0 -1
  151. package/dist/assets/Pagination-Bfi4vkpp.js +0 -1
  152. package/dist/assets/QueueActions-DTbD6uek.js +0 -1
  153. package/dist/assets/QueueControl-B5xK_SQ-.js +0 -1
  154. package/dist/assets/QueueDetail-BH59kwm9.js +0 -1
  155. package/dist/assets/QueueDetailPro-DoN25ndy.js +0 -1
  156. package/dist/assets/Queues-CKMJqFHX.js +0 -1
  157. package/dist/assets/QueuesOverview-BX1lzojl.js +0 -1
  158. package/dist/assets/S3Backup-Bz3GfoZ9.js +0 -1
  159. package/dist/assets/S3BackupPro-BC_6-uX-.js +0 -2
  160. package/dist/assets/ServerControl-CSSrN8G5.js +0 -3
  161. package/dist/assets/Settings-DkqmZUIS.js +0 -1
  162. package/dist/assets/Usage-Ba20p0xd.js +0 -1
  163. package/dist/assets/UsagePro-C2pQwjN9.js +0 -1
  164. package/dist/assets/Webhooks-CVuuvb0L.js +0 -1
  165. package/dist/assets/Workers-C1wPU6CS.js +0 -1
  166. package/dist/assets/WorkersPro-B0vEkC0T.js +0 -1
  167. package/dist/assets/Workflows-C8Qmkflf.js +0 -1
  168. package/dist/assets/cn-CMFYpumL.js +0 -1
  169. package/dist/assets/connectionStore-Cd9-3Fw0.js +0 -1
  170. package/dist/assets/index-BCNzbhhD.css +0 -1
  171. package/dist/assets/index-CAtsVmyh.js +0 -8
  172. package/dist/assets/install-D0eRLf9F.js +0 -3
  173. package/dist/assets/useThroughputSeries-DjxGN8ob.js +0 -1
@@ -1,491 +1,16 @@
1
1
  /**
2
- * Read-only observability adapter for Bunqueue 2.8.57's Workflow Engine store.
3
- *
4
- * Workflow control belongs to the live `Engine` instance because it owns the
5
- * registered handlers. The dashboard agent therefore reads persisted state but
6
- * never writes rows or pretends that a SQLite edit is equivalent to signal(),
7
- * recover(), resumeCompensation(), or abandonCompensation().
2
+ * Read-only facade for Bunqueue 2.8.57 Workflow Engine observability.
3
+ * Persistence, decoding and validation live behind this stable public module.
8
4
  */
9
- import { existsSync } from 'node:fs';
10
- import { Database } from 'bun:sqlite';
11
- import { Unpackr } from 'msgpackr';
12
- import { MissingDbError } from './db';
13
-
14
- export const WORKFLOW_STATES = [
15
- 'running',
16
- 'waiting',
17
- 'completed',
18
- 'failed',
19
- 'compensating',
20
- 'compensation-stuck',
21
- ] as const;
22
-
23
- export type WorkflowExecutionState = (typeof WORKFLOW_STATES)[number];
24
- export type WorkflowStateFilter = WorkflowExecutionState | 'compensation';
25
- export type WorkflowStoreKind = 'active' | 'archive';
26
-
27
- const MAX_PAGE = 100;
28
- const MAX_OFFSET = 1_000_000;
29
- const MAX_FILTER = 256;
30
- const MAX_DETAIL_BLOB = 4 * 1024 * 1024;
31
- const MAX_META_BLOB = 256 * 1024;
32
- const MAX_JSON_NODES = 25_000;
33
- const MAX_JSON_DEPTH = 40;
34
- const MAX_STRING = 250_000;
35
-
36
- const unpackr = new Unpackr({ structuredClone: true });
37
-
38
- interface WorkflowRowBase {
39
- id: string;
40
- workflow_name: string;
41
- state: string;
42
- current_node_index: number;
43
- created_at: number;
44
- updated_at: number;
45
- archived_at?: number | null;
46
- meta?: Uint8Array | null;
47
- }
48
-
49
- export interface WorkflowExecutionSummary {
50
- id: string;
51
- workflowName: string;
52
- state: WorkflowExecutionState;
53
- currentNodeIndex: number;
54
- createdAt: number;
55
- updatedAt: number;
56
- archivedAt?: number;
57
- rollbackStatus?: string;
58
- failureReason?: string;
59
- parentExecutionId?: string;
60
- definitionHash?: string;
61
- }
62
-
63
- export interface WorkflowExecutionDetail extends WorkflowExecutionSummary {
64
- input: unknown;
65
- steps: Record<string, unknown>;
66
- resolvedSteps?: string[];
67
- signals: Record<string, unknown>;
68
- decisions?: Record<string, unknown>;
69
- committedAt?: number;
70
- }
71
-
72
- export interface WorkflowStats {
73
- available: boolean;
74
- activeTotal: number;
75
- archiveTotal: number;
76
- states: Record<WorkflowExecutionState, number>;
77
- workflowNames: string[];
78
- }
79
-
80
- function open(path: string): Database {
81
- if (!existsSync(path)) {
82
- throw new MissingDbError(
83
- `Database not found at "${path}" — configure the Workflow Engine dataPath and start it once.`
84
- );
85
- }
86
- try {
87
- return new Database(path, { readonly: true });
88
- } catch (error) {
89
- throw new Error(`Could not open workflow database: ${(error as Error).message}`);
90
- }
91
- }
92
-
93
- function tableName(kind: WorkflowStoreKind): string {
94
- return kind === 'archive' ? 'workflow_executions_archive' : 'workflow_executions';
95
- }
96
-
97
- function columns(db: Database, table: string): Set<string> {
98
- return new Set(
99
- (db.query(`PRAGMA table_info("${table}")`).all() as { name: string }[]).map((row) => row.name)
100
- );
101
- }
102
-
103
- function hasTable(db: Database, table: string): boolean {
104
- return Boolean(
105
- db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table)
106
- );
107
- }
108
-
109
- const REQUIRED_COLUMNS = [
110
- 'id',
111
- 'workflow_name',
112
- 'state',
113
- 'input',
114
- 'steps',
115
- 'current_node_index',
116
- 'resolved_steps',
117
- 'signals',
118
- 'created_at',
119
- 'updated_at',
120
- ];
121
-
122
- function schema(db: Database, kind: WorkflowStoreKind): { table: string; hasMeta: boolean } | null {
123
- const table = tableName(kind);
124
- if (!hasTable(db, table)) return null;
125
- const found = columns(db, table);
126
- const missing = REQUIRED_COLUMNS.filter((name) => !found.has(name));
127
- if (kind === 'archive' && !found.has('archived_at')) missing.push('archived_at');
128
- if (missing.length > 0) {
129
- throw new Error(`Invalid ${table} schema: missing ${missing.join(', ')}`);
130
- }
131
- return { table, hasMeta: found.has('meta') };
132
- }
133
-
134
- function validState(value: string): value is WorkflowExecutionState {
135
- return (WORKFLOW_STATES as readonly string[]).includes(value);
136
- }
137
-
138
- function assertRow(row: WorkflowRowBase): WorkflowExecutionSummary {
139
- if (
140
- typeof row.id !== 'string' ||
141
- typeof row.workflow_name !== 'string' ||
142
- row.workflow_name.length < 1 ||
143
- row.workflow_name.length > MAX_FILTER ||
144
- !validState(row.state) ||
145
- !Number.isSafeInteger(row.current_node_index) ||
146
- !Number.isFinite(row.created_at) ||
147
- !Number.isFinite(row.updated_at)
148
- ) {
149
- throw new Error('Workflow store contains an invalid execution row');
150
- }
151
- const meta = decodeObject(row.meta, MAX_META_BLOB, 'meta');
152
- return {
153
- id: row.id,
154
- workflowName: row.workflow_name,
155
- state: row.state,
156
- currentNodeIndex: row.current_node_index,
157
- createdAt: row.created_at,
158
- updatedAt: row.updated_at,
159
- ...(typeof row.archived_at === 'number' ? { archivedAt: row.archived_at } : {}),
160
- ...(typeof meta.rollbackStatus === 'string' ? { rollbackStatus: meta.rollbackStatus } : {}),
161
- ...(typeof meta.failureReason === 'string' ? { failureReason: meta.failureReason } : {}),
162
- ...(typeof meta.parentExecutionId === 'string'
163
- ? { parentExecutionId: meta.parentExecutionId }
164
- : {}),
165
- ...(typeof meta.definitionHash === 'string' ? { definitionHash: meta.definitionHash } : {}),
166
- };
167
- }
168
-
169
- const STEP_STATES = new Set(['pending', 'running', 'completed', 'failed']);
170
- const COMPENSATION_STATES = new Set([
171
- 'compensated',
172
- 'compensation-failed',
173
- 'compensation-skipped',
174
- ]);
175
-
176
- function optionalFinite(record: Record<string, unknown>, key: string): boolean {
177
- return !Object.hasOwn(record, key) ||
178
- (typeof record[key] === 'number' && Number.isFinite(record[key]));
179
- }
180
-
181
- function optionalInteger(record: Record<string, unknown>, key: string): boolean {
182
- return !Object.hasOwn(record, key) ||
183
- (typeof record[key] === 'number' && Number.isSafeInteger(record[key]) && record[key] >= 0);
184
- }
185
-
186
- function assertSteps(value: unknown): Record<string, unknown> {
187
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
188
- throw new Error('Workflow steps must decode to an object');
189
- }
190
- for (const [name, candidate] of Object.entries(value)) {
191
- if (!name || candidate === null || typeof candidate !== 'object' || Array.isArray(candidate)) {
192
- throw new Error('Workflow store contains an invalid step record');
193
- }
194
- const step = candidate as Record<string, unknown>;
195
- if (
196
- typeof step.status !== 'string' ||
197
- !STEP_STATES.has(step.status) ||
198
- (Object.hasOwn(step, 'error') && typeof step.error !== 'string') ||
199
- (Object.hasOwn(step, 'compensatable') && typeof step.compensatable !== 'boolean') ||
200
- (Object.hasOwn(step, 'idempotencyKey') && typeof step.idempotencyKey !== 'string') ||
201
- (Object.hasOwn(step, 'childExecutionId') && typeof step.childExecutionId !== 'string') ||
202
- !optionalFinite(step, 'startedAt') ||
203
- !optionalFinite(step, 'completedAt') ||
204
- !optionalInteger(step, 'attempts') ||
205
- !optionalInteger(step, 'loopIndex') ||
206
- !optionalInteger(step, 'occurrence')
207
- ) {
208
- throw new Error('Workflow store contains an invalid step record');
209
- }
210
- if (Object.hasOwn(step, 'compensation')) {
211
- const candidateCompensation = step.compensation;
212
- if (
213
- candidateCompensation === null ||
214
- typeof candidateCompensation !== 'object' ||
215
- Array.isArray(candidateCompensation)
216
- ) {
217
- throw new Error('Workflow store contains an invalid compensation record');
218
- }
219
- const compensation = candidateCompensation as Record<string, unknown>;
220
- if (
221
- typeof compensation.status !== 'string' ||
222
- !COMPENSATION_STATES.has(compensation.status) ||
223
- typeof compensation.at !== 'number' ||
224
- !Number.isFinite(compensation.at) ||
225
- (Object.hasOwn(compensation, 'error') && typeof compensation.error !== 'string')
226
- ) {
227
- throw new Error('Workflow store contains an invalid compensation record');
228
- }
229
- }
230
- }
231
- return value as Record<string, unknown>;
232
- }
233
-
234
- function decode(blob: Uint8Array | null | undefined, cap: number, label: string): unknown {
235
- if (!blob) return null;
236
- if (!(blob instanceof Uint8Array)) throw new Error(`Workflow ${label} is not a BLOB`);
237
- if (blob.byteLength > cap) {
238
- throw new Error(`Workflow ${label} exceeds the ${cap}-byte inspection limit`);
239
- }
240
- try {
241
- return unpackr.unpack(blob);
242
- } catch (error) {
243
- throw new Error(`Could not decode workflow ${label}: ${(error as Error).message}`);
244
- }
245
- }
246
-
247
- function decodeObject(
248
- blob: Uint8Array | null | undefined,
249
- cap: number,
250
- label: string
251
- ): Record<string, unknown> {
252
- const value = decode(blob, cap, label);
253
- if (value == null) return {};
254
- if (typeof value !== 'object' || Array.isArray(value)) {
255
- throw new Error(`Workflow ${label} must decode to an object`);
256
- }
257
- return value as Record<string, unknown>;
258
- }
259
-
260
- /** Convert decoded MessagePack extensions to a bounded, JSON-safe value. */
261
- function jsonSafe(value: unknown): unknown {
262
- let nodes = 0;
263
- const seen = new WeakSet<object>();
264
- const visit = (entry: unknown, depth: number): unknown => {
265
- nodes++;
266
- if (nodes > MAX_JSON_NODES) throw new Error('Workflow detail exceeds the inspection node limit');
267
- if (depth > MAX_JSON_DEPTH) throw new Error('Workflow detail exceeds the inspection depth limit');
268
- if (entry == null || typeof entry === 'boolean' || typeof entry === 'number') return entry;
269
- if (typeof entry === 'string') {
270
- if (entry.length > MAX_STRING) throw new Error('Workflow detail contains an oversized string');
271
- return entry;
272
- }
273
- if (typeof entry === 'bigint') return entry.toString();
274
- if (typeof entry === 'undefined') return null;
275
- if (typeof entry !== 'object') return String(entry);
276
- if (seen.has(entry)) throw new Error('Workflow detail contains a cyclic value');
277
- seen.add(entry);
278
- try {
279
- if (entry instanceof Date) return entry.toISOString();
280
- if (entry instanceof Uint8Array) return `<binary ${entry.byteLength} B>`;
281
- if (entry instanceof Map) {
282
- const out = Object.create(null) as Record<string, unknown>;
283
- for (const [key, item] of entry) out[String(key)] = visit(item, depth + 1);
284
- return out;
285
- }
286
- if (entry instanceof Set) return [...entry].map((item) => visit(item, depth + 1));
287
- if (Array.isArray(entry)) return entry.map((item) => visit(item, depth + 1));
288
- const out = Object.create(null) as Record<string, unknown>;
289
- for (const [key, item] of Object.entries(entry)) out[key] = visit(item, depth + 1);
290
- return out;
291
- } finally {
292
- seen.delete(entry);
293
- }
294
- };
295
- return visit(value, 0);
296
- }
297
-
298
- export function workflowStats(
299
- path: string,
300
- /** @internal Deterministic concurrency seam used by the SQLite snapshot regression test. */
301
- afterActiveTotals?: () => void
302
- ): WorkflowStats {
303
- const db = open(path);
304
- try {
305
- const active = schema(db, 'active');
306
- const archive = schema(db, 'archive');
307
- const states = Object.fromEntries(WORKFLOW_STATES.map((state) => [state, 0])) as Record<
308
- WorkflowExecutionState,
309
- number
310
- >;
311
- if (!active) {
312
- return { available: false, activeTotal: 0, archiveTotal: 0, states, workflowNames: [] };
313
- }
314
- const snapshot = db.transaction((): WorkflowStats => {
315
- const stateColumns = WORKFLOW_STATES.map(
316
- (state, index) => `SUM(CASE WHEN state = '${state}' THEN 1 ELSE 0 END) AS state_${index}`
317
- ).join(', ');
318
- const totals = db
319
- .query(`SELECT COUNT(*) AS total, ${stateColumns} FROM workflow_executions`)
320
- .get() as { total: number } & Record<`state_${number}`, number | null>;
321
- for (const [index, state] of WORKFLOW_STATES.entries()) {
322
- states[state] = totals[`state_${index}`] ?? 0;
323
- }
324
- const recognizedTotal = Object.values(states).reduce((sum, count) => sum + count, 0);
325
- if (recognizedTotal !== totals.total) {
326
- throw new Error('Workflow store contains an unknown execution state');
327
- }
328
- afterActiveTotals?.();
329
- const names = db
330
- .query(
331
- archive
332
- ? `SELECT workflow_name FROM (
333
- SELECT workflow_name FROM workflow_executions
334
- UNION
335
- SELECT workflow_name FROM workflow_executions_archive
336
- ) ORDER BY workflow_name LIMIT 500`
337
- : 'SELECT DISTINCT workflow_name FROM workflow_executions ORDER BY workflow_name LIMIT 500'
338
- )
339
- .all() as { workflow_name: unknown }[];
340
- const workflowNames = names.map((row) => {
341
- if (
342
- typeof row.workflow_name !== 'string' ||
343
- row.workflow_name.length < 1 ||
344
- row.workflow_name.length > MAX_FILTER
345
- ) {
346
- throw new Error(`Workflow store contains an invalid workflow name`);
347
- }
348
- return row.workflow_name;
349
- });
350
- const archiveTotal = archive
351
- ? ((db.query('SELECT COUNT(*) AS count FROM workflow_executions_archive').get() as {
352
- count: number;
353
- }).count ?? 0)
354
- : 0;
355
- return {
356
- available: true,
357
- activeTotal: totals.total,
358
- archiveTotal,
359
- states,
360
- workflowNames,
361
- };
362
- });
363
- return snapshot.deferred();
364
- } finally {
365
- db.close();
366
- }
367
- }
368
-
369
- export function workflowExecutions(
370
- path: string,
371
- options: {
372
- kind?: WorkflowStoreKind;
373
- workflowName?: string;
374
- state?: WorkflowStateFilter;
375
- limit?: number;
376
- offset?: number;
377
- } = {},
378
- /** @internal Deterministic concurrency seam used by the SQLite snapshot regression test. */
379
- afterRowsRead?: () => void
380
- ): { available: boolean; executions: WorkflowExecutionSummary[]; total: number; limit: number; offset: number } {
381
- const kind = options.kind ?? 'active';
382
- const limit = options.limit ?? 50;
383
- const offset = options.offset ?? 0;
384
- if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PAGE) {
385
- throw new Error(`Workflow limit must be an integer between 1 and ${MAX_PAGE}`);
386
- }
387
- if (!Number.isSafeInteger(offset) || offset < 0 || offset > MAX_OFFSET) {
388
- throw new Error(`Workflow offset must be an integer between 0 and ${MAX_OFFSET}`);
389
- }
390
- if (options.workflowName !== undefined && (!options.workflowName || options.workflowName.length > MAX_FILTER)) {
391
- throw new Error(`Workflow name must contain 1–${MAX_FILTER} characters`);
392
- }
393
- if (options.state !== undefined && options.state !== 'compensation' && !validState(options.state)) {
394
- throw new Error('Unknown workflow execution state');
395
- }
396
-
397
- const db = open(path);
398
- try {
399
- const info = schema(db, kind);
400
- if (!info) return { available: false, executions: [], total: 0, limit, offset };
401
- const where: string[] = [];
402
- const bindings: (string | number)[] = [];
403
- if (options.workflowName) {
404
- where.push('workflow_name = ?');
405
- bindings.push(options.workflowName);
406
- }
407
- if (options.state) {
408
- if (options.state === 'compensation') {
409
- where.push("state IN ('compensating', 'compensation-stuck')");
410
- } else {
411
- where.push('state = ?');
412
- bindings.push(options.state);
413
- }
414
- }
415
- const predicate = where.length ? ` WHERE ${where.join(' AND ')}` : '';
416
- const archiveColumn = kind === 'archive' ? ', archived_at' : '';
417
- const metaColumn = info.hasMeta ? ', meta' : '';
418
- db.run('BEGIN');
419
- let transactionOpen = true;
420
- try {
421
- const rows = db
422
- .query(
423
- `SELECT id, workflow_name, state, current_node_index, created_at, updated_at${archiveColumn}${metaColumn}
424
- FROM ${info.table}${predicate} ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?`
425
- )
426
- .all(...bindings, limit, offset) as WorkflowRowBase[];
427
- afterRowsRead?.();
428
- const count = db.query(`SELECT COUNT(*) AS count FROM ${info.table}${predicate}`).get(...bindings) as {
429
- count: number;
430
- };
431
- const executions = rows.map(assertRow);
432
- db.run('COMMIT');
433
- transactionOpen = false;
434
- return { available: true, executions, total: count.count, limit, offset };
435
- } finally {
436
- if (transactionOpen) {
437
- try {
438
- db.run('ROLLBACK');
439
- } catch {
440
- // Preserve the original query/decoding error if SQLite already closed the transaction.
441
- }
442
- }
443
- }
444
- } finally {
445
- db.close();
446
- }
447
- }
448
-
449
- export function workflowExecution(
450
- path: string,
451
- id: string,
452
- kind: WorkflowStoreKind = 'active'
453
- ): WorkflowExecutionDetail | null {
454
- if (!id || id.length > 1024) throw new Error('Workflow execution id must contain 1–1024 characters');
455
- const db = open(path);
456
- try {
457
- const info = schema(db, kind);
458
- if (!info) return null;
459
- const row = db.query(`SELECT * FROM ${info.table} WHERE id = ?`).get(id) as
460
- | (WorkflowRowBase & {
461
- input: Uint8Array | null;
462
- steps: Uint8Array | null;
463
- resolved_steps: Uint8Array | null;
464
- signals: Uint8Array | null;
465
- })
466
- | null;
467
- if (!row) return null;
468
- const summary = assertRow(row);
469
- const input = jsonSafe(decode(row.input, MAX_DETAIL_BLOB, 'input'));
470
- const steps = assertSteps(jsonSafe(decodeObject(row.steps, MAX_DETAIL_BLOB, 'steps')));
471
- const resolved = jsonSafe(decode(row.resolved_steps, MAX_DETAIL_BLOB, 'resolved steps'));
472
- const signals = jsonSafe(decodeObject(row.signals, MAX_DETAIL_BLOB, 'signals'));
473
- const meta = decodeObject(row.meta, MAX_META_BLOB, 'meta');
474
- if (resolved != null && (!Array.isArray(resolved) || resolved.some((item) => typeof item !== 'string'))) {
475
- throw new Error('Workflow resolved steps must decode to a string array');
476
- }
477
- return {
478
- ...summary,
479
- input,
480
- steps,
481
- ...(resolved ? { resolvedSteps: resolved as string[] } : {}),
482
- signals: signals as Record<string, unknown>,
483
- ...(meta.decisions && typeof meta.decisions === 'object'
484
- ? { decisions: jsonSafe(meta.decisions) as Record<string, unknown> }
485
- : {}),
486
- ...(typeof meta.committedAt === 'number' ? { committedAt: meta.committedAt } : {}),
487
- };
488
- } finally {
489
- db.close();
490
- }
491
- }
5
+ export { workflowExecution } from './workflow/detail';
6
+ export { workflowExecutions } from './workflow/list';
7
+ export { workflowStats } from './workflow/stats';
8
+ export {
9
+ WORKFLOW_STATES,
10
+ type WorkflowExecutionDetail,
11
+ type WorkflowExecutionState,
12
+ type WorkflowExecutionSummary,
13
+ type WorkflowStateFilter,
14
+ type WorkflowStats,
15
+ type WorkflowStoreKind,
16
+ } from './workflow/types';