@getworkbench/core 0.2.1 → 0.3.1

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/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";
@@ -78,7 +168,7 @@ var QueueManager = class {
78
168
  if (!client) {
79
169
  const jobs2 = await queue.getJobs([status], 0, limit * 2);
80
170
  return jobs2.filter(
81
- (job) => job.finishedOn && job.finishedOn >= startTime && job.finishedOn <= endTime
171
+ (job) => job != null && !!job.finishedOn && job.finishedOn >= startTime && job.finishedOn <= endTime
82
172
  );
83
173
  }
84
174
  const queueKey = `bull:${queue.name}:${status}`;
@@ -101,7 +191,7 @@ var QueueManager = class {
101
191
  } catch (_error) {
102
192
  const jobs = await queue.getJobs([status], 0, limit * 2);
103
193
  return jobs.filter(
104
- (job) => job.finishedOn && job.finishedOn >= startTime && job.finishedOn <= endTime
194
+ (job) => job != null && !!job.finishedOn && job.finishedOn >= startTime && job.finishedOn <= endTime
105
195
  );
106
196
  }
107
197
  }
@@ -175,6 +265,8 @@ var QueueManager = class {
175
265
  completed: 0,
176
266
  failed: 0,
177
267
  delayed: 0,
268
+ prioritized: 0,
269
+ "waiting-children": 0,
178
270
  total: 0,
179
271
  timestamp: Date.now()
180
272
  };
@@ -186,9 +278,11 @@ var QueueManager = class {
186
278
  totals.completed += counts.completed || 0;
187
279
  totals.failed += counts.failed || 0;
188
280
  totals.delayed += counts.delayed || 0;
281
+ totals.prioritized += counts.prioritized || 0;
282
+ totals["waiting-children"] += counts["waiting-children"] || 0;
189
283
  })
190
284
  );
191
- totals.total = totals.waiting + totals.active + totals.completed + totals.failed + totals.delayed;
285
+ totals.total = totals.waiting + totals.active + totals.completed + totals.failed + totals.delayed + totals.prioritized + totals["waiting-children"];
192
286
  return totals;
193
287
  });
194
288
  }
@@ -231,6 +325,8 @@ var QueueManager = class {
231
325
  completed: counts.completed || 0,
232
326
  failed: counts.failed || 0,
233
327
  delayed: counts.delayed || 0,
328
+ prioritized: counts.prioritized || 0,
329
+ "waiting-children": counts["waiting-children"] || 0,
234
330
  paused: counts.paused || 0
235
331
  },
236
332
  isPaused
