@getworkbench/core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/chunk-O7DNWUZD.js +571 -0
- package/dist/chunk-O7DNWUZD.js.map +1 -0
- package/dist/hono.d.ts +43 -0
- package/dist/hono.js +32 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.d.ts +9 -673
- package/dist/index.js +131 -575
- package/dist/index.js.map +1 -1
- package/dist/ui/assets/index.css +1 -1
- package/dist/ui/assets/index.js +116 -116
- package/dist/workbench-CRdU4cB7.d.ts +692 -0
- package/dist/workbench-DMfd1JuA.d.ts +692 -0
- package/package.json +14 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UI_DIST_PATH,
|
|
3
|
+
buildRouteTable,
|
|
4
|
+
buildWorkbenchApp,
|
|
5
|
+
computeBasePath,
|
|
6
|
+
renderIndexHtml,
|
|
7
|
+
resolveBasePath,
|
|
8
|
+
serveStaticAsset
|
|
9
|
+
} from "./chunk-O7DNWUZD.js";
|
|
10
|
+
|
|
11
|
+
// src/core/discover.ts
|
|
12
|
+
import { Queue } from "bullmq";
|
|
13
|
+
import { Redis } from "ioredis";
|
|
14
|
+
async function discoverQueues(connection, prefix = "bull") {
|
|
15
|
+
const normalized = normalizeConnection(connection);
|
|
16
|
+
const client = createScanClient(normalized);
|
|
17
|
+
const firstError = captureFirstError(client);
|
|
18
|
+
try {
|
|
19
|
+
await Promise.race([client.ping(), firstError.promise]);
|
|
20
|
+
const names = await scanQueueNames(client, prefix);
|
|
21
|
+
return names.map(
|
|
22
|
+
(name) => new Queue(name, {
|
|
23
|
+
connection: { ...normalized },
|
|
24
|
+
prefix
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
} finally {
|
|
28
|
+
firstError.dispose();
|
|
29
|
+
client.disconnect();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function captureFirstError(client) {
|
|
33
|
+
let onError = null;
|
|
34
|
+
const promise = new Promise((_, reject) => {
|
|
35
|
+
onError = (err) => reject(err);
|
|
36
|
+
client.once("error", onError);
|
|
37
|
+
});
|
|
38
|
+
client.on("error", () => {
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
promise,
|
|
42
|
+
dispose: () => {
|
|
43
|
+
if (onError) client.off("error", onError);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function normalizeConnection(connection) {
|
|
48
|
+
if (typeof connection === "string") {
|
|
49
|
+
return { url: connection };
|
|
50
|
+
}
|
|
51
|
+
return { ...connection };
|
|
52
|
+
}
|
|
53
|
+
function createScanClient(opts) {
|
|
54
|
+
const { url, ...rest } = opts;
|
|
55
|
+
if (url) {
|
|
56
|
+
return new Redis(url, {
|
|
57
|
+
...rest,
|
|
58
|
+
lazyConnect: false,
|
|
59
|
+
maxRetriesPerRequest: 1
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return new Redis({ ...rest, lazyConnect: false, maxRetriesPerRequest: 1 });
|
|
63
|
+
}
|
|
64
|
+
async function scanQueueNames(client, prefix) {
|
|
65
|
+
const pattern = `${prefix}:*:meta`;
|
|
66
|
+
const names = /* @__PURE__ */ new Set();
|
|
67
|
+
let cursor = "0";
|
|
68
|
+
do {
|
|
69
|
+
const [next, batch] = await client.scan(
|
|
70
|
+
cursor,
|
|
71
|
+
"MATCH",
|
|
72
|
+
pattern,
|
|
73
|
+
"COUNT",
|
|
74
|
+
500
|
|
75
|
+
);
|
|
76
|
+
cursor = next;
|
|
77
|
+
for (const key of batch) {
|
|
78
|
+
const name = parseQueueName(key, prefix);
|
|
79
|
+
if (name) names.add(name);
|
|
80
|
+
}
|
|
81
|
+
} while (cursor !== "0");
|
|
82
|
+
return Array.from(names).sort();
|
|
83
|
+
}
|
|
84
|
+
function parseQueueName(key, prefix) {
|
|
85
|
+
const head = `${prefix}:`;
|
|
86
|
+
const tail = ":meta";
|
|
87
|
+
if (!key.startsWith(head) || !key.endsWith(tail)) return null;
|
|
88
|
+
return key.slice(head.length, key.length - tail.length);
|
|
89
|
+
}
|
|
90
|
+
|
|
1
91
|
// src/core/queue-manager.ts
|
|
2
92
|
import { FlowProducer } from "bullmq";
|
|
3
93
|
import { LRUCache } from "lru-cache";
|
|
@@ -1689,24 +1779,56 @@ var QueueManager = class {
|
|
|
1689
1779
|
};
|
|
1690
1780
|
|
|
1691
1781
|
// src/core/workbench.ts
|
|
1692
|
-
var WorkbenchCore = class {
|
|
1782
|
+
var WorkbenchCore = class _WorkbenchCore {
|
|
1693
1783
|
options;
|
|
1694
1784
|
queueManager;
|
|
1695
|
-
|
|
1785
|
+
discovery;
|
|
1786
|
+
constructor(options, discovery = null) {
|
|
1696
1787
|
const opts = Array.isArray(options) ? { queues: options } : options;
|
|
1697
1788
|
this.options = {
|
|
1698
1789
|
title: "Workbench",
|
|
1699
1790
|
readonly: false,
|
|
1700
1791
|
...opts
|
|
1701
1792
|
};
|
|
1702
|
-
|
|
1793
|
+
this.discovery = discovery;
|
|
1794
|
+
const explicit = this.options.queues ?? [];
|
|
1795
|
+
const allowEmpty = !!this.options.redis;
|
|
1796
|
+
if (explicit.length === 0 && !allowEmpty) {
|
|
1703
1797
|
throw new Error(
|
|
1704
1798
|
"Workbench requires at least one queue. Pass queues directly or provide a redis connection for auto-discovery."
|
|
1705
1799
|
);
|
|
1706
1800
|
}
|
|
1707
|
-
this.queueManager = new QueueManager(
|
|
1708
|
-
|
|
1709
|
-
|
|
1801
|
+
this.queueManager = new QueueManager(explicit, this.options.tags || []);
|
|
1802
|
+
}
|
|
1803
|
+
/**
|
|
1804
|
+
* Async factory: build a `WorkbenchCore` from `WorkbenchOptions`, performing
|
|
1805
|
+
* BullMQ queue auto-discovery via `SCAN <prefix>:*:meta` when `queues` is
|
|
1806
|
+
* not provided.
|
|
1807
|
+
*
|
|
1808
|
+
* - When `queues` is set explicitly, behaves like `new WorkbenchCore(opts)`.
|
|
1809
|
+
* - When only `redis` is set, scans the connection for queues, caps at
|
|
1810
|
+
* `maxQueues` (default 100) to avoid connection storms with very large
|
|
1811
|
+
* deployments, and constructs the core with the resulting list.
|
|
1812
|
+
* - When no queues are discovered, the core is constructed with an empty
|
|
1813
|
+
* queue map so the dashboard can render an "empty" state instead of
|
|
1814
|
+
* erroring out.
|
|
1815
|
+
*/
|
|
1816
|
+
static async fromOptions(opts) {
|
|
1817
|
+
if (opts.queues?.length) {
|
|
1818
|
+
return new _WorkbenchCore(opts);
|
|
1819
|
+
}
|
|
1820
|
+
if (!opts.redis) {
|
|
1821
|
+
throw new Error(
|
|
1822
|
+
"WorkbenchCore.fromOptions requires either `queues` or `redis`"
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
const prefix = opts.prefix ?? "bull";
|
|
1826
|
+
const cap = opts.maxQueues ?? 100;
|
|
1827
|
+
const all = await discoverQueues(opts.redis, prefix);
|
|
1828
|
+
const queues = all.slice(0, cap);
|
|
1829
|
+
return new _WorkbenchCore(
|
|
1830
|
+
{ ...opts, queues },
|
|
1831
|
+
{ total: all.length, capped: all.length > cap, cap }
|
|
1710
1832
|
);
|
|
1711
1833
|
}
|
|
1712
1834
|
/**
|
|
@@ -1737,577 +1859,12 @@ var WorkbenchCore = class {
|
|
|
1737
1859
|
logo: this.options.logo,
|
|
1738
1860
|
readonly: this.options.readonly,
|
|
1739
1861
|
queues: this.queueManager.getQueueNames(),
|
|
1740
|
-
tags: this.queueManager.getTagFields()
|
|
1862
|
+
tags: this.queueManager.getTagFields(),
|
|
1863
|
+
discovery: this.discovery
|
|
1741
1864
|
};
|
|
1742
1865
|
}
|
|
1743
1866
|
};
|
|
1744
1867
|
|
|
1745
|
-
// src/server/hono-app.ts
|
|
1746
|
-
import { Hono as Hono2 } from "hono";
|
|
1747
|
-
import { basicAuth } from "hono/basic-auth";
|
|
1748
|
-
import { cors } from "hono/cors";
|
|
1749
|
-
|
|
1750
|
-
// src/api/router.ts
|
|
1751
|
-
import { Hono } from "hono";
|
|
1752
|
-
|
|
1753
|
-
// src/api/handlers.ts
|
|
1754
|
-
function parseSort(sort) {
|
|
1755
|
-
if (!sort) return void 0;
|
|
1756
|
-
const [field, dir] = sort.split(":");
|
|
1757
|
-
if (!field) return void 0;
|
|
1758
|
-
return {
|
|
1759
|
-
field,
|
|
1760
|
-
direction: dir === "asc" ? "asc" : "desc"
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
var readonlyError = {
|
|
1764
|
-
status: 403,
|
|
1765
|
-
body: { error: "Dashboard is in readonly mode" }
|
|
1766
|
-
};
|
|
1767
|
-
function buildRouteTable(core) {
|
|
1768
|
-
const qm = core.queueManager;
|
|
1769
|
-
const isReadonly = () => !!core.options.readonly;
|
|
1770
|
-
return [
|
|
1771
|
-
{
|
|
1772
|
-
method: "post",
|
|
1773
|
-
path: "/refresh",
|
|
1774
|
-
handler: async () => {
|
|
1775
|
-
qm.clearCache();
|
|
1776
|
-
return { status: 200, body: { success: true } };
|
|
1777
|
-
}
|
|
1778
|
-
},
|
|
1779
|
-
{
|
|
1780
|
-
method: "get",
|
|
1781
|
-
path: "/overview",
|
|
1782
|
-
handler: async () => ({
|
|
1783
|
-
status: 200,
|
|
1784
|
-
body: await qm.getOverview()
|
|
1785
|
-
})
|
|
1786
|
-
},
|
|
1787
|
-
{
|
|
1788
|
-
method: "get",
|
|
1789
|
-
path: "/counts",
|
|
1790
|
-
handler: async () => ({
|
|
1791
|
-
status: 200,
|
|
1792
|
-
body: await qm.getQuickCounts()
|
|
1793
|
-
})
|
|
1794
|
-
},
|
|
1795
|
-
{
|
|
1796
|
-
method: "get",
|
|
1797
|
-
path: "/runs",
|
|
1798
|
-
handler: async ({ query }) => {
|
|
1799
|
-
const limit = Number(query.limit) || 50;
|
|
1800
|
-
const cursor = query.cursor;
|
|
1801
|
-
const start = cursor ? Number(cursor) : 0;
|
|
1802
|
-
const sort = parseSort(query.sort);
|
|
1803
|
-
const status = query.status;
|
|
1804
|
-
const q = query.q;
|
|
1805
|
-
const from = query.from;
|
|
1806
|
-
const to = query.to;
|
|
1807
|
-
const tagsParam = query.tags;
|
|
1808
|
-
let tags;
|
|
1809
|
-
if (tagsParam) {
|
|
1810
|
-
try {
|
|
1811
|
-
tags = JSON.parse(tagsParam);
|
|
1812
|
-
} catch {
|
|
1813
|
-
const tagPairs = tagsParam.split(",");
|
|
1814
|
-
tags = {};
|
|
1815
|
-
for (const pair of tagPairs) {
|
|
1816
|
-
const [key, value] = pair.split(":");
|
|
1817
|
-
if (key && value) {
|
|
1818
|
-
tags[key.trim()] = value.trim();
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
}
|
|
1823
|
-
let timeRange;
|
|
1824
|
-
if (from && to) {
|
|
1825
|
-
timeRange = {
|
|
1826
|
-
start: Number(from),
|
|
1827
|
-
end: Number(to)
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
let text;
|
|
1831
|
-
if (q) {
|
|
1832
|
-
if (!q.includes(":")) {
|
|
1833
|
-
text = q;
|
|
1834
|
-
} else {
|
|
1835
|
-
const parts = q.split(" ");
|
|
1836
|
-
const textParts = parts.filter((p) => !p.includes(":"));
|
|
1837
|
-
if (textParts.length > 0) {
|
|
1838
|
-
text = textParts.join(" ");
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
const filters = status || tags || text || timeRange ? {
|
|
1843
|
-
status,
|
|
1844
|
-
tags,
|
|
1845
|
-
text,
|
|
1846
|
-
timeRange
|
|
1847
|
-
} : void 0;
|
|
1848
|
-
return {
|
|
1849
|
-
status: 200,
|
|
1850
|
-
body: await qm.getAllRuns(limit, start, sort, filters)
|
|
1851
|
-
};
|
|
1852
|
-
}
|
|
1853
|
-
},
|
|
1854
|
-
{
|
|
1855
|
-
method: "get",
|
|
1856
|
-
path: "/schedulers",
|
|
1857
|
-
handler: async ({ query }) => {
|
|
1858
|
-
const repeatableSort = parseSort(query.repeatableSort);
|
|
1859
|
-
const delayedSort = parseSort(query.delayedSort);
|
|
1860
|
-
return {
|
|
1861
|
-
status: 200,
|
|
1862
|
-
body: await qm.getSchedulers(repeatableSort, delayedSort)
|
|
1863
|
-
};
|
|
1864
|
-
}
|
|
1865
|
-
},
|
|
1866
|
-
{
|
|
1867
|
-
method: "post",
|
|
1868
|
-
path: "/test",
|
|
1869
|
-
handler: async ({ body }) => {
|
|
1870
|
-
if (isReadonly()) return readonlyError;
|
|
1871
|
-
const req = body;
|
|
1872
|
-
if (!req?.queueName || !req.jobName) {
|
|
1873
|
-
return {
|
|
1874
|
-
status: 400,
|
|
1875
|
-
body: { error: "queueName and jobName are required" }
|
|
1876
|
-
};
|
|
1877
|
-
}
|
|
1878
|
-
try {
|
|
1879
|
-
const result = await qm.enqueueJob(req);
|
|
1880
|
-
return { status: 200, body: result };
|
|
1881
|
-
} catch (e) {
|
|
1882
|
-
return { status: 400, body: { error: e.message } };
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
},
|
|
1886
|
-
{
|
|
1887
|
-
method: "get",
|
|
1888
|
-
path: "/queue-names",
|
|
1889
|
-
handler: async () => ({
|
|
1890
|
-
status: 200,
|
|
1891
|
-
body: qm.getQueueNames()
|
|
1892
|
-
})
|
|
1893
|
-
},
|
|
1894
|
-
{
|
|
1895
|
-
method: "get",
|
|
1896
|
-
path: "/queues",
|
|
1897
|
-
handler: async () => ({
|
|
1898
|
-
status: 200,
|
|
1899
|
-
body: await qm.getQueues()
|
|
1900
|
-
})
|
|
1901
|
-
},
|
|
1902
|
-
{
|
|
1903
|
-
method: "get",
|
|
1904
|
-
path: "/metrics",
|
|
1905
|
-
handler: async () => ({
|
|
1906
|
-
status: 200,
|
|
1907
|
-
body: await qm.getMetrics()
|
|
1908
|
-
})
|
|
1909
|
-
},
|
|
1910
|
-
{
|
|
1911
|
-
method: "get",
|
|
1912
|
-
path: "/activity",
|
|
1913
|
-
handler: async () => ({
|
|
1914
|
-
status: 200,
|
|
1915
|
-
body: await qm.getActivityStats()
|
|
1916
|
-
})
|
|
1917
|
-
},
|
|
1918
|
-
{
|
|
1919
|
-
method: "get",
|
|
1920
|
-
path: "/queues/:name/jobs",
|
|
1921
|
-
handler: async ({ params, query }) => {
|
|
1922
|
-
const name = params.name;
|
|
1923
|
-
const status = query.status;
|
|
1924
|
-
const limit = Number(query.limit) || 50;
|
|
1925
|
-
const cursor = query.cursor;
|
|
1926
|
-
const start = cursor ? Number(cursor) : 0;
|
|
1927
|
-
const sort = parseSort(query.sort);
|
|
1928
|
-
return {
|
|
1929
|
-
status: 200,
|
|
1930
|
-
body: await qm.getJobs(name, status, limit, start, sort)
|
|
1931
|
-
};
|
|
1932
|
-
}
|
|
1933
|
-
},
|
|
1934
|
-
{
|
|
1935
|
-
method: "get",
|
|
1936
|
-
path: "/jobs/:queue/:id",
|
|
1937
|
-
handler: async ({ params }) => {
|
|
1938
|
-
const job = await qm.getJob(params.queue, params.id);
|
|
1939
|
-
if (!job) {
|
|
1940
|
-
return { status: 404, body: { error: "Job not found" } };
|
|
1941
|
-
}
|
|
1942
|
-
return { status: 200, body: job };
|
|
1943
|
-
}
|
|
1944
|
-
},
|
|
1945
|
-
{
|
|
1946
|
-
method: "post",
|
|
1947
|
-
path: "/jobs/:queue/:id/retry",
|
|
1948
|
-
handler: async ({ params }) => {
|
|
1949
|
-
if (isReadonly()) return readonlyError;
|
|
1950
|
-
const success = await qm.retryJob(params.queue, params.id);
|
|
1951
|
-
if (!success) {
|
|
1952
|
-
return { status: 400, body: { error: "Failed to retry job" } };
|
|
1953
|
-
}
|
|
1954
|
-
return { status: 200, body: { success: true } };
|
|
1955
|
-
}
|
|
1956
|
-
},
|
|
1957
|
-
{
|
|
1958
|
-
method: "post",
|
|
1959
|
-
path: "/jobs/:queue/:id/remove",
|
|
1960
|
-
handler: async ({ params }) => {
|
|
1961
|
-
if (isReadonly()) return readonlyError;
|
|
1962
|
-
const success = await qm.removeJob(params.queue, params.id);
|
|
1963
|
-
if (!success) {
|
|
1964
|
-
return { status: 400, body: { error: "Failed to remove job" } };
|
|
1965
|
-
}
|
|
1966
|
-
return { status: 200, body: { success: true } };
|
|
1967
|
-
}
|
|
1968
|
-
},
|
|
1969
|
-
{
|
|
1970
|
-
method: "post",
|
|
1971
|
-
path: "/jobs/:queue/:id/promote",
|
|
1972
|
-
handler: async ({ params }) => {
|
|
1973
|
-
if (isReadonly()) return readonlyError;
|
|
1974
|
-
const success = await qm.promoteJob(params.queue, params.id);
|
|
1975
|
-
if (!success) {
|
|
1976
|
-
return { status: 400, body: { error: "Failed to promote job" } };
|
|
1977
|
-
}
|
|
1978
|
-
return { status: 200, body: { success: true } };
|
|
1979
|
-
}
|
|
1980
|
-
},
|
|
1981
|
-
{
|
|
1982
|
-
method: "get",
|
|
1983
|
-
path: "/search",
|
|
1984
|
-
handler: async ({ query }) => {
|
|
1985
|
-
const q = query.q || "";
|
|
1986
|
-
const limit = Number(query.limit) || 20;
|
|
1987
|
-
if (!q) return { status: 200, body: { results: [] } };
|
|
1988
|
-
const results = await qm.search(q, limit);
|
|
1989
|
-
return { status: 200, body: { results } };
|
|
1990
|
-
}
|
|
1991
|
-
},
|
|
1992
|
-
{
|
|
1993
|
-
method: "get",
|
|
1994
|
-
path: "/tags/:field/values",
|
|
1995
|
-
handler: async ({ params, query }) => {
|
|
1996
|
-
const field = params.field;
|
|
1997
|
-
const limit = Number(query.limit) || 50;
|
|
1998
|
-
const tagFields = qm.getTagFields();
|
|
1999
|
-
if (tagFields.length > 0 && !tagFields.includes(field)) {
|
|
2000
|
-
return {
|
|
2001
|
-
status: 400,
|
|
2002
|
-
body: {
|
|
2003
|
-
error: `Field "${field}" is not a configured tag field`
|
|
2004
|
-
}
|
|
2005
|
-
};
|
|
2006
|
-
}
|
|
2007
|
-
const values = await qm.getTagValues(field, limit);
|
|
2008
|
-
return { status: 200, body: { field, values } };
|
|
2009
|
-
}
|
|
2010
|
-
},
|
|
2011
|
-
{
|
|
2012
|
-
method: "post",
|
|
2013
|
-
path: "/queues/:name/clean",
|
|
2014
|
-
handler: async ({ params, body }) => {
|
|
2015
|
-
if (isReadonly()) return readonlyError;
|
|
2016
|
-
const req = body;
|
|
2017
|
-
if (!req) {
|
|
2018
|
-
return { status: 400, body: { error: "Body required" } };
|
|
2019
|
-
}
|
|
2020
|
-
const count = await qm.cleanJobs(
|
|
2021
|
-
params.name,
|
|
2022
|
-
req.status,
|
|
2023
|
-
req.grace || 0
|
|
2024
|
-
);
|
|
2025
|
-
return { status: 200, body: { removed: count } };
|
|
2026
|
-
}
|
|
2027
|
-
},
|
|
2028
|
-
{
|
|
2029
|
-
method: "post",
|
|
2030
|
-
path: "/bulk/retry",
|
|
2031
|
-
handler: async ({ body }) => {
|
|
2032
|
-
if (isReadonly()) return readonlyError;
|
|
2033
|
-
const req = body;
|
|
2034
|
-
if (!req?.jobs) {
|
|
2035
|
-
return { status: 400, body: { error: "jobs is required" } };
|
|
2036
|
-
}
|
|
2037
|
-
return { status: 200, body: await qm.bulkRetry(req.jobs) };
|
|
2038
|
-
}
|
|
2039
|
-
},
|
|
2040
|
-
{
|
|
2041
|
-
method: "post",
|
|
2042
|
-
path: "/bulk/delete",
|
|
2043
|
-
handler: async ({ body }) => {
|
|
2044
|
-
if (isReadonly()) return readonlyError;
|
|
2045
|
-
const req = body;
|
|
2046
|
-
if (!req?.jobs) {
|
|
2047
|
-
return { status: 400, body: { error: "jobs is required" } };
|
|
2048
|
-
}
|
|
2049
|
-
return { status: 200, body: await qm.bulkDelete(req.jobs) };
|
|
2050
|
-
}
|
|
2051
|
-
},
|
|
2052
|
-
{
|
|
2053
|
-
method: "post",
|
|
2054
|
-
path: "/bulk/promote",
|
|
2055
|
-
handler: async ({ body }) => {
|
|
2056
|
-
if (isReadonly()) return readonlyError;
|
|
2057
|
-
const req = body;
|
|
2058
|
-
if (!req?.jobs) {
|
|
2059
|
-
return { status: 400, body: { error: "jobs is required" } };
|
|
2060
|
-
}
|
|
2061
|
-
return { status: 200, body: await qm.bulkPromote(req.jobs) };
|
|
2062
|
-
}
|
|
2063
|
-
},
|
|
2064
|
-
{
|
|
2065
|
-
method: "post",
|
|
2066
|
-
path: "/queues/:name/pause",
|
|
2067
|
-
handler: async ({ params }) => {
|
|
2068
|
-
if (isReadonly()) return readonlyError;
|
|
2069
|
-
try {
|
|
2070
|
-
await qm.pauseQueue(params.name);
|
|
2071
|
-
return { status: 200, body: { success: true, paused: true } };
|
|
2072
|
-
} catch (error) {
|
|
2073
|
-
return {
|
|
2074
|
-
status: 404,
|
|
2075
|
-
body: {
|
|
2076
|
-
error: error instanceof Error ? error.message : "Failed to pause queue"
|
|
2077
|
-
}
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
},
|
|
2082
|
-
{
|
|
2083
|
-
method: "post",
|
|
2084
|
-
path: "/queues/:name/resume",
|
|
2085
|
-
handler: async ({ params }) => {
|
|
2086
|
-
if (isReadonly()) return readonlyError;
|
|
2087
|
-
try {
|
|
2088
|
-
await qm.resumeQueue(params.name);
|
|
2089
|
-
return { status: 200, body: { success: true, paused: false } };
|
|
2090
|
-
} catch (error) {
|
|
2091
|
-
return {
|
|
2092
|
-
status: 404,
|
|
2093
|
-
body: {
|
|
2094
|
-
error: error instanceof Error ? error.message : "Failed to resume queue"
|
|
2095
|
-
}
|
|
2096
|
-
};
|
|
2097
|
-
}
|
|
2098
|
-
}
|
|
2099
|
-
},
|
|
2100
|
-
{
|
|
2101
|
-
method: "get",
|
|
2102
|
-
path: "/flows",
|
|
2103
|
-
handler: async ({ query }) => {
|
|
2104
|
-
const limit = Number(query.limit) || 50;
|
|
2105
|
-
const flows = await qm.getFlows(limit);
|
|
2106
|
-
return { status: 200, body: { flows } };
|
|
2107
|
-
}
|
|
2108
|
-
},
|
|
2109
|
-
{
|
|
2110
|
-
method: "get",
|
|
2111
|
-
path: "/flows/:queueName/:jobId",
|
|
2112
|
-
handler: async ({ params }) => {
|
|
2113
|
-
const flow = await qm.getFlow(params.queueName, params.jobId);
|
|
2114
|
-
if (!flow) {
|
|
2115
|
-
return { status: 404, body: { error: "Flow not found" } };
|
|
2116
|
-
}
|
|
2117
|
-
return { status: 200, body: flow };
|
|
2118
|
-
}
|
|
2119
|
-
},
|
|
2120
|
-
{
|
|
2121
|
-
method: "post",
|
|
2122
|
-
path: "/flows",
|
|
2123
|
-
handler: async ({ body }) => {
|
|
2124
|
-
if (isReadonly()) return readonlyError;
|
|
2125
|
-
const req = body;
|
|
2126
|
-
if (!req?.name || !req.queueName || !req.children?.length) {
|
|
2127
|
-
return {
|
|
2128
|
-
status: 400,
|
|
2129
|
-
body: { error: "name, queueName, and children are required" }
|
|
2130
|
-
};
|
|
2131
|
-
}
|
|
2132
|
-
try {
|
|
2133
|
-
const result = await qm.createFlow(req);
|
|
2134
|
-
return { status: 200, body: result };
|
|
2135
|
-
} catch (e) {
|
|
2136
|
-
return { status: 400, body: { error: e.message } };
|
|
2137
|
-
}
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
];
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
// src/api/router.ts
|
|
2144
|
-
function createApiRoutes(core) {
|
|
2145
|
-
const app = new Hono();
|
|
2146
|
-
for (const route of buildRouteTable(core)) {
|
|
2147
|
-
app[route.method](route.path, async (c) => {
|
|
2148
|
-
let body;
|
|
2149
|
-
const method = c.req.method;
|
|
2150
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
2151
|
-
const contentType = c.req.header("content-type") ?? "";
|
|
2152
|
-
if (contentType.includes("application/json")) {
|
|
2153
|
-
try {
|
|
2154
|
-
body = await c.req.json();
|
|
2155
|
-
} catch {
|
|
2156
|
-
body = void 0;
|
|
2157
|
-
}
|
|
2158
|
-
}
|
|
2159
|
-
}
|
|
2160
|
-
const input = {
|
|
2161
|
-
params: c.req.param(),
|
|
2162
|
-
query: c.req.query(),
|
|
2163
|
-
body
|
|
2164
|
-
};
|
|
2165
|
-
const result = await route.handler(input);
|
|
2166
|
-
return new Response(JSON.stringify(result.body), {
|
|
2167
|
-
status: result.status,
|
|
2168
|
-
headers: { "Content-Type": "application/json" }
|
|
2169
|
-
});
|
|
2170
|
-
});
|
|
2171
|
-
}
|
|
2172
|
-
return app;
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
// src/server/base-path.ts
|
|
2176
|
-
var CLIENT_ROUTES = [
|
|
2177
|
-
/\/queues\/[^/]+\/jobs\/[^/]+\/?$/,
|
|
2178
|
-
/\/queues\/[^/]+\/?$/,
|
|
2179
|
-
/\/flows\/[^/]+\/[^/]+\/?$/,
|
|
2180
|
-
/\/schedulers\/?$/,
|
|
2181
|
-
/\/flows\/?$/,
|
|
2182
|
-
/\/metrics\/?$/,
|
|
2183
|
-
/\/test\/?$/
|
|
2184
|
-
];
|
|
2185
|
-
function computeBasePath(pathname) {
|
|
2186
|
-
let basePath = pathname;
|
|
2187
|
-
for (const route of CLIENT_ROUTES) {
|
|
2188
|
-
basePath = basePath.replace(route, "");
|
|
2189
|
-
}
|
|
2190
|
-
if (!basePath.endsWith("/")) {
|
|
2191
|
-
basePath = `${basePath}/`;
|
|
2192
|
-
}
|
|
2193
|
-
return basePath;
|
|
2194
|
-
}
|
|
2195
|
-
function resolveBasePath(override, pathname) {
|
|
2196
|
-
if (override) {
|
|
2197
|
-
return override.endsWith("/") ? override : `${override}/`;
|
|
2198
|
-
}
|
|
2199
|
-
return computeBasePath(pathname);
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
// src/server/static-assets.ts
|
|
2203
|
-
import { existsSync, readFileSync } from "fs";
|
|
2204
|
-
import { join as join2 } from "path";
|
|
2205
|
-
|
|
2206
|
-
// src/ui-dist.ts
|
|
2207
|
-
import { dirname, join } from "path";
|
|
2208
|
-
import { fileURLToPath } from "url";
|
|
2209
|
-
var UI_DIST_PATH = join(dirname(fileURLToPath(import.meta.url)), "ui");
|
|
2210
|
-
|
|
2211
|
-
// src/server/static-assets.ts
|
|
2212
|
-
function serveStaticAsset(filename) {
|
|
2213
|
-
const filePath = join2(UI_DIST_PATH, "assets", filename);
|
|
2214
|
-
if (!existsSync(filePath)) {
|
|
2215
|
-
return { status: 404, body: null, contentType: "text/plain" };
|
|
2216
|
-
}
|
|
2217
|
-
const body = readFileSync(filePath);
|
|
2218
|
-
const contentType = filename.endsWith(".js") ? "application/javascript" : filename.endsWith(".css") ? "text/css" : filename.endsWith(".svg") ? "image/svg+xml" : filename.endsWith(".png") ? "image/png" : filename.endsWith(".woff2") ? "font/woff2" : "application/octet-stream";
|
|
2219
|
-
return { status: 200, body, contentType };
|
|
2220
|
-
}
|
|
2221
|
-
function renderIndexHtml(basePath, title) {
|
|
2222
|
-
const indexPath = join2(UI_DIST_PATH, "index.html");
|
|
2223
|
-
if (existsSync(indexPath)) {
|
|
2224
|
-
let html = readFileSync(indexPath, "utf-8");
|
|
2225
|
-
html = html.replace("<head>", `<head>
|
|
2226
|
-
<base href="${basePath}">`);
|
|
2227
|
-
return { body: html, contentType: "text/html; charset=utf-8" };
|
|
2228
|
-
}
|
|
2229
|
-
return {
|
|
2230
|
-
body: fallbackHtml(title, basePath),
|
|
2231
|
-
contentType: "text/html; charset=utf-8"
|
|
2232
|
-
};
|
|
2233
|
-
}
|
|
2234
|
-
function fallbackHtml(title, basePath) {
|
|
2235
|
-
return `<!DOCTYPE html>
|
|
2236
|
-
<html lang="en">
|
|
2237
|
-
<head>
|
|
2238
|
-
<base href="${basePath}">
|
|
2239
|
-
<meta charset="UTF-8" />
|
|
2240
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
2241
|
-
<title>${title}</title>
|
|
2242
|
-
<style>
|
|
2243
|
-
body {
|
|
2244
|
-
font-family: system-ui, sans-serif;
|
|
2245
|
-
display: flex;
|
|
2246
|
-
align-items: center;
|
|
2247
|
-
justify-content: center;
|
|
2248
|
-
min-height: 100vh;
|
|
2249
|
-
margin: 0;
|
|
2250
|
-
background: #0a0a0a;
|
|
2251
|
-
color: #fafafa;
|
|
2252
|
-
}
|
|
2253
|
-
.message {
|
|
2254
|
-
text-align: center;
|
|
2255
|
-
padding: 2rem;
|
|
2256
|
-
}
|
|
2257
|
-
code {
|
|
2258
|
-
background: #1a1a1a;
|
|
2259
|
-
padding: 0.5rem 1rem;
|
|
2260
|
-
border-radius: 0.5rem;
|
|
2261
|
-
display: block;
|
|
2262
|
-
margin-top: 1rem;
|
|
2263
|
-
}
|
|
2264
|
-
</style>
|
|
2265
|
-
</head>
|
|
2266
|
-
<body>
|
|
2267
|
-
<div class="message">
|
|
2268
|
-
<h1>${title}</h1>
|
|
2269
|
-
<p>UI assets not found. Build @getworkbench/core first:</p>
|
|
2270
|
-
<code>bun run --filter=@getworkbench/core build</code>
|
|
2271
|
-
</div>
|
|
2272
|
-
</body>
|
|
2273
|
-
</html>`;
|
|
2274
|
-
}
|
|
2275
|
-
|
|
2276
|
-
// src/server/hono-app.ts
|
|
2277
|
-
function buildWorkbenchApp(core) {
|
|
2278
|
-
const app = new Hono2();
|
|
2279
|
-
app.use("/api/*", cors());
|
|
2280
|
-
if (core.requiresAuth()) {
|
|
2281
|
-
app.use(
|
|
2282
|
-
"*",
|
|
2283
|
-
basicAuth({
|
|
2284
|
-
username: core.options.auth.username,
|
|
2285
|
-
password: core.options.auth.password
|
|
2286
|
-
})
|
|
2287
|
-
);
|
|
2288
|
-
}
|
|
2289
|
-
app.route("/api", createApiRoutes(core));
|
|
2290
|
-
app.get("/config", (c) => c.json(core.getConfig()));
|
|
2291
|
-
app.get("/assets/:file", (c) => {
|
|
2292
|
-
const fileName = c.req.param("file");
|
|
2293
|
-
const asset = serveStaticAsset(fileName);
|
|
2294
|
-
if (asset.status === 404 || !asset.body) {
|
|
2295
|
-
return c.text("Not found", 404);
|
|
2296
|
-
}
|
|
2297
|
-
return new Response(new Uint8Array(asset.body), {
|
|
2298
|
-
status: 200,
|
|
2299
|
-
headers: { "Content-Type": asset.contentType }
|
|
2300
|
-
});
|
|
2301
|
-
});
|
|
2302
|
-
app.get("*", (c) => {
|
|
2303
|
-
const url = new URL(c.req.url);
|
|
2304
|
-
const basePath = resolveBasePath(core.options.basePath, url.pathname);
|
|
2305
|
-
const html = renderIndexHtml(basePath, core.options.title || "Workbench");
|
|
2306
|
-
return c.html(html.body);
|
|
2307
|
-
});
|
|
2308
|
-
return app;
|
|
2309
|
-
}
|
|
2310
|
-
|
|
2311
1868
|
// src/api/fetch-handler.ts
|
|
2312
1869
|
function createFetchHandler(options) {
|
|
2313
1870
|
const core = new WorkbenchCore(options);
|
|
@@ -2380,11 +1937,10 @@ export {
|
|
|
2380
1937
|
UI_DIST_PATH,
|
|
2381
1938
|
WorkbenchCore,
|
|
2382
1939
|
buildRouteTable,
|
|
2383
|
-
buildWorkbenchApp,
|
|
2384
1940
|
checkBasicAuth,
|
|
2385
1941
|
computeBasePath,
|
|
2386
|
-
createApiRoutes,
|
|
2387
1942
|
createFetchHandler,
|
|
1943
|
+
discoverQueues,
|
|
2388
1944
|
renderIndexHtml,
|
|
2389
1945
|
resolveBasePath,
|
|
2390
1946
|
serveStaticAsset
|