bunqueue-dashboard 0.0.35 → 0.0.37
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 +36 -16
- package/agent/backup/processExecutor.ts +90 -0
- package/agent/backup/resultValidation.ts +85 -0
- package/agent/backup/routes.ts +161 -0
- package/agent/backup/runner.ts +91 -0
- package/agent/backup/standaloneWorker.ts +23 -0
- package/agent/backup/workerExecutor.ts +40 -0
- package/agent/backup/workerFactory.ts +21 -0
- package/agent/backup/workerProtocol.ts +10 -0
- package/agent/db/core.ts +141 -0
- package/agent/db/csvExport.ts +172 -0
- package/agent/db/exportTimeout.ts +190 -0
- package/agent/db/query.ts +82 -0
- package/agent/db/queryTimeout.ts +76 -0
- package/agent/db/schema.ts +79 -0
- package/agent/db/tables.ts +157 -0
- package/agent/db/types.ts +118 -0
- package/agent/db/workerFactory.ts +25 -0
- package/agent/db.ts +43 -1099
- package/agent/errorMessage.ts +9 -0
- package/agent/flow/routes.ts +251 -0
- package/agent/flow/service.ts +266 -0
- package/agent/flow/types.ts +34 -0
- package/agent/flow/validation.ts +230 -0
- package/agent/index.ts +22 -21
- package/agent/logger.ts +121 -17
- package/agent/managedTarget.ts +82 -0
- package/agent/manager/config.ts +84 -0
- package/agent/manager/logs.ts +83 -0
- package/agent/manager/process.ts +233 -0
- package/agent/manager/storage.ts +28 -0
- package/agent/manager/types.ts +38 -0
- package/agent/manager.ts +10 -490
- package/agent/queue/routes.ts +135 -0
- package/agent/queue/runtime.ts +119 -0
- package/agent/queue/types.ts +34 -0
- package/agent/queue/validation.ts +100 -0
- package/agent/server/controlRoutes.ts +185 -0
- package/agent/server/controlTarget.ts +116 -0
- package/agent/server/databaseRoutes.ts +165 -0
- package/agent/server/errors.ts +49 -0
- package/agent/server/handler.ts +155 -0
- package/agent/server/jsonBody.ts +109 -0
- package/agent/server/lifecycle.ts +72 -0
- package/agent/server/managedRuntime.ts +41 -0
- package/agent/server/policy.ts +60 -0
- package/agent/server/router.ts +161 -0
- package/agent/server/types.ts +46 -0
- package/agent/server/workflowReadRoutes.ts +80 -0
- package/agent/server.ts +26 -441
- package/agent/shutdown.ts +156 -0
- package/agent/workflow/codec.ts +78 -0
- package/agent/workflow/detail.ts +63 -0
- package/agent/workflow/list.ts +87 -0
- package/agent/workflow/runtime.ts +274 -0
- package/agent/workflow/runtimeConfig.ts +103 -0
- package/agent/workflow/runtimeRoutes.ts +163 -0
- package/agent/workflow/sqlite.ts +52 -0
- package/agent/workflow/stats.ts +79 -0
- package/agent/workflow/types.ts +81 -0
- package/agent/workflow/validation.ts +114 -0
- package/agent/workflows.ts +14 -489
- package/dist/.vite/manifest.json +611 -250
- package/dist/assets/AddJob-CWkkVHmf.js +3 -0
- package/dist/assets/Alerts-Gh1pVbqj.js +1 -0
- package/dist/assets/{AreaChart-REK4ZsTC.js → AreaChart-w0ruozXH.js} +1 -1
- package/dist/assets/Benchmark-Dir2A1CQ.js +1 -0
- package/dist/assets/BulkAddJobs-Di_IgRSH.js +5 -0
- package/dist/assets/Button-BPqpRZJD.js +1 -0
- package/dist/assets/Card-C7NMhqUt.js +1 -0
- package/dist/assets/{CopilotPanel-COLw-atS.js → CopilotPanel-Db5nYpo1.js} +3 -3
- package/dist/assets/Cron-BRgWcBGk.js +1 -0
- package/dist/assets/CronManager-CbM0x_s0.js +1 -0
- package/dist/assets/Database-BeoEreW0.js +3 -0
- package/dist/assets/Diagnostics-Cw9iJ84f.js +1 -0
- package/dist/assets/Dlq-DfCg04Jd.js +1 -0
- package/dist/assets/DlqControl-BRCkUXvb.js +1 -0
- package/dist/assets/DlqPro-BHJwBYsN.js +1 -0
- package/dist/assets/Flows-BAzbFP_W.js +2 -0
- package/dist/assets/JobInspector-qvW_APO6.js +2 -0
- package/dist/assets/Jobs-URpnFjC6.js +1 -0
- package/dist/assets/JobsPro-pQ_RFSU3.js +1 -0
- package/dist/assets/Logs-C5jX2OPE.js +1 -0
- package/dist/assets/LogsPro-Cq_npmmS.js +1 -0
- package/dist/assets/{McpServer-CbaU52l2.js → McpServer-DtCTmQBu.js} +2 -2
- package/dist/assets/Metrics-BdxitePr.js +1 -0
- package/dist/assets/MetricsPro-orHfW0mg.js +1 -0
- package/dist/assets/Overview-DaNns1aj.js +1 -0
- package/dist/assets/PageHeader-BXlZSBcc.js +1 -0
- package/dist/assets/Pagination-Q-vf4hsX.js +1 -0
- package/dist/assets/QueueActions-jiTzndPH.js +1 -0
- package/dist/assets/QueueControl-l5bSdMgb.js +1 -0
- package/dist/assets/QueueDetail-URK2pjtH.js +1 -0
- package/dist/assets/QueueDetailPro-C9M5CV4K.js +1 -0
- package/dist/assets/Queues-DeLXlww2.js +1 -0
- package/dist/assets/QueuesOverview-DoQ-MUfT.js +1 -0
- package/dist/assets/S3Backup-D2agV9_z.js +1 -0
- package/dist/assets/S3BackupPro--WSZWTQn.js +3 -0
- package/dist/assets/ServerControl-QVd6m_iR.js +3 -0
- package/dist/assets/Settings-CiJRj_SM.js +1 -0
- package/dist/assets/StatusBadge-B0I7hNBX.js +1 -0
- package/dist/assets/Usage-DbHJgA4t.js +1 -0
- package/dist/assets/UsagePro-LoPo3k1L.js +1 -0
- package/dist/assets/Webhooks-CuzIJHwk.js +1 -0
- package/dist/assets/Workers-BpUgpwUH.js +1 -0
- package/dist/assets/WorkersPro-BYY0P1Pt.js +1 -0
- package/dist/assets/Workflows-ByJw4FFH.js +1 -0
- package/dist/assets/api-V_sVpHDo.js +1 -0
- package/dist/assets/bq-DYmbVkgR.js +1 -0
- package/dist/assets/cn-zFEt5SO8.js +1 -0
- package/dist/assets/connectionStore-CKFQrxDo.js +1 -0
- package/dist/assets/controlConnectionEpoch-JM4omZ6j.js +1 -0
- package/dist/assets/feedback-BnGE2oW2.js +1 -0
- package/dist/assets/{flowMutationSafety-CoOp94wS.js → flowMutationSafety-DObp9nJo.js} +1 -1
- package/dist/assets/{form-DsUq0w8s.js → form-tC20r4Ma.js} +1 -1
- package/dist/assets/format-Cg7VCIZW.js +1 -0
- package/dist/assets/icons-CIvFfU2b.js +1 -0
- package/dist/assets/index-D64v2vNO.js +8 -0
- package/dist/assets/index-DOze4TKg.css +1 -0
- package/dist/assets/install-jXi1SOYY.js +3 -0
- package/dist/assets/{react-vendor-Bjui4wS4.js → react-vendor-CyjgmGTq.js} +3 -3
- package/dist/assets/responses-CBLyy1Ob.js +1 -0
- package/dist/assets/toastStore-B7n0zS0y.js +1 -0
- package/dist/assets/{upstreamPaths-D_LPsv7b.js → upstreamPaths-CDVsu6IM.js} +1 -1
- package/dist/assets/usePolledData-c53tx1jw.js +1 -0
- package/dist/assets/{useServerActionGuard-CItj3hvh.js → useServerActionGuard-OTv6_fU0.js} +1 -1
- package/dist/assets/useThroughputSeries-DWHQl7cM.js +1 -0
- package/dist/index.html +17 -6
- package/package.json +15 -4
- package/scripts/embedded.gen.ts +160 -134
- package/scripts/serve.ts +127 -484
- package/scripts/serveHandler.ts +237 -0
- package/scripts/servePolicy.ts +187 -0
- package/dist/assets/AddJob-CG28MSxU.js +0 -3
- package/dist/assets/Alerts-D9JojJrj.js +0 -1
- package/dist/assets/Benchmark-BVjUH9dG.js +0 -1
- package/dist/assets/BulkAddJobs-BHzUCMj_.js +0 -5
- package/dist/assets/Cron-DoEHDHwy.js +0 -1
- package/dist/assets/CronManager-vulcesqJ.js +0 -1
- package/dist/assets/Database-DlB_t04Z.js +0 -3
- package/dist/assets/Diagnostics-DddCUgeR.js +0 -1
- package/dist/assets/Dlq-DMKrsSMP.js +0 -1
- package/dist/assets/DlqControl-9m0Re_TH.js +0 -1
- package/dist/assets/DlqPro-Dmw-0wta.js +0 -1
- package/dist/assets/Flows-C3-9nXW4.js +0 -1
- package/dist/assets/JobInspector-DwIfABmI.js +0 -2
- package/dist/assets/Jobs-j8tTS3NC.js +0 -1
- package/dist/assets/JobsPro-CS__CJJl.js +0 -1
- package/dist/assets/Logs-BB-VDPA1.js +0 -1
- package/dist/assets/LogsPro-RNtSQZRT.js +0 -1
- package/dist/assets/Metrics-ivcddPwT.js +0 -1
- package/dist/assets/MetricsPro-xjZno4vg.js +0 -1
- package/dist/assets/Overview-D84aD2ph.js +0 -1
- package/dist/assets/Pagination-Bfi4vkpp.js +0 -1
- package/dist/assets/QueueActions-DTbD6uek.js +0 -1
- package/dist/assets/QueueControl-B5xK_SQ-.js +0 -1
- package/dist/assets/QueueDetail-BH59kwm9.js +0 -1
- package/dist/assets/QueueDetailPro-DoN25ndy.js +0 -1
- package/dist/assets/Queues-CKMJqFHX.js +0 -1
- package/dist/assets/QueuesOverview-BX1lzojl.js +0 -1
- package/dist/assets/S3Backup-Bz3GfoZ9.js +0 -1
- package/dist/assets/S3BackupPro-BC_6-uX-.js +0 -2
- package/dist/assets/ServerControl-CSSrN8G5.js +0 -3
- package/dist/assets/Settings-DkqmZUIS.js +0 -1
- package/dist/assets/Usage-Ba20p0xd.js +0 -1
- package/dist/assets/UsagePro-C2pQwjN9.js +0 -1
- package/dist/assets/Webhooks-CVuuvb0L.js +0 -1
- package/dist/assets/Workers-C1wPU6CS.js +0 -1
- package/dist/assets/WorkersPro-B0vEkC0T.js +0 -1
- package/dist/assets/Workflows-C8Qmkflf.js +0 -1
- package/dist/assets/cn-CMFYpumL.js +0 -1
- package/dist/assets/connectionStore-Cd9-3Fw0.js +0 -1
- package/dist/assets/index-BCNzbhhD.css +0 -1
- package/dist/assets/index-CAtsVmyh.js +0 -8
- package/dist/assets/install-D0eRLf9F.js +0 -3
- package/dist/assets/useThroughputSeries-DjxGN8ob.js +0 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Convert any thrown JavaScript value without invoking a failure-prone boundary. */
|
|
2
|
+
export function safeErrorMessage(error: unknown): string {
|
|
3
|
+
try {
|
|
4
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5
|
+
return typeof message === 'string' && message.length > 0 ? message : 'Unknown error';
|
|
6
|
+
} catch {
|
|
7
|
+
return 'Unprintable error';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import type { ServerConfig } from '../manager';
|
|
2
|
+
import { assertManagedTarget, type ManagedTargetPolicy } from '../managedTarget';
|
|
3
|
+
import type {
|
|
4
|
+
ManagedRuntimeAdmission,
|
|
5
|
+
ManagedRuntimeSnapshot,
|
|
6
|
+
} from '../server/managedRuntime';
|
|
7
|
+
import { assertNoFlowBody, readFlowJsonBody } from './validation';
|
|
8
|
+
import {
|
|
9
|
+
createFlow,
|
|
10
|
+
inspectFlowJob,
|
|
11
|
+
mutateFlowJob,
|
|
12
|
+
readParentResults,
|
|
13
|
+
readFlow,
|
|
14
|
+
waitForFlowJob,
|
|
15
|
+
} from './service';
|
|
16
|
+
import type { FlowInspectOperation, FlowMutationOperation } from './types';
|
|
17
|
+
|
|
18
|
+
export interface FlowOperationsPort {
|
|
19
|
+
create: typeof createFlow;
|
|
20
|
+
inspect: typeof inspectFlowJob;
|
|
21
|
+
mutate: typeof mutateFlowJob;
|
|
22
|
+
parentResults: typeof readParentResults;
|
|
23
|
+
read: typeof readFlow;
|
|
24
|
+
wait: typeof waitForFlowJob;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface FlowRouteDependencies {
|
|
28
|
+
admission?: ManagedRuntimeAdmission;
|
|
29
|
+
operations?: FlowOperationsPort;
|
|
30
|
+
targetPolicy?: ManagedTargetPolicy;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_OPERATIONS: FlowOperationsPort = {
|
|
34
|
+
create: createFlow,
|
|
35
|
+
inspect: inspectFlowJob,
|
|
36
|
+
mutate: mutateFlowJob,
|
|
37
|
+
parentResults: readParentResults,
|
|
38
|
+
read: readFlow,
|
|
39
|
+
wait: waitForFlowJob,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface FlowRouteResponse {
|
|
43
|
+
status: number;
|
|
44
|
+
body: Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const INSPECTIONS = new Set<FlowInspectOperation>([
|
|
48
|
+
'getState',
|
|
49
|
+
'isWaiting',
|
|
50
|
+
'isActive',
|
|
51
|
+
'isDelayed',
|
|
52
|
+
'isCompleted',
|
|
53
|
+
'isFailed',
|
|
54
|
+
'isWaitingChildren',
|
|
55
|
+
'toJSON',
|
|
56
|
+
'asJSON',
|
|
57
|
+
'getChildrenValues',
|
|
58
|
+
'getDependencies',
|
|
59
|
+
'getDependenciesCount',
|
|
60
|
+
'getFailedChildrenValues',
|
|
61
|
+
'getIgnoredChildrenFailures',
|
|
62
|
+
]);
|
|
63
|
+
const MUTATIONS = new Set<FlowMutationOperation>([
|
|
64
|
+
'removeChildDependency',
|
|
65
|
+
'removeUnprocessedChildren',
|
|
66
|
+
'updateData',
|
|
67
|
+
'updateProgress',
|
|
68
|
+
'log',
|
|
69
|
+
'changeDelay',
|
|
70
|
+
'changePriority',
|
|
71
|
+
'clearLogs',
|
|
72
|
+
'removeDeduplicationKey',
|
|
73
|
+
'retry',
|
|
74
|
+
'promote',
|
|
75
|
+
'remove',
|
|
76
|
+
]);
|
|
77
|
+
const MUTATIONS_WITH_BODY = new Set<FlowMutationOperation>([
|
|
78
|
+
'updateData',
|
|
79
|
+
'updateProgress',
|
|
80
|
+
'log',
|
|
81
|
+
'changeDelay',
|
|
82
|
+
'changePriority',
|
|
83
|
+
'clearLogs',
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
export async function routeFlowRequest(
|
|
87
|
+
request: Request,
|
|
88
|
+
pathname: string,
|
|
89
|
+
method: string,
|
|
90
|
+
config: ServerConfig,
|
|
91
|
+
serverRunning = true,
|
|
92
|
+
dependencies: FlowRouteDependencies = {}
|
|
93
|
+
): Promise<FlowRouteResponse | null> {
|
|
94
|
+
if (isFlowRoute(pathname, method) && !serverRunning) {
|
|
95
|
+
throw new Error('Start the managed Bunqueue server before Flow operations.');
|
|
96
|
+
}
|
|
97
|
+
if (pathname === '/flows/create' && method === 'POST') {
|
|
98
|
+
const query = exactQuery(request.url, ['target']);
|
|
99
|
+
assertManagedTarget(query, config, dependencies.targetPolicy);
|
|
100
|
+
const body = await readFlowJsonBody(request);
|
|
101
|
+
return success(
|
|
102
|
+
await admitted(query, config, serverRunning, dependencies, ({ config: current }) =>
|
|
103
|
+
operations(dependencies).create(current, body)
|
|
104
|
+
)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (pathname === '/flows/tree' && method === 'GET') {
|
|
108
|
+
const query = exactQuery(request.url, ['id', 'queueName', 'depth', 'maxChildren', 'target']);
|
|
109
|
+
assertManagedTarget(query, config, dependencies.targetPolicy);
|
|
110
|
+
const target = {
|
|
111
|
+
id: required(query, 'id'),
|
|
112
|
+
queueName: required(query, 'queueName'),
|
|
113
|
+
depth: optionalInteger(query, 'depth'),
|
|
114
|
+
maxChildren: optionalInteger(query, 'maxChildren'),
|
|
115
|
+
};
|
|
116
|
+
return success(
|
|
117
|
+
await admitted(query, config, serverRunning, dependencies, ({ config: current }) =>
|
|
118
|
+
operations(dependencies).read(current, target)
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
if (pathname === '/flows/results' && method === 'POST') {
|
|
123
|
+
const query = exactQuery(request.url, ['target']);
|
|
124
|
+
assertManagedTarget(query, config, dependencies.targetPolicy);
|
|
125
|
+
const body = await readFlowJsonBody(request);
|
|
126
|
+
return success(
|
|
127
|
+
await admitted(query, config, serverRunning, dependencies, ({ config: current }) =>
|
|
128
|
+
operations(dependencies).parentResults(current, body)
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const match = pathname.match(/^\/flows\/jobs\/([^/]+)\/([^/]+)$/);
|
|
133
|
+
if (!match) return null;
|
|
134
|
+
const query = exactQuery(
|
|
135
|
+
request.url,
|
|
136
|
+
operationFromPath(pathname) === 'waitUntilFinished'
|
|
137
|
+
? ['queueName', 'target', 'ttl']
|
|
138
|
+
: ['queueName', 'target']
|
|
139
|
+
);
|
|
140
|
+
assertManagedTarget(query, config, dependencies.targetPolicy);
|
|
141
|
+
const target = { id: decodeURIComponent(match[1]), queueName: required(query, 'queueName') };
|
|
142
|
+
const operation = match[2];
|
|
143
|
+
if (method === 'GET' && operation === 'waitUntilFinished') {
|
|
144
|
+
const ttl = requiredInteger(query, 'ttl', 1, 60_000);
|
|
145
|
+
return success(
|
|
146
|
+
await admitted(query, config, serverRunning, dependencies, ({ config: current }) =>
|
|
147
|
+
operations(dependencies).wait(current, target, ttl)
|
|
148
|
+
)
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
if (method === 'GET' && INSPECTIONS.has(operation as FlowInspectOperation)) {
|
|
152
|
+
return success(
|
|
153
|
+
await admitted(query, config, serverRunning, dependencies, ({ config: current }) =>
|
|
154
|
+
operations(dependencies).inspect(current, target, operation as FlowInspectOperation)
|
|
155
|
+
)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (method === 'POST' && MUTATIONS.has(operation as FlowMutationOperation)) {
|
|
159
|
+
const mutation = operation as FlowMutationOperation;
|
|
160
|
+
const payload = MUTATIONS_WITH_BODY.has(mutation)
|
|
161
|
+
? await readFlowJsonBody(request)
|
|
162
|
+
: (assertNoFlowBody(request), {});
|
|
163
|
+
return success(
|
|
164
|
+
await admitted(query, config, serverRunning, dependencies, ({ config: current }) =>
|
|
165
|
+
operations(dependencies).mutate(current, target, mutation, payload)
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return { status: 404, body: { ok: false, error: 'Unknown flow operation' } };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function admitted<T>(
|
|
173
|
+
query: URLSearchParams,
|
|
174
|
+
config: ServerConfig,
|
|
175
|
+
running: boolean,
|
|
176
|
+
dependencies: FlowRouteDependencies,
|
|
177
|
+
operation: (snapshot: ManagedRuntimeSnapshot) => Promise<T>
|
|
178
|
+
): Promise<T> {
|
|
179
|
+
const execute = async (snapshot: ManagedRuntimeSnapshot) => {
|
|
180
|
+
assertManagedTarget(query, snapshot.config, dependencies.targetPolicy);
|
|
181
|
+
assertRunning(snapshot.running);
|
|
182
|
+
return operation(snapshot);
|
|
183
|
+
};
|
|
184
|
+
return dependencies.admission
|
|
185
|
+
? dependencies.admission(execute)
|
|
186
|
+
: execute({ config, running });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function operations(dependencies: FlowRouteDependencies): FlowOperationsPort {
|
|
190
|
+
return dependencies.operations ?? DEFAULT_OPERATIONS;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function assertRunning(running: boolean): void {
|
|
194
|
+
if (!running) throw new Error('Start the managed Bunqueue server before Flow operations.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isFlowRoute(pathname: string, method: string): boolean {
|
|
198
|
+
if (method === 'GET' && pathname === '/flows/tree') return true;
|
|
199
|
+
if (method === 'POST' && ['/flows/create', '/flows/results'].includes(pathname)) return true;
|
|
200
|
+
return (
|
|
201
|
+
(method === 'GET' || method === 'POST') &&
|
|
202
|
+
/^\/flows\/jobs\/[^/]+\/[^/]+$/.test(pathname)
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function operationFromPath(pathname: string): string {
|
|
207
|
+
return pathname.slice(pathname.lastIndexOf('/') + 1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function success(value: unknown): FlowRouteResponse {
|
|
211
|
+
return { status: 200, body: { ok: true, result: value } };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function exactQuery(url: string, allowed: string[]): URLSearchParams {
|
|
215
|
+
const query = new URL(url).searchParams;
|
|
216
|
+
for (const key of query.keys()) {
|
|
217
|
+
if (!allowed.includes(key)) throw new Error(`Unknown flow option: ${key}`);
|
|
218
|
+
if (query.getAll(key).length !== 1) throw new Error(`Duplicate flow option: ${key}`);
|
|
219
|
+
}
|
|
220
|
+
return query;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function required(query: URLSearchParams, key: string): string {
|
|
224
|
+
const value = query.get(key);
|
|
225
|
+
if (!value) throw new Error(`Flow ${key} is required`);
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function optionalInteger(query: URLSearchParams, key: string): number | undefined {
|
|
230
|
+
const raw = query.get(key);
|
|
231
|
+
if (raw === null) return undefined;
|
|
232
|
+
if (!/^\d+$/.test(raw)) throw new Error(`Flow ${key} must be a non-negative integer`);
|
|
233
|
+
const value = Number(raw);
|
|
234
|
+
if (!Number.isSafeInteger(value) || value > 500) throw new Error(`Flow ${key} must be at most 500`);
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function requiredInteger(
|
|
239
|
+
query: URLSearchParams,
|
|
240
|
+
key: string,
|
|
241
|
+
minimum: number,
|
|
242
|
+
maximum: number
|
|
243
|
+
): number {
|
|
244
|
+
const raw = required(query, key);
|
|
245
|
+
if (!/^\d+$/.test(raw)) throw new Error(`Flow ${key} must be an integer`);
|
|
246
|
+
const value = Number(raw);
|
|
247
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
248
|
+
throw new Error(`Flow ${key} must be from ${minimum} to ${maximum}`);
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FlowProducer,
|
|
3
|
+
TcpConnectionPool,
|
|
4
|
+
type FlowJob,
|
|
5
|
+
type FlowOpts,
|
|
6
|
+
type FlowStep,
|
|
7
|
+
type JobNode,
|
|
8
|
+
} from 'bunqueue/client';
|
|
9
|
+
import type { ServerConfig } from '../manager';
|
|
10
|
+
import { managedAuthToken } from '../managedTarget';
|
|
11
|
+
import {
|
|
12
|
+
asFlowRecord,
|
|
13
|
+
assertFlowBodySize,
|
|
14
|
+
assertExactFlowKeys,
|
|
15
|
+
normalizeFlowProgressPayload,
|
|
16
|
+
validateFlowTarget,
|
|
17
|
+
validateMutationPayload,
|
|
18
|
+
validateParentIds,
|
|
19
|
+
} from './validation';
|
|
20
|
+
import type { FlowInspectOperation, FlowJobTarget, FlowMutationOperation } from './types';
|
|
21
|
+
|
|
22
|
+
type CreateOperation =
|
|
23
|
+
| { operation: 'add'; flow: FlowJob; options?: FlowOpts }
|
|
24
|
+
| { operation: 'addBulk'; flows: FlowJob[] }
|
|
25
|
+
| { operation: 'addChain'; steps: FlowStep[] }
|
|
26
|
+
| { operation: 'addBulkThen'; parallel: FlowStep[]; final: FlowStep }
|
|
27
|
+
| { operation: 'addTree'; root: FlowStep };
|
|
28
|
+
|
|
29
|
+
type ParentResultRequest =
|
|
30
|
+
| { operation: 'getParentResult'; parentId: string }
|
|
31
|
+
| { operation: 'getParentResults'; parentIds: string[] };
|
|
32
|
+
|
|
33
|
+
export async function createFlow(config: ServerConfig, input: unknown): Promise<unknown> {
|
|
34
|
+
assertFlowBodySize(input);
|
|
35
|
+
const request = createRequest(input);
|
|
36
|
+
const producer = producerFor(config);
|
|
37
|
+
try {
|
|
38
|
+
switch (request.operation) {
|
|
39
|
+
case 'add':
|
|
40
|
+
return { operation: request.operation, root: await serializeNode(await producer.add(request.flow, request.options)) };
|
|
41
|
+
case 'addBulk':
|
|
42
|
+
return { operation: request.operation, roots: await Promise.all((await producer.addBulk(request.flows)).map(serializeNode)) };
|
|
43
|
+
case 'addChain':
|
|
44
|
+
return { operation: request.operation, ...(await producer.addChain(request.steps)) };
|
|
45
|
+
case 'addBulkThen':
|
|
46
|
+
return { operation: request.operation, ...(await producer.addBulkThen(request.parallel, request.final)) };
|
|
47
|
+
case 'addTree':
|
|
48
|
+
return { operation: request.operation, ...(await producer.addTree(request.root)) };
|
|
49
|
+
default:
|
|
50
|
+
throw new Error('Unknown FlowProducer create operation');
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
await producer.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function readFlow(
|
|
58
|
+
config: ServerConfig,
|
|
59
|
+
target: FlowJobTarget & { depth?: number; maxChildren?: number }
|
|
60
|
+
): Promise<unknown> {
|
|
61
|
+
validateFlowTarget(target);
|
|
62
|
+
const producer = producerFor(config);
|
|
63
|
+
try {
|
|
64
|
+
const node = await producer.getFlow(target);
|
|
65
|
+
return node ? { flow: await serializeNode(node) } : { flow: null };
|
|
66
|
+
} finally {
|
|
67
|
+
await producer.close();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function readParentResults(config: ServerConfig, input: unknown): Promise<unknown> {
|
|
72
|
+
assertFlowBodySize(input);
|
|
73
|
+
const request = parentResultRequest(input);
|
|
74
|
+
const producer = producerFor(config);
|
|
75
|
+
try {
|
|
76
|
+
if (request.operation === 'getParentResult') {
|
|
77
|
+
const value = await producer.getParentResult(request.parentId);
|
|
78
|
+
return { operation: request.operation, parentId: request.parentId, value };
|
|
79
|
+
}
|
|
80
|
+
const values = await producer.getParentResults(request.parentIds);
|
|
81
|
+
return { operation: request.operation, entries: Array.from(values.entries()) };
|
|
82
|
+
} finally {
|
|
83
|
+
await producer.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function inspectFlowJob(
|
|
88
|
+
config: ServerConfig,
|
|
89
|
+
target: FlowJobTarget,
|
|
90
|
+
operation: FlowInspectOperation
|
|
91
|
+
): Promise<unknown> {
|
|
92
|
+
const { producer, job } = await loadJob(config, target);
|
|
93
|
+
try {
|
|
94
|
+
switch (operation) {
|
|
95
|
+
case 'getState': return { state: await job.getState() };
|
|
96
|
+
case 'isWaiting': return { matches: await job.isWaiting() };
|
|
97
|
+
case 'isActive': return { matches: await job.isActive() };
|
|
98
|
+
case 'isDelayed': return { matches: await job.isDelayed() };
|
|
99
|
+
case 'isCompleted': return { matches: await job.isCompleted() };
|
|
100
|
+
case 'isFailed': return { matches: await job.isFailed() };
|
|
101
|
+
case 'isWaitingChildren': return { matches: await job.isWaitingChildren() };
|
|
102
|
+
case 'toJSON': return { job: job.toJSON() };
|
|
103
|
+
case 'asJSON': return { job: job.asJSON() };
|
|
104
|
+
case 'getChildrenValues': return { values: await job.getChildrenValues() };
|
|
105
|
+
case 'getDependencies': return { dependencies: await job.getDependencies() };
|
|
106
|
+
case 'getDependenciesCount': return { counts: await job.getDependenciesCount() };
|
|
107
|
+
case 'getFailedChildrenValues': return { values: await job.getFailedChildrenValues() };
|
|
108
|
+
case 'getIgnoredChildrenFailures': return { values: await job.getIgnoredChildrenFailures() };
|
|
109
|
+
default: throw new Error('Unknown flow inspection operation');
|
|
110
|
+
}
|
|
111
|
+
} finally {
|
|
112
|
+
await producer.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function mutateFlowJob(
|
|
117
|
+
config: ServerConfig,
|
|
118
|
+
target: FlowJobTarget,
|
|
119
|
+
operation: FlowMutationOperation,
|
|
120
|
+
input: unknown
|
|
121
|
+
): Promise<unknown> {
|
|
122
|
+
assertFlowBodySize(input);
|
|
123
|
+
const payload = asFlowRecord(input);
|
|
124
|
+
validateMutationPayload(operation, payload);
|
|
125
|
+
const { producer, job } = await loadJob(config, target);
|
|
126
|
+
try {
|
|
127
|
+
switch (operation) {
|
|
128
|
+
case 'removeChildDependency': return { removed: await job.removeChildDependency() };
|
|
129
|
+
case 'removeUnprocessedChildren': await job.removeUnprocessedChildren(); break;
|
|
130
|
+
case 'updateData': await job.updateData(payload.data); break;
|
|
131
|
+
case 'updateProgress': await updateProgress(config, target, payload); break;
|
|
132
|
+
case 'log': await job.log(payload.message as string); break;
|
|
133
|
+
case 'changeDelay': await job.changeDelay(payload.delay as number); break;
|
|
134
|
+
case 'changePriority': await job.changePriority({ priority: payload.priority as number, ...(payload.lifo === undefined ? {} : { lifo: payload.lifo as boolean }) }); break;
|
|
135
|
+
case 'clearLogs': await job.clearLogs(payload.keepLogs as number | undefined); break;
|
|
136
|
+
case 'removeDeduplicationKey': return { removed: await job.removeDeduplicationKey() };
|
|
137
|
+
case 'retry': await job.retry(); break;
|
|
138
|
+
case 'promote': await job.promote(); break;
|
|
139
|
+
case 'remove': await job.remove(); break;
|
|
140
|
+
default: throw new Error('Unknown flow mutation operation');
|
|
141
|
+
}
|
|
142
|
+
return { applied: true };
|
|
143
|
+
} finally {
|
|
144
|
+
await producer.close();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function waitForFlowJob(
|
|
149
|
+
config: ServerConfig,
|
|
150
|
+
target: FlowJobTarget,
|
|
151
|
+
ttl: number
|
|
152
|
+
): Promise<unknown> {
|
|
153
|
+
if (!Number.isSafeInteger(ttl) || ttl < 1 || ttl > 60_000) {
|
|
154
|
+
throw new Error('Flow wait TTL must be an integer from 1 to 60000 ms');
|
|
155
|
+
}
|
|
156
|
+
const { producer, job } = await loadJob(config, target, flowWaitTransportOptions(ttl));
|
|
157
|
+
try {
|
|
158
|
+
return { value: await job.waitUntilFinished(undefined, ttl) };
|
|
159
|
+
} finally {
|
|
160
|
+
await producer.close();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function flowWaitTransportOptions(ttl: number) {
|
|
165
|
+
return { commandTimeout: ttl + 5_000, poolSize: 1 as const };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function producerFor(
|
|
169
|
+
config: ServerConfig,
|
|
170
|
+
overrides?: { commandTimeout: number; poolSize: 1 }
|
|
171
|
+
): FlowProducer {
|
|
172
|
+
return new FlowProducer({ connection: { ...connectionFor(config), ...overrides } });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function connectionFor(config: ServerConfig) {
|
|
176
|
+
return { host: '127.0.0.1', port: config.tcpPort, token: managedAuthToken(config) };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function loadJob(
|
|
180
|
+
config: ServerConfig,
|
|
181
|
+
target: FlowJobTarget,
|
|
182
|
+
connectionOverrides?: { commandTimeout: number; poolSize: 1 }
|
|
183
|
+
) {
|
|
184
|
+
validateFlowTarget(target);
|
|
185
|
+
const producer = producerFor(config, connectionOverrides);
|
|
186
|
+
try {
|
|
187
|
+
const node = await producer.getFlow({ ...target, depth: 0, maxChildren: 0 });
|
|
188
|
+
if (!node) throw new Error(`Flow job ${target.id} was not found in queue ${target.queueName}`);
|
|
189
|
+
return { producer, job: node.job };
|
|
190
|
+
} catch (error) {
|
|
191
|
+
await producer.close();
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createRequest(input: unknown): CreateOperation {
|
|
197
|
+
const request = asFlowRecord(input);
|
|
198
|
+
switch (request.operation) {
|
|
199
|
+
case 'add':
|
|
200
|
+
assertExactFlowKeys(request, ['operation', 'flow', 'options'], 'create');
|
|
201
|
+
break;
|
|
202
|
+
case 'addBulk':
|
|
203
|
+
assertExactFlowKeys(request, ['operation', 'flows'], 'create');
|
|
204
|
+
break;
|
|
205
|
+
case 'addChain':
|
|
206
|
+
assertExactFlowKeys(request, ['operation', 'steps'], 'create');
|
|
207
|
+
break;
|
|
208
|
+
case 'addBulkThen':
|
|
209
|
+
assertExactFlowKeys(request, ['operation', 'parallel', 'final'], 'create');
|
|
210
|
+
break;
|
|
211
|
+
case 'addTree':
|
|
212
|
+
assertExactFlowKeys(request, ['operation', 'root'], 'create');
|
|
213
|
+
break;
|
|
214
|
+
default:
|
|
215
|
+
throw new Error('Unknown FlowProducer create operation');
|
|
216
|
+
}
|
|
217
|
+
return request as unknown as CreateOperation;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parentResultRequest(input: unknown): ParentResultRequest {
|
|
221
|
+
const request = asFlowRecord(input);
|
|
222
|
+
if (request.operation === 'getParentResult') {
|
|
223
|
+
assertExactFlowKeys(request, ['operation', 'parentId'], 'parent result');
|
|
224
|
+
validateParentIds([request.parentId]);
|
|
225
|
+
} else if (request.operation === 'getParentResults') {
|
|
226
|
+
assertExactFlowKeys(request, ['operation', 'parentIds'], 'parent result');
|
|
227
|
+
validateParentIds(request.parentIds);
|
|
228
|
+
} else {
|
|
229
|
+
throw new Error('Unknown FlowProducer parent result operation');
|
|
230
|
+
}
|
|
231
|
+
return request as unknown as ParentResultRequest;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function updateProgress(
|
|
235
|
+
config: ServerConfig,
|
|
236
|
+
target: FlowJobTarget,
|
|
237
|
+
payload: Record<string, unknown>
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const wire = normalizeFlowProgressPayload(payload);
|
|
240
|
+
const tcp = new TcpConnectionPool({ ...connectionFor(config), poolSize: 1 });
|
|
241
|
+
try {
|
|
242
|
+
const response = await tcp.send({
|
|
243
|
+
cmd: 'Progress',
|
|
244
|
+
id: target.id,
|
|
245
|
+
progress: wire.progress,
|
|
246
|
+
message: wire.message,
|
|
247
|
+
});
|
|
248
|
+
if (response.ok !== true) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
typeof response.error === 'string' ? response.error : 'Flow updateProgress failed'
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
tcp.close();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function serializeNode(node: JobNode): Promise<unknown> {
|
|
259
|
+
return {
|
|
260
|
+
id: String(node.job.id),
|
|
261
|
+
name: node.job.name,
|
|
262
|
+
queueName: node.job.queueName,
|
|
263
|
+
state: await node.job.getState(),
|
|
264
|
+
children: node.children ? await Promise.all(node.children.map(serializeNode)) : [],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type FlowInspectOperation =
|
|
2
|
+
| 'getState'
|
|
3
|
+
| 'isWaiting'
|
|
4
|
+
| 'isActive'
|
|
5
|
+
| 'isDelayed'
|
|
6
|
+
| 'isCompleted'
|
|
7
|
+
| 'isFailed'
|
|
8
|
+
| 'isWaitingChildren'
|
|
9
|
+
| 'toJSON'
|
|
10
|
+
| 'asJSON'
|
|
11
|
+
| 'getChildrenValues'
|
|
12
|
+
| 'getDependencies'
|
|
13
|
+
| 'getDependenciesCount'
|
|
14
|
+
| 'getFailedChildrenValues'
|
|
15
|
+
| 'getIgnoredChildrenFailures';
|
|
16
|
+
|
|
17
|
+
export type FlowMutationOperation =
|
|
18
|
+
| 'removeChildDependency'
|
|
19
|
+
| 'removeUnprocessedChildren'
|
|
20
|
+
| 'updateData'
|
|
21
|
+
| 'updateProgress'
|
|
22
|
+
| 'log'
|
|
23
|
+
| 'changeDelay'
|
|
24
|
+
| 'changePriority'
|
|
25
|
+
| 'clearLogs'
|
|
26
|
+
| 'removeDeduplicationKey'
|
|
27
|
+
| 'retry'
|
|
28
|
+
| 'promote'
|
|
29
|
+
| 'remove';
|
|
30
|
+
|
|
31
|
+
export interface FlowJobTarget {
|
|
32
|
+
id: string;
|
|
33
|
+
queueName: string;
|
|
34
|
+
}
|