bunqueue-dashboard 0.0.38 → 0.0.39
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 +16 -8
- package/agent/backup/resultValidation.ts +1 -1
- package/agent/backup/routes.ts +63 -17
- package/agent/db/query.ts +2 -1
- package/agent/manager/config.ts +2 -1
- package/agent/manager/process.ts +51 -5
- package/agent/manager/storageMode.ts +21 -0
- package/agent/manager/types.ts +2 -0
- package/agent/manager.ts +1 -0
- package/agent/queue/routes.ts +1 -2
- package/agent/queue/runtime.ts +5 -0
- package/agent/queue/types.ts +1 -0
- package/agent/server/controlRoutes.ts +109 -8
- package/agent/server/databaseRoutes.ts +47 -14
- package/agent/server/errors.ts +1 -0
- package/agent/server/lifecycle.ts +25 -8
- package/agent/server/managedRuntime.ts +27 -0
- package/agent/server/router.ts +90 -40
- package/agent/server/workflowReadRoutes.ts +28 -11
- package/agent/workflows.ts +1 -1
- package/dist/.vite/manifest.json +490 -476
- package/dist/assets/AddJob-d5KBvmN8.js +3 -0
- package/dist/assets/{Alerts-C2bAi-Kj.js → Alerts-5I44QTnQ.js} +1 -1
- package/dist/assets/{AreaChart-w0ruozXH.js → AreaChart-DEAalJ6P.js} +1 -1
- package/dist/assets/Benchmark-S1afsQRd.js +1 -0
- package/dist/assets/{BulkAddJobs-BBWqQpP7.js → BulkAddJobs-BO8LWUHE.js} +1 -1
- package/dist/assets/{Button-BPqpRZJD.js → Button-Ch1W94fJ.js} +1 -1
- package/dist/assets/{Card-C7NMhqUt.js → Card-CWxrC8OY.js} +1 -1
- package/dist/assets/{CopilotPanel-Bhh-uCpN.js → CopilotPanel-DDkRd35p.js} +2 -2
- package/dist/assets/{Cron-pJJ5KXFG.js → Cron-X3Y5uISO.js} +1 -1
- package/dist/assets/CronManager-ugmYEAwY.js +1 -0
- package/dist/assets/{Database-DxiXsUl4.js → Database-koy8haPX.js} +1 -1
- package/dist/assets/Diagnostics-D6oqvkOs.js +1 -0
- package/dist/assets/{Dlq-Bx4YXTp5.js → Dlq-D70Lzmsa.js} +1 -1
- package/dist/assets/{DlqControl-DJGhuyu3.js → DlqControl-nUT5Zzkl.js} +1 -1
- package/dist/assets/{DlqPro-BhXIKtrt.js → DlqPro-VMuBKgP-.js} +1 -1
- package/dist/assets/Flows-CAG0nDnt.js +2 -0
- package/dist/assets/JobInspector-CiS5HOSQ.js +2 -0
- package/dist/assets/{Jobs-Blavwd1H.js → Jobs-CkyRvzOT.js} +1 -1
- package/dist/assets/{JobsPro-D8dH9um7.js → JobsPro-B4-CSoq3.js} +1 -1
- package/dist/assets/{Logs-B3dsGSF8.js → Logs-DBTz2PTz.js} +1 -1
- package/dist/assets/{LogsPro-BCB7etG2.js → LogsPro-BCFeegvr.js} +1 -1
- package/dist/assets/{McpServer-Cz1-GwD5.js → McpServer-BKlTR1qr.js} +2 -2
- package/dist/assets/{Metrics-Bkuy6Ntb.js → Metrics-Du__9Lbn.js} +1 -1
- package/dist/assets/{MetricsPro-BbJmy0oO.js → MetricsPro-CqnaO6YX.js} +1 -1
- package/dist/assets/{Overview-Ba5odzYe.js → Overview-udSSt7ZB.js} +1 -1
- package/dist/assets/{PageHeader-BXlZSBcc.js → PageHeader-CvIyaFOO.js} +1 -1
- package/dist/assets/{Pagination-Q-vf4hsX.js → Pagination-x-Cwgrul.js} +1 -1
- package/dist/assets/QueueActions-D7cQrGvJ.js +1 -0
- package/dist/assets/{QueueControl-CWHsVfWU.js → QueueControl-BF638r_0.js} +1 -1
- package/dist/assets/QueueDetail-C-ms6bMe.js +1 -0
- package/dist/assets/QueueDetailPro-DXEdsZx_.js +1 -0
- package/dist/assets/{Queues-CGPgEkVm.js → Queues-BUMwTVAf.js} +1 -1
- package/dist/assets/{QueuesOverview-ChC5Dtqh.js → QueuesOverview-Du8XLgN0.js} +1 -1
- package/dist/assets/{S3Backup-5OKa8G13.js → S3Backup-CDICSzxM.js} +1 -1
- package/dist/assets/S3BackupPro-CBidbI-s.js +3 -0
- package/dist/assets/ServerControl-CPT0LpsC.js +3 -0
- package/dist/assets/{Settings-C1HtCK6h.js → Settings-CmxfSX_8.js} +1 -1
- package/dist/assets/{StatusBadge-B0I7hNBX.js → StatusBadge-BoOVTnKu.js} +1 -1
- package/dist/assets/{Usage-QyZkUhgs.js → Usage-B895tSxi.js} +1 -1
- package/dist/assets/{UsagePro-DMxrXxur.js → UsagePro-CASN77iV.js} +1 -1
- package/dist/assets/{Webhooks-D0JQQf0q.js → Webhooks-CXbZOFFw.js} +1 -1
- package/dist/assets/{Workers-Cn8Bkgy1.js → Workers-BceQlUhX.js} +1 -1
- package/dist/assets/WorkersPro-Dtml89jB.js +1 -0
- package/dist/assets/Workflows-B8wmQN2X.js +1 -0
- package/dist/assets/{api-V_sVpHDo.js → api-81m1TwB7.js} +1 -1
- package/dist/assets/bq-BOfwNIlJ.js +1 -0
- package/dist/assets/{connectionStore-CKFQrxDo.js → connectionStore-CFuNYZ_8.js} +1 -1
- package/dist/assets/{controlConnectionEpoch-JM4omZ6j.js → controlConnectionEpoch-DzbuWOhe.js} +1 -1
- package/dist/assets/{feedback-BnGE2oW2.js → feedback-QDN1AsYt.js} +1 -1
- package/dist/assets/flowMutationSafety-6cP-l4Pa.js +1 -0
- package/dist/assets/{form-tC20r4Ma.js → form-C4oby9pI.js} +1 -1
- package/dist/assets/{icons-CIvFfU2b.js → icons-BVzwsfbG.js} +1 -1
- package/dist/assets/index-B-QjN7pA.css +1 -0
- package/dist/assets/{index-Cue53Myw.js → index-TploGF7u.js} +3 -3
- package/dist/assets/install-BPp8nydm.js +3 -0
- package/dist/assets/{react-vendor-CyjgmGTq.js → react-vendor-CvwrP0tU.js} +2 -2
- package/dist/assets/{responses-CBLyy1Ob.js → responses-C9lP49qu.js} +1 -1
- package/dist/assets/{toastStore-B7n0zS0y.js → toastStore-B31o6Z5J.js} +1 -1
- package/dist/assets/{upstreamPaths-CDVsu6IM.js → upstreamPaths-ZeijhPPb.js} +1 -1
- package/dist/assets/useControlActionGuard-DT7uudrj.js +1 -0
- package/dist/assets/{usePolledData-g80Z_UB-.js → usePolledData-CNtNtd0a.js} +1 -1
- package/dist/assets/useServerActionGuard-Db7PPgaI.js +1 -0
- package/dist/assets/{useThroughputSeries-CrsZYKbT.js → useThroughputSeries-_J7gWbcI.js} +1 -1
- package/dist/index.html +21 -18
- package/package.json +9 -4
- package/scripts/embedded.gen.ts +152 -150
- package/scripts/serveHandler.ts +22 -1
- package/scripts/servePolicy.ts +12 -3
- package/dist/assets/AddJob-CC4bfHLH.js +0 -3
- package/dist/assets/Benchmark-ClJmYZoC.js +0 -1
- package/dist/assets/CronManager-lNZ4lHh1.js +0 -1
- package/dist/assets/Diagnostics-DLvCT7zI.js +0 -1
- package/dist/assets/Flows-Bb9j-YhZ.js +0 -2
- package/dist/assets/JobInspector-DfqBq4O5.js +0 -2
- package/dist/assets/QueueActions-B6M0ByGt.js +0 -1
- package/dist/assets/QueueDetail-BClRbWC5.js +0 -1
- package/dist/assets/QueueDetailPro-CIimG0pX.js +0 -1
- package/dist/assets/S3BackupPro-BkwTXdtw.js +0 -3
- package/dist/assets/ServerControl-DJHKMaf9.js +0 -3
- package/dist/assets/WorkersPro-BOTuX9EP.js +0 -1
- package/dist/assets/Workflows-DqTKWPmT.js +0 -1
- package/dist/assets/bq-Bmysbg0r.js +0 -1
- package/dist/assets/flowMutationSafety-DObp9nJo.js +0 -1
- package/dist/assets/index-DOze4TKg.css +0 -1
- package/dist/assets/install-CFiYX86d.js +0 -3
- package/dist/assets/useServerActionGuard-OTv6_fU0.js +0 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '../db';
|
|
11
11
|
import { corsHeaders } from './policy';
|
|
12
12
|
import { readLimitedJsonBody } from './jsonBody';
|
|
13
|
+
import type { ManagedDatabaseAdmission } from './managedRuntime';
|
|
13
14
|
import type { RouteResponse } from './types';
|
|
14
15
|
|
|
15
16
|
const MAX_QUERY_BODY_BYTES = 64 * 1024;
|
|
@@ -100,46 +101,58 @@ export async function routeDatabaseRequest(
|
|
|
100
101
|
method: string,
|
|
101
102
|
dataPath: string,
|
|
102
103
|
origin: string | null,
|
|
103
|
-
allowedOrigins: string[]
|
|
104
|
+
allowedOrigins: string[],
|
|
105
|
+
admission?: ManagedDatabaseAdmission
|
|
104
106
|
): Promise<RouteResponse | Response | null> {
|
|
105
107
|
if (pathname === '/db/info' && method === 'GET') {
|
|
106
|
-
return
|
|
108
|
+
return withDatabase(dataPath, admission, (path) => ({
|
|
109
|
+
status: 200,
|
|
110
|
+
body: { ok: true, ...dbInfo(path) },
|
|
111
|
+
}));
|
|
107
112
|
}
|
|
108
113
|
if (pathname === '/db/tables' && method === 'GET') {
|
|
109
|
-
return
|
|
114
|
+
return withDatabase(dataPath, admission, (path) => ({
|
|
115
|
+
status: 200,
|
|
116
|
+
body: { ok: true, tables: dbTables(path) },
|
|
117
|
+
}));
|
|
110
118
|
}
|
|
111
119
|
if (pathname.startsWith('/db/tables/') && method === 'GET') {
|
|
112
120
|
const segments = pathname.slice('/db/tables/'.length).split('/');
|
|
113
121
|
const table = decodeURIComponent(segments[0] ?? '');
|
|
114
122
|
const subresource = segments[1];
|
|
115
123
|
if (segments.length === 2 && subresource === 'export') {
|
|
116
|
-
return
|
|
124
|
+
return withDatabase(dataPath, admission, (path) =>
|
|
125
|
+
exportTable(request, table, path, origin, allowedOrigins)
|
|
126
|
+
);
|
|
117
127
|
}
|
|
118
128
|
if (segments.length === 2 && subresource === 'schema') {
|
|
119
|
-
return
|
|
129
|
+
return withDatabase(dataPath, admission, (path) => ({
|
|
130
|
+
status: 200,
|
|
131
|
+
body: { ok: true, ...dbSchema(path, table) },
|
|
132
|
+
}));
|
|
120
133
|
}
|
|
121
134
|
if (segments.length === 2 && subresource === 'cell') {
|
|
122
135
|
const query = new URL(request.url).searchParams;
|
|
123
136
|
const rowid = query.get('rowid');
|
|
124
137
|
if (rowid === null) throw new Error('rowid is required');
|
|
125
|
-
return {
|
|
138
|
+
return withDatabase(dataPath, admission, (path) => ({
|
|
126
139
|
status: 200,
|
|
127
140
|
body: {
|
|
128
141
|
ok: true,
|
|
129
|
-
...dbCell(
|
|
142
|
+
...dbCell(path, table, rowid, query.get('column') ?? ''),
|
|
130
143
|
},
|
|
131
|
-
};
|
|
144
|
+
}));
|
|
132
145
|
}
|
|
133
146
|
if (segments.length !== 1) {
|
|
134
147
|
return { status: 404, body: { ok: false, error: 'Not found' } };
|
|
135
148
|
}
|
|
136
149
|
const query = new URL(request.url).searchParams;
|
|
137
|
-
return {
|
|
150
|
+
return withDatabase(dataPath, admission, (path) => ({
|
|
138
151
|
status: 200,
|
|
139
152
|
body: {
|
|
140
153
|
ok: true,
|
|
141
154
|
...dbRows(
|
|
142
|
-
|
|
155
|
+
path,
|
|
143
156
|
table,
|
|
144
157
|
Number(query.get('limit')) || 50,
|
|
145
158
|
Number(query.get('offset')) || 0,
|
|
@@ -148,7 +161,7 @@ export async function routeDatabaseRequest(
|
|
|
148
161
|
browseFilter(query)
|
|
149
162
|
),
|
|
150
163
|
},
|
|
151
|
-
};
|
|
164
|
+
}));
|
|
152
165
|
}
|
|
153
166
|
if (pathname === '/db/query' && method === 'POST') {
|
|
154
167
|
const { sql } = (await readLimitedJsonBody(request, {
|
|
@@ -156,10 +169,30 @@ export async function routeDatabaseRequest(
|
|
|
156
169
|
maxBytes: MAX_QUERY_BODY_BYTES,
|
|
157
170
|
limitLabel: '64 KiB',
|
|
158
171
|
})) as { sql?: string };
|
|
159
|
-
return {
|
|
172
|
+
return withDatabase(dataPath, admission, async (path) => ({
|
|
160
173
|
status: 200,
|
|
161
|
-
body: { ok: true, ...(await queryWithTimeout(
|
|
162
|
-
};
|
|
174
|
+
body: { ok: true, ...(await queryWithTimeout(path, sql ?? '')) },
|
|
175
|
+
}));
|
|
163
176
|
}
|
|
164
177
|
return null;
|
|
165
178
|
}
|
|
179
|
+
|
|
180
|
+
function withDatabase<T>(
|
|
181
|
+
dataPath: string,
|
|
182
|
+
admission: ManagedDatabaseAdmission | undefined,
|
|
183
|
+
operation: (path: string) => T | Promise<T>
|
|
184
|
+
): Promise<T | RouteResponse> {
|
|
185
|
+
return admission
|
|
186
|
+
? admission((snapshot) =>
|
|
187
|
+
snapshot.storageMode === 'sqlite'
|
|
188
|
+
? operation(snapshot.dataPath)
|
|
189
|
+
: {
|
|
190
|
+
status: 409,
|
|
191
|
+
body: {
|
|
192
|
+
ok: false,
|
|
193
|
+
error: `Database inspection requires Bunqueue SQLite storage; ${snapshot.storageMode} is active`,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
: Promise.resolve(operation(dataPath));
|
|
198
|
+
}
|
package/agent/server/errors.ts
CHANGED
|
@@ -19,6 +19,7 @@ const CONFLICT_MESSAGES = [
|
|
|
19
19
|
'BUNQUEUE_WORKFLOW_CONCURRENCY must',
|
|
20
20
|
'Managed Bunqueue server restarted',
|
|
21
21
|
'Server lifecycle is disabled in external mode',
|
|
22
|
+
'Configuration changed since revision',
|
|
22
23
|
];
|
|
23
24
|
|
|
24
25
|
export function errorStatus(error: unknown): number {
|
|
@@ -18,6 +18,7 @@ export class AgentLifecycleGate implements AgentLifecyclePort {
|
|
|
18
18
|
private leases = new Set<Promise<void>>();
|
|
19
19
|
private closed = false;
|
|
20
20
|
private closing: Promise<void> | null = null;
|
|
21
|
+
private pendingTransitions = 0;
|
|
21
22
|
|
|
22
23
|
assertOpen(): void {
|
|
23
24
|
if (this.closed) throw new AgentLifecycleClosedError();
|
|
@@ -29,14 +30,25 @@ export class AgentLifecycleGate implements AgentLifecyclePort {
|
|
|
29
30
|
} catch (error) {
|
|
30
31
|
return Promise.reject(error);
|
|
31
32
|
}
|
|
32
|
-
const result = this.barrier.then(operation);
|
|
33
33
|
const batch = this.leases;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
34
|
+
let complete!: () => void;
|
|
35
|
+
const completion = new Promise<void>((resolve) => {
|
|
36
|
+
complete = resolve;
|
|
37
|
+
});
|
|
38
|
+
// Publish the lease before invoking user code. Its synchronous prefix may
|
|
39
|
+
// re-enter run()/close(), which must observe this lease and wait for it.
|
|
38
40
|
batch.add(completion);
|
|
39
|
-
|
|
41
|
+
let result: Promise<T>;
|
|
42
|
+
if (this.pendingTransitions === 0) {
|
|
43
|
+
try {
|
|
44
|
+
result = Promise.resolve(operation());
|
|
45
|
+
} catch (error) {
|
|
46
|
+
result = Promise.reject(error);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
result = this.barrier.then(operation);
|
|
50
|
+
}
|
|
51
|
+
void result.then(complete, complete).then(() => batch.delete(completion));
|
|
40
52
|
return result;
|
|
41
53
|
}
|
|
42
54
|
|
|
@@ -60,12 +72,17 @@ export class AgentLifecycleGate implements AgentLifecyclePort {
|
|
|
60
72
|
const precedingBarrier = this.barrier;
|
|
61
73
|
const precedingLeases = this.leases;
|
|
62
74
|
this.leases = new Set();
|
|
75
|
+
this.pendingTransitions += 1;
|
|
63
76
|
const result = precedingBarrier
|
|
64
77
|
.then(() => Promise.all(precedingLeases))
|
|
65
78
|
.then(operation);
|
|
66
79
|
this.barrier = result.then(
|
|
67
|
-
() =>
|
|
68
|
-
|
|
80
|
+
() => {
|
|
81
|
+
this.pendingTransitions -= 1;
|
|
82
|
+
},
|
|
83
|
+
() => {
|
|
84
|
+
this.pendingTransitions -= 1;
|
|
85
|
+
}
|
|
69
86
|
);
|
|
70
87
|
return result;
|
|
71
88
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ProcessManager, ServerConfig, StatusSnapshot } from '../manager';
|
|
2
|
+
import { managedStorageMode } from '../manager';
|
|
3
|
+
import type { ManagedStorageMode } from '../manager/storageMode';
|
|
2
4
|
import type { AgentLifecyclePort } from './lifecycle';
|
|
3
5
|
|
|
4
6
|
export interface ManagedRuntimeSnapshot {
|
|
@@ -10,6 +12,15 @@ export type ManagedRuntimeAdmission = <T>(
|
|
|
10
12
|
operation: (snapshot: ManagedRuntimeSnapshot) => Promise<T>
|
|
11
13
|
) => Promise<T>;
|
|
12
14
|
|
|
15
|
+
export interface ManagedDatabaseSnapshot {
|
|
16
|
+
dataPath: string;
|
|
17
|
+
storageMode: ManagedStorageMode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ManagedDatabaseAdmission = <T>(
|
|
21
|
+
operation: (snapshot: ManagedDatabaseSnapshot) => T | Promise<T>
|
|
22
|
+
) => Promise<T>;
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* Admit SDK work against one managed-process generation. The operation and its
|
|
15
26
|
* final state/config check share the same lifecycle section as start/stop.
|
|
@@ -32,6 +43,22 @@ export function managedRuntimeAdmission(
|
|
|
32
43
|
});
|
|
33
44
|
}
|
|
34
45
|
|
|
46
|
+
/** Pin DB-backed observability to the live process config for one reader lease. */
|
|
47
|
+
export function managedDatabaseAdmission(
|
|
48
|
+
manager: ProcessManager,
|
|
49
|
+
lifecycle: AgentLifecyclePort
|
|
50
|
+
): ManagedDatabaseAdmission {
|
|
51
|
+
return (operation) =>
|
|
52
|
+
lifecycle.lease(async () => {
|
|
53
|
+
const snapshot = manager.getStatus();
|
|
54
|
+
const config = snapshot.runningConfig ?? snapshot.config;
|
|
55
|
+
return operation({
|
|
56
|
+
dataPath: config.dataPath,
|
|
57
|
+
storageMode: managedStorageMode(config),
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
function generationChanged(initial: StatusSnapshot, current: StatusSnapshot): boolean {
|
|
36
63
|
return (
|
|
37
64
|
initial.status === 'running' &&
|
package/agent/server/router.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
executePreparedBackupRequest,
|
|
3
|
+
prepareBackupRequest,
|
|
4
|
+
} from '../backup/routes';
|
|
2
5
|
import type { BackupRunnerPort } from '../backup/runner';
|
|
3
6
|
import { routeFlowRequest } from '../flow/routes';
|
|
4
7
|
import type { ManagedTargetPolicy } from '../managedTarget';
|
|
5
|
-
import type
|
|
8
|
+
import { managedStorageMode, type ProcessManager } from '../manager';
|
|
6
9
|
import { routeQueueOperationsRequest } from '../queue/routes';
|
|
7
10
|
import type { QueueOperationsPort } from '../queue/types';
|
|
8
11
|
import type { WorkflowRuntimePort } from '../workflow/runtime';
|
|
@@ -11,7 +14,7 @@ import { assertManagedControl, routeControlRequest } from './controlRoutes';
|
|
|
11
14
|
import type { ServerControlTarget } from './controlTarget';
|
|
12
15
|
import { routeDatabaseRequest } from './databaseRoutes';
|
|
13
16
|
import type { AgentLifecyclePort } from './lifecycle';
|
|
14
|
-
import { managedRuntimeAdmission } from './managedRuntime';
|
|
17
|
+
import { managedDatabaseAdmission, managedRuntimeAdmission } from './managedRuntime';
|
|
15
18
|
import type { RouteResponse } from './types';
|
|
16
19
|
import { routeWorkflowReadRequest } from './workflowReadRoutes';
|
|
17
20
|
|
|
@@ -44,7 +47,9 @@ export async function routeAgentRequest(
|
|
|
44
47
|
|
|
45
48
|
const snapshot = manager.getStatus();
|
|
46
49
|
const managedConfig = snapshot.runningConfig ?? snapshot.config;
|
|
50
|
+
const storageMode = managedStorageMode(managedConfig);
|
|
47
51
|
const admission = managedRuntimeAdmission(manager, lifecycle, snapshot);
|
|
52
|
+
const databaseAdmission = managedDatabaseAdmission(manager, lifecycle);
|
|
48
53
|
if (pathname.startsWith('/flows/')) {
|
|
49
54
|
const flow = await routeFlowRequest(
|
|
50
55
|
request,
|
|
@@ -73,14 +78,16 @@ export async function routeAgentRequest(
|
|
|
73
78
|
if (workflowRuntime) return workflowRuntime;
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
if (isWorkflowReadRoute(pathname, method)) {
|
|
82
|
+
const workflowRead = await routeWorkflowReadRequest(
|
|
83
|
+
request,
|
|
84
|
+
pathname,
|
|
85
|
+
method,
|
|
86
|
+
managedConfig.dataPath,
|
|
87
|
+
databaseAdmission
|
|
88
|
+
);
|
|
89
|
+
if (workflowRead) return workflowRead;
|
|
90
|
+
}
|
|
84
91
|
|
|
85
92
|
if (pathname.startsWith('/queue-operations/')) {
|
|
86
93
|
const queueOperation = await routeQueueOperationsRequest(
|
|
@@ -97,54 +104,93 @@ export async function routeAgentRequest(
|
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
if (pathname.startsWith('/backup/')) {
|
|
107
|
+
if (storageMode !== 'sqlite') return sqliteOnly('S3 backup', storageMode);
|
|
100
108
|
if (pathname === '/backup/restore' && method === 'POST') {
|
|
101
109
|
assertManagedControl(controlTarget);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
110
|
+
const prepared = await prepareBackupRequest(
|
|
111
|
+
request,
|
|
112
|
+
pathname,
|
|
113
|
+
method,
|
|
114
|
+
managedConfig,
|
|
115
|
+
managedTargetPolicy
|
|
116
|
+
);
|
|
117
|
+
if (!prepared || prepared.operation !== 'restore') return null;
|
|
118
|
+
return lifecycle.run(async () => {
|
|
119
|
+
const current = manager.getStatus();
|
|
120
|
+
const restoreConfig = current.runningConfig ?? current.config;
|
|
121
|
+
const currentStorageMode = managedStorageMode(restoreConfig);
|
|
122
|
+
if (currentStorageMode !== 'sqlite') {
|
|
123
|
+
return sqliteOnly('S3 backup', currentStorageMode);
|
|
124
|
+
}
|
|
125
|
+
return manager.withStoppedMaintenance('restoring a backup', async () => {
|
|
126
|
+
return executePreparedBackupRequest(
|
|
127
|
+
prepared,
|
|
128
|
+
restoreConfig,
|
|
129
|
+
current.status === 'running',
|
|
130
|
+
await manager.dbStats(restoreConfig.dataPath),
|
|
131
|
+
backupRunner,
|
|
132
|
+
undefined,
|
|
133
|
+
managedTargetPolicy
|
|
134
|
+
);
|
|
135
|
+
});
|
|
120
136
|
});
|
|
121
137
|
}
|
|
122
|
-
const
|
|
123
|
-
const backupConfig = { ...managedConfig, extraEnv: desiredConfig.extraEnv };
|
|
124
|
-
const backup = await routeBackupRequest(
|
|
138
|
+
const prepared = await prepareBackupRequest(
|
|
125
139
|
request,
|
|
126
140
|
pathname,
|
|
127
141
|
method,
|
|
128
|
-
|
|
129
|
-
snapshot.status === 'running',
|
|
130
|
-
database,
|
|
131
|
-
backupRunner,
|
|
132
|
-
(extraEnv) => manager.setConfig({ extraEnv }),
|
|
142
|
+
managedConfig,
|
|
133
143
|
managedTargetPolicy
|
|
134
144
|
);
|
|
135
|
-
if (
|
|
145
|
+
if (prepared) {
|
|
146
|
+
const execute = async () => {
|
|
147
|
+
const current = manager.getStatus();
|
|
148
|
+
const desired = manager.getConfig();
|
|
149
|
+
const effective = current.runningConfig ?? current.config;
|
|
150
|
+
const backupConfig = { ...effective, extraEnv: desired.extraEnv };
|
|
151
|
+
const currentStorageMode = managedStorageMode(backupConfig);
|
|
152
|
+
if (currentStorageMode !== 'sqlite') {
|
|
153
|
+
return sqliteOnly('S3 backup', currentStorageMode);
|
|
154
|
+
}
|
|
155
|
+
return executePreparedBackupRequest(
|
|
156
|
+
prepared,
|
|
157
|
+
backupConfig,
|
|
158
|
+
current.status === 'running',
|
|
159
|
+
await manager.dbStats(backupConfig.dataPath),
|
|
160
|
+
backupRunner,
|
|
161
|
+
(extraEnv) => {
|
|
162
|
+
manager.setConfig({ extraEnv });
|
|
163
|
+
},
|
|
164
|
+
managedTargetPolicy
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
return prepared.operation === 'configure'
|
|
168
|
+
? lifecycle.run(execute)
|
|
169
|
+
: lifecycle.lease(execute);
|
|
170
|
+
}
|
|
136
171
|
}
|
|
137
172
|
|
|
173
|
+
if ((pathname === '/db' || pathname.startsWith('/db/')) && storageMode !== 'sqlite') {
|
|
174
|
+
return sqliteOnly('Database inspection', storageMode);
|
|
175
|
+
}
|
|
138
176
|
return routeDatabaseRequest(
|
|
139
177
|
request,
|
|
140
178
|
pathname,
|
|
141
179
|
method,
|
|
142
|
-
|
|
180
|
+
managedConfig.dataPath,
|
|
143
181
|
origin,
|
|
144
|
-
allowedOrigins
|
|
182
|
+
allowedOrigins,
|
|
183
|
+
databaseAdmission
|
|
145
184
|
);
|
|
146
185
|
}
|
|
147
186
|
|
|
187
|
+
function sqliteOnly(feature: string, storageMode: 'memory' | 'postgres'): RouteResponse {
|
|
188
|
+
return {
|
|
189
|
+
status: 409,
|
|
190
|
+
body: { ok: false, error: `${feature} requires Bunqueue SQLite storage; ${storageMode} is active` },
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
148
194
|
function isWorkflowRuntimeRoute(pathname: string, method: string): boolean {
|
|
149
195
|
if (method === 'GET') return pathname === '/workflows/runtime';
|
|
150
196
|
if (method !== 'POST') return false;
|
|
@@ -159,3 +205,7 @@ function isWorkflowRuntimeRoute(pathname: string, method: string): boolean {
|
|
|
159
205
|
/^\/workflows\/[^/]+\/(signal|resume-compensation|abandon-compensation)$/.test(pathname)
|
|
160
206
|
);
|
|
161
207
|
}
|
|
208
|
+
|
|
209
|
+
function isWorkflowReadRoute(pathname: string, method: string): boolean {
|
|
210
|
+
return method === 'GET' && (pathname === '/workflows' || pathname.startsWith('/workflows/'));
|
|
211
|
+
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
WORKFLOW_STATES,
|
|
8
8
|
} from '../workflows';
|
|
9
9
|
import type { RouteResponse } from './types';
|
|
10
|
+
import type { ManagedDatabaseAdmission } from './managedRuntime';
|
|
10
11
|
|
|
11
12
|
function workflowKind(query: URLSearchParams): WorkflowStoreKind {
|
|
12
13
|
const kind = query.get('kind') ?? 'active';
|
|
@@ -23,14 +24,18 @@ function integer(query: URLSearchParams, name: 'limit' | 'offset', fallback: num
|
|
|
23
24
|
return Number(raw);
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
export function routeWorkflowReadRequest(
|
|
27
|
+
export async function routeWorkflowReadRequest(
|
|
27
28
|
request: Request,
|
|
28
29
|
pathname: string,
|
|
29
30
|
method: string,
|
|
30
|
-
dataPath: string
|
|
31
|
-
|
|
31
|
+
dataPath: string,
|
|
32
|
+
admission?: ManagedDatabaseAdmission
|
|
33
|
+
): Promise<RouteResponse | null> {
|
|
32
34
|
if (pathname === '/workflows/stats' && method === 'GET') {
|
|
33
|
-
return
|
|
35
|
+
return withDatabase(dataPath, admission, (path) => ({
|
|
36
|
+
status: 200,
|
|
37
|
+
body: { ok: true, ...workflowStats(path) },
|
|
38
|
+
}));
|
|
34
39
|
}
|
|
35
40
|
if (pathname === '/workflows' && method === 'GET') {
|
|
36
41
|
const query = new URL(request.url).searchParams;
|
|
@@ -47,11 +52,11 @@ export function routeWorkflowReadRequest(
|
|
|
47
52
|
) {
|
|
48
53
|
throw new Error('Unknown workflow execution state');
|
|
49
54
|
}
|
|
50
|
-
return {
|
|
55
|
+
return withDatabase(dataPath, admission, (path) => ({
|
|
51
56
|
status: 200,
|
|
52
57
|
body: {
|
|
53
58
|
ok: true,
|
|
54
|
-
...workflowExecutions(
|
|
59
|
+
...workflowExecutions(path, {
|
|
55
60
|
kind: workflowKind(query),
|
|
56
61
|
workflowName: query.get('workflowName') || undefined,
|
|
57
62
|
state: state as WorkflowStateFilter | undefined,
|
|
@@ -59,7 +64,7 @@ export function routeWorkflowReadRequest(
|
|
|
59
64
|
offset: integer(query, 'offset', 0),
|
|
60
65
|
}),
|
|
61
66
|
},
|
|
62
|
-
};
|
|
67
|
+
}));
|
|
63
68
|
}
|
|
64
69
|
if (!pathname.startsWith('/workflows/') || method !== 'GET') return null;
|
|
65
70
|
const rawId = pathname.slice('/workflows/'.length);
|
|
@@ -73,8 +78,20 @@ export function routeWorkflowReadRequest(
|
|
|
73
78
|
throw new Error(`Duplicate workflow detail option: ${key}`);
|
|
74
79
|
}
|
|
75
80
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
81
|
+
return withDatabase(dataPath, admission, (path) => {
|
|
82
|
+
const execution = workflowExecution(path, decodeURIComponent(rawId), workflowKind(query));
|
|
83
|
+
return execution
|
|
84
|
+
? { status: 200, body: { ok: true, execution } }
|
|
85
|
+
: { status: 404, body: { ok: false, error: 'Workflow execution not found' } };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function withDatabase<T>(
|
|
90
|
+
dataPath: string,
|
|
91
|
+
admission: ManagedDatabaseAdmission | undefined,
|
|
92
|
+
operation: (path: string) => T
|
|
93
|
+
): Promise<T> {
|
|
94
|
+
return admission
|
|
95
|
+
? admission((snapshot) => operation(snapshot.dataPath))
|
|
96
|
+
: Promise.resolve(operation(dataPath));
|
|
80
97
|
}
|
package/agent/workflows.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Read-only facade for Bunqueue 2.
|
|
2
|
+
* Read-only facade for Bunqueue 2.9.0 Workflow Engine observability.
|
|
3
3
|
* Persistence, decoding and validation live behind this stable public module.
|
|
4
4
|
*/
|
|
5
5
|
export { workflowExecution } from './workflow/detail';
|