@@ -250,7 +346,7 @@ var QueueManager = class {
250
346
  let activeJobs = 0;
251
347
  let failedJobs = 0;
252
348
  for (const queue of queues) {
253
- totalJobs += queue.counts.waiting + queue.counts.active + queue.counts.delayed;
349
+ totalJobs += queue.counts.waiting + queue.counts.active + queue.counts.delayed + queue.counts.prioritized + queue.counts["waiting-children"];
254
350
  activeJobs += queue.counts.active;
255
351
  failedJobs += queue.counts.failed;
256
352
  }
@@ -619,7 +715,15 @@ var QueueManager = class {
619
715
  if (!queue) {
620
716
  return { data: [], total: 0, hasMore: false };
621
717
  }
622
- const types = status ? [status] : ["waiting", "active", "completed", "failed", "delayed"];
718
+ const types = status ? [status] : [
719
+ "active",
720
+ "waiting",
721
+ "waiting-children",
722
+ "prioritized",
723
+ "completed",
724
+ "failed",
725
+ "delayed"
726
+ ];
623
727
  const counts = await this.getCachedJobCounts(queue);
624
728
  const jobsWithState = [];
625
729
  let total = 0;
@@ -826,7 +930,7 @@ var QueueManager = class {
826
930
  const queueChecks = await Promise.all(
827
931
  queueEntries.map(async ([queueName, queue]) => {
828
932
  const counts = await this.getCachedJobCounts(queue);
829
- const hasJobs = (counts.waiting || 0) > 0 || (counts.active || 0) > 0 || (counts.completed || 0) > 0 || (counts.failed || 0) > 0 || (counts.delayed || 0) > 0;
933
+ const hasJobs = (counts.waiting || 0) > 0 || (counts.active || 0) > 0 || (counts.completed || 0) > 0 || (counts.failed || 0) > 0 || (counts.delayed || 0) > 0 || (counts.prioritized || 0) > 0 || (counts["waiting-children"] || 0) > 0;
830
934
  return { queueName, queue, hasJobs };
831
935
  })
832
936
  );
@@ -834,7 +938,15 @@ var QueueManager = class {
834
938
  if (relevantQueues.length === 0) {
835
939
  return [];
836
940
  }
837
- const types = ["waiting", "active", "completed", "failed", "delayed"];
941
+ const types = [
942
+ "active",
943
+ "waiting",
944
+ "waiting-children",
945
+ "prioritized",
946
+ "completed",
947
+ "failed",
948
+ "delayed"
949
+ ];
838
950
  const fetchLimit = Math.min(limit * 2, 50);
839
951
  const stringifiedDataCache = /* @__PURE__ */ new WeakMap();
840
952
  const queueResults = await Promise.allSettled(
@@ -914,16 +1026,23 @@ var QueueManager = class {
914
1026
  }
915
1027
  const perQueueFetch = Math.max(5, Math.ceil((limit + 10) / numQueues) + 2);
916
1028
  const allTypes = [
917
- "waiting",
918
1029
  "active",
1030
+ "waiting",
1031
+ "waiting-children",
1032
+ "prioritized",
919
1033
  "completed",
920
1034
  "failed",
921
1035
  "delayed"
922
1036
  ];
923
1037
  const results = await Promise.all(
924
1038
  queueEntries.map(async ([queueName, queue]) => {
925
- const jobs = await queue.getJobs(allTypes, 0, perQueueFetch);
926
- return jobs.map((job) => ({ job, queueName }));
1039
+ const jobArrays = await Promise.all(
1040
+ allTypes.map(async (type) => {
1041
+ const jobs = await queue.getJobs(type, 0, perQueueFetch);
1042
+ return jobs.map((job) => ({ job, queueName, state: type }));
1043
+ })
1044
+ );
1045
+ return jobArrays.flat();
927
1046
  })
928
1047
  );
929
1048
  const allJobs = results.flat();
@@ -934,15 +1053,7 @@ var QueueManager = class {
934
1053
  });
935
1054
  const jobsToConvert = allJobs.slice(start, start + limit);
936
1055
  const runInfos = await Promise.all(
937
- jobsToConvert.map(async ({ job, queueName }) => {
938
- let state = "waiting";
939
- if (job.finishedOn) {
940
- state = job.failedReason ? "failed" : "completed";
941
- } else if (job.processedOn) {
942
- state = "active";
943
- } else if (job.delay && job.delay > 0) {
944
- state = "delayed";
945
- }
1056
+ jobsToConvert.map(async ({ job, queueName, state }) => {
946
1057
  const info = await this.jobToInfo(job, "list", state);
947
1058
  return { ...info, queueName };
948
1059
  })
@@ -969,7 +1080,15 @@ var QueueManager = class {
969
1080
  return this.getLatestRuns(limit, start);
970
1081
  }
971
1082
  const queueEntries = Array.from(this.queues.entries());
972
- const types = filters?.status ? [filters.status] : ["waiting", "active", "completed", "failed", "delayed"];
1083
+ const types = filters?.status ? [filters.status] : [
1084
+ "active",
1085
+ "waiting",
1086
+ "waiting-children",
1087
+ "prioritized",
1088
+ "completed",
1089
+ "failed",
1090
+ "delayed"
1091
+ ];
973
1092
  const hasTimeRange = !!filters?.timeRange;
974
1093
  const numQueues = Math.max(queueEntries.length, 1);
975
1094
  if (queueEntries.length === 0) {
@@ -1197,7 +1316,15 @@ var QueueManager = class {
1197
1316
  */
1198
1317
  async getTagValues(field, limit = 50) {
1199
1318
  const valueMap = /* @__PURE__ */ new Map();
1200
- const types = ["waiting", "active", "completed", "failed", "delayed"];
1319
+ const types = [
1320
+ "active",
1321
+ "waiting",
1322
+ "waiting-children",
1323
+ "prioritized",
1324
+ "completed",
1325
+ "failed",
1326
+ "delayed"
1327
+ ];
1201
1328
  const queueEntries = Array.from(this.queues.entries());
1202
1329
  const queueResults = await Promise.all(
1203
1330
  queueEntries.map(async ([, queue]) => {
@@ -1494,8 +1621,9 @@ var QueueManager = class {
1494
1621
  return { queueName, jobs: waitingChildrenJobs };
1495
1622
  }
1496
1623
  const otherTypes = [
1497
- "waiting",
1498
1624
  "active",
1625
+ "waiting",
1626
+ "prioritized",
1499
1627
  "completed",
1500
1628
  "failed",
1501
1629
  "delayed"
@@ -1689,24 +1817,56 @@ var QueueManager = class {
1689
1817
  };
1690
1818
 
1691
1819
  // src/core/workbench.ts
1692
- var WorkbenchCore = class {
1820
+ var WorkbenchCore = class _WorkbenchCore {
1693
1821
  options;
1694
1822
  queueManager;
1695
- constructor(options) {
1823
+ discovery;
1824
+ constructor(options, discovery = null) {
1696
1825
  const opts = Array.isArray(options) ? { queues: options } : options;
1697
1826
  this.options = {
1698
1827
  title: "Workbench",
1699
1828
  readonly: false,
1700
1829
  ...opts
1701
1830
  };
1702
- if (!this.options.queues || this.options.queues.length === 0) {
1831
+ this.discovery = discovery;
1832
+ const explicit = this.options.queues ?? [];
1833
+ const allowEmpty = !!this.options.redis;
1834
+ if (explicit.length === 0 && !allowEmpty) {
1703
1835
  throw new Error(
1704
1836
  "Workbench requires at least one queue. Pass queues directly or provide a redis connection for auto-discovery."
1705
1837
  );
1706
1838
  }
1707
- this.queueManager = new QueueManager(
1708
- this.options.queues,
1709
- this.options.tags || []
1839
+ this.queueManager = new QueueManager(explicit, this.options.tags || []);
1840
+ }
1841
+ /**
1842
+ * Async factory: build a `WorkbenchCore` from `WorkbenchOptions`, performing
1843
+ * BullMQ queue auto-discovery via `SCAN <prefix>:*:meta` when `queues` is
1844
+ * not provided.
1845
+ *
1846
+ * - When `queues` is set explicitly, behaves like `new WorkbenchCore(opts)`.
1847
+ * - When only `redis` is set, scans the connection for queues, caps at
1848
+ * `maxQueues` (default 100) to avoid connection storms with very large
1849
+ * deployments, and constructs the core with the resulting list.
1850
+ * - When no queues are discovered, the core is constructed with an empty
1851
+ * queue map so the dashboard can render an "empty" state instead of
1852
+ * erroring out.
1853
+ */
1854
+ static async fromOptions(opts) {
1855
+ if (opts.queues?.length) {
1856
+ return new _WorkbenchCore(opts);
1857
+ }
1858
+ if (!opts.redis) {
1859
+ throw new Error(
1860
+ "WorkbenchCore.fromOptions requires either `queues` or `redis`"
1861
+ );
1862
+ }
1863
+ const prefix = opts.prefix ?? "bull";
1864
+ const cap = opts.maxQueues ?? 100;
1865
+ const all = await discoverQueues(opts.redis, prefix);
1866
+ const queues = all.slice(0, cap);
1867
+ return new _WorkbenchCore(
1868
+ { ...opts, queues },
1869
+ { total: all.length, capped: all.length > cap, cap }
1710
1870
  );
1711
1871
  }
1712
1872
  /**
@@ -1737,577 +1897,12 @@ var WorkbenchCore = class {
1737
1897
  logo: this.options.logo,
1738
1898
  readonly: this.options.readonly,
1739
1899
  queues: this.queueManager.getQueueNames(),
1740
- tags: this.queueManager.getTagFields()
1900
+ tags: this.queueManager.getTagFields(),
1901
+ discovery: this.discovery
1741
1902
  };
1742
1903
  }
1743
1904
  };
1744
1905
 
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
1906
  // src/api/fetch-handler.ts
2312
1907
  function createFetchHandler(options) {
2313
1908
  const core = new WorkbenchCore(options);
@@ -2380,11 +1975,10 @@ export {
2380
1975
  UI_DIST_PATH,
2381
1976
  WorkbenchCore,
2382
1977
  buildRouteTable,
2383
- buildWorkbenchApp,
2384
1978
  checkBasicAuth,
2385
1979
  computeBasePath,
2386
- createApiRoutes,
2387
1980
  createFetchHandler,
1981
+ discoverQueues,
2388
1982
  renderIndexHtml,
2389
1983
  resolveBasePath,
2390
1984
  serveStaticAsset