@kingsleyweb/bun-ui 0.0.0-stage → 3.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/app/docs/App.tsx +72 -0
  4. package/app/docs/api/client.ts +303 -0
  5. package/app/docs/api/contract.ts +24 -0
  6. package/app/docs/api/docs.ts +38 -0
  7. package/app/docs/api/errors.ts +144 -0
  8. package/app/docs/boot.tsx +58 -0
  9. package/app/docs/components/Badge.tsx +65 -0
  10. package/app/docs/components/Button.tsx +37 -0
  11. package/app/docs/components/Card.tsx +41 -0
  12. package/app/docs/components/ConfirmDialog.tsx +173 -0
  13. package/app/docs/components/CopyButton.tsx +68 -0
  14. package/app/docs/components/Dialog.tsx +230 -0
  15. package/app/docs/components/EmptyState.tsx +34 -0
  16. package/app/docs/components/ErrorView.tsx +70 -0
  17. package/app/docs/components/Field.tsx +93 -0
  18. package/app/docs/components/JsonEditor.tsx +151 -0
  19. package/app/docs/components/JsonView.tsx +338 -0
  20. package/app/docs/components/KeyValue.tsx +63 -0
  21. package/app/docs/components/ProblemBanner.tsx +116 -0
  22. package/app/docs/components/ScreenErrorBoundary.tsx +141 -0
  23. package/app/docs/components/Spinner.tsx +33 -0
  24. package/app/docs/components/Table.tsx +33 -0
  25. package/app/docs/components/Tabs.tsx +226 -0
  26. package/app/docs/components/classNames.ts +4 -0
  27. package/app/docs/components/components.css +691 -0
  28. package/app/docs/components/copy.ts +89 -0
  29. package/app/docs/components/fieldContext.ts +29 -0
  30. package/app/docs/components/inputValues.ts +142 -0
  31. package/app/docs/components/inputs.tsx +387 -0
  32. package/app/docs/components/jsonParse.ts +338 -0
  33. package/app/docs/components/listKeys.ts +30 -0
  34. package/app/docs/config.ts +86 -0
  35. package/app/docs/context.ts +33 -0
  36. package/app/docs/embed.ts +74 -0
  37. package/app/docs/env.d.ts +2 -0
  38. package/app/docs/format.ts +9 -0
  39. package/app/docs/hooks/useUrlTab.ts +16 -0
  40. package/app/docs/main.tsx +11 -0
  41. package/app/docs/queryClient.ts +54 -0
  42. package/app/docs/router.tsx +194 -0
  43. package/app/docs/routing.ts +194 -0
  44. package/app/docs/screens/docs/document.ts +27 -0
  45. package/app/docs/screens/docs/http/OperationView.tsx +407 -0
  46. package/app/docs/screens/docs/http/Sidebar.tsx +190 -0
  47. package/app/docs/screens/docs/http/TryItPanel.tsx +603 -0
  48. package/app/docs/screens/docs/http/http.css +405 -0
  49. package/app/docs/screens/docs/http/index.tsx +395 -0
  50. package/app/docs/screens/docs/http/model.ts +604 -0
  51. package/app/docs/screens/docs/http/tryIt.ts +455 -0
  52. package/app/docs/screens/docs/permissions.ts +6 -0
  53. package/app/docs/screens/docs/schema/Prose.tsx +43 -0
  54. package/app/docs/screens/docs/schema/SchemaTree.tsx +319 -0
  55. package/app/docs/screens/docs/schema/index.ts +30 -0
  56. package/app/docs/screens/docs/schema/resolve.ts +466 -0
  57. package/app/docs/screens/docs/schema/schema.css +137 -0
  58. package/app/docs/screens/docs/schema/skeleton.ts +92 -0
  59. package/app/docs/screens/docs/viewer.tsx +154 -0
  60. package/app/docs/screens/docs/ws/InlineText.tsx +25 -0
  61. package/app/docs/screens/docs/ws/PayloadSchema.tsx +39 -0
  62. package/app/docs/screens/docs/ws/format.ts +82 -0
  63. package/app/docs/screens/docs/ws/index.tsx +286 -0
  64. package/app/docs/screens/docs/ws/items.tsx +786 -0
  65. package/app/docs/screens/docs/ws/model.ts +1178 -0
  66. package/app/docs/screens/docs/ws/panels.tsx +440 -0
  67. package/app/docs/screens/docs/ws/ws.css +231 -0
  68. package/app/docs/shortcuts.ts +94 -0
  69. package/app/docs/styles/base.css +847 -0
  70. package/app/docs/styles/tokens.css +143 -0
  71. package/app/docs/theme.ts +56 -0
  72. package/app/kit/unauthorized.ts +222 -0
  73. package/dist/docs/assets/main-0rchxtv5.js +18 -0
  74. package/dist/docs/assets/main-0rchxtv5.js.map +99 -0
  75. package/dist/docs/assets/main-cqgyvs86.css +1 -0
  76. package/dist/docs/manifest.json +26 -0
  77. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js +6 -0
  78. package/dist/jobs/assets/AddFlowDialog-h6xkgx9b.js.map +11 -0
  79. package/dist/jobs/assets/AddFlowDialog-n7gtc84v.css +1 -0
  80. package/dist/jobs/assets/added-cbwsxv3e.js +4 -0
  81. package/dist/jobs/assets/added-cbwsxv3e.js.map +11 -0
  82. package/dist/jobs/assets/bodies-yyw5hmrp.js +6 -0
  83. package/dist/jobs/assets/bodies-yyw5hmrp.js.map +10 -0
  84. package/dist/jobs/assets/channel-tgkvqm8y.js +4 -0
  85. package/dist/jobs/assets/channel-tgkvqm8y.js.map +10 -0
  86. package/dist/jobs/assets/channelChecks-s4yz4yhw.js +4 -0
  87. package/dist/jobs/assets/channelChecks-s4yz4yhw.js.map +10 -0
  88. package/dist/jobs/assets/index-02427smp.css +1 -0
  89. package/dist/jobs/assets/index-09z2cjft.js +4 -0
  90. package/dist/jobs/assets/index-09z2cjft.js.map +16 -0
  91. package/dist/jobs/assets/index-18bn0gvy.css +1 -0
  92. package/dist/jobs/assets/index-47pxmyxn.js +4 -0
  93. package/dist/jobs/assets/index-47pxmyxn.js.map +10 -0
  94. package/dist/jobs/assets/index-4e55z986.js +4 -0
  95. package/dist/jobs/assets/index-4e55z986.js.map +11 -0
  96. package/dist/jobs/assets/index-5c933424.js +4 -0
  97. package/dist/jobs/assets/index-5c933424.js.map +18 -0
  98. package/dist/jobs/assets/index-5kbyqspm.css +1 -0
  99. package/dist/jobs/assets/index-5ng37hk6.css +1 -0
  100. package/dist/jobs/assets/index-5w8w7fvc.css +1 -0
  101. package/dist/jobs/assets/index-71qs5qyx.css +1 -0
  102. package/dist/jobs/assets/index-btpg4r8b.css +1 -0
  103. package/dist/jobs/assets/index-d5r5n0mm.js +4 -0
  104. package/dist/jobs/assets/index-d5r5n0mm.js.map +23 -0
  105. package/dist/jobs/assets/index-e0yy2skq.css +1 -0
  106. package/dist/jobs/assets/index-hs60b25v.js +10 -0
  107. package/dist/jobs/assets/index-hs60b25v.js.map +62 -0
  108. package/dist/jobs/assets/index-k2smrr70.js +4 -0
  109. package/dist/jobs/assets/index-k2smrr70.js.map +9 -0
  110. package/dist/jobs/assets/index-q24jw3w9.js +4 -0
  111. package/dist/jobs/assets/index-q24jw3w9.js.map +32 -0
  112. package/dist/jobs/assets/index-qfb80xen.css +1 -0
  113. package/dist/jobs/assets/index-rtynx6wj.js +4 -0
  114. package/dist/jobs/assets/index-rtynx6wj.js.map +10 -0
  115. package/dist/jobs/assets/live-6k7s6358.js +4 -0
  116. package/dist/jobs/assets/live-6k7s6358.js.map +10 -0
  117. package/dist/jobs/assets/main-0jmeh6mq.js +6 -0
  118. package/dist/jobs/assets/main-0jmeh6mq.js.map +12 -0
  119. package/dist/jobs/assets/main-3yj6bv90.js +5 -0
  120. package/dist/jobs/assets/main-3yj6bv90.js.map +10 -0
  121. package/dist/jobs/assets/main-4g69fn3x.js +5 -0
  122. package/dist/jobs/assets/main-4g69fn3x.js.map +10 -0
  123. package/dist/jobs/assets/main-4gte3eqq.js +5 -0
  124. package/dist/jobs/assets/main-4gte3eqq.js.map +10 -0
  125. package/dist/jobs/assets/main-4j1fcevh.js +5 -0
  126. package/dist/jobs/assets/main-4j1fcevh.js.map +10 -0
  127. package/dist/jobs/assets/main-54qwkz28.css +1 -0
  128. package/dist/jobs/assets/main-5grjvsy7.js +5 -0
  129. package/dist/jobs/assets/main-5grjvsy7.js.map +11 -0
  130. package/dist/jobs/assets/main-5zbx60gq.js +5 -0
  131. package/dist/jobs/assets/main-5zbx60gq.js.map +11 -0
  132. package/dist/jobs/assets/main-9prgjhg4.js +5 -0
  133. package/dist/jobs/assets/main-9prgjhg4.js.map +10 -0
  134. package/dist/jobs/assets/main-a250ffk7.js +17 -0
  135. package/dist/jobs/assets/main-a250ffk7.js.map +115 -0
  136. package/dist/jobs/assets/main-d6ge7w08.js +5 -0
  137. package/dist/jobs/assets/main-d6ge7w08.js.map +10 -0
  138. package/dist/jobs/assets/main-gngzht43.js +5 -0
  139. package/dist/jobs/assets/main-gngzht43.js.map +10 -0
  140. package/dist/jobs/assets/main-hg0hrg8j.js +5 -0
  141. package/dist/jobs/assets/main-hg0hrg8j.js.map +10 -0
  142. package/dist/jobs/assets/main-hm1ng3yg.js +5 -0
  143. package/dist/jobs/assets/main-hm1ng3yg.js.map +12 -0
  144. package/dist/jobs/assets/main-jkvd92z9.js +5 -0
  145. package/dist/jobs/assets/main-jkvd92z9.js.map +10 -0
  146. package/dist/jobs/assets/main-jv5xqd13.js +5 -0
  147. package/dist/jobs/assets/main-jv5xqd13.js.map +21 -0
  148. package/dist/jobs/assets/main-jxre7w93.js +5 -0
  149. package/dist/jobs/assets/main-jxre7w93.js.map +23 -0
  150. package/dist/jobs/assets/main-kwmqkjfx.js +5 -0
  151. package/dist/jobs/assets/main-kwmqkjfx.js.map +11 -0
  152. package/dist/jobs/assets/main-n9cf6bxm.js +5 -0
  153. package/dist/jobs/assets/main-n9cf6bxm.js.map +11 -0
  154. package/dist/jobs/assets/main-pegp0ygb.js +5 -0
  155. package/dist/jobs/assets/main-pegp0ygb.js.map +10 -0
  156. package/dist/jobs/assets/main-qr573853.js +5 -0
  157. package/dist/jobs/assets/main-qr573853.js.map +12 -0
  158. package/dist/jobs/assets/main-rq97rdr4.js +4 -0
  159. package/dist/jobs/assets/main-rq97rdr4.js.map +22 -0
  160. package/dist/jobs/assets/main-szjdmydc.js +5 -0
  161. package/dist/jobs/assets/main-szjdmydc.js.map +13 -0
  162. package/dist/jobs/assets/main-tnbhrm9n.js +7 -0
  163. package/dist/jobs/assets/main-tnbhrm9n.js.map +11 -0
  164. package/dist/jobs/assets/main-ts19j4n0.js +5 -0
  165. package/dist/jobs/assets/main-ts19j4n0.js.map +25 -0
  166. package/dist/jobs/assets/main-w93x1dzs.js +5 -0
  167. package/dist/jobs/assets/main-w93x1dzs.js.map +10 -0
  168. package/dist/jobs/assets/sections-0ksdxs8m.css +1 -0
  169. package/dist/jobs/assets/sections-0y7w4fb4.js +4 -0
  170. package/dist/jobs/assets/sections-0y7w4fb4.js.map +10 -0
  171. package/dist/jobs/assets/sse-mn0t0fh7.js +5 -0
  172. package/dist/jobs/assets/sse-mn0t0fh7.js.map +11 -0
  173. package/dist/jobs/assets/sseChecks-t7tcm7ra.js +4 -0
  174. package/dist/jobs/assets/sseChecks-t7tcm7ra.js.map +10 -0
  175. package/dist/jobs/assets/validation-ysjfy072.js +4 -0
  176. package/dist/jobs/assets/validation-ysjfy072.js.map +9 -0
  177. package/dist/jobs/manifest.json +511 -0
  178. package/dts/docs/assets.d.ts +144 -0
  179. package/dts/docs/assets.d.ts.map +1 -0
  180. package/dts/docs/browser/config.d.ts +61 -0
  181. package/dts/docs/browser/config.d.ts.map +1 -0
  182. package/dts/docs/browser/index.d.ts +12 -0
  183. package/dts/docs/browser/index.d.ts.map +1 -0
  184. package/dts/docs/docsUi.d.ts +42 -0
  185. package/dts/docs/docsUi.d.ts.map +1 -0
  186. package/dts/docs/hosted.d.ts +22 -0
  187. package/dts/docs/hosted.d.ts.map +1 -0
  188. package/dts/docs/index.d.ts +18 -0
  189. package/dts/docs/index.d.ts.map +1 -0
  190. package/dts/docs/shell.d.ts +57 -0
  191. package/dts/docs/shell.d.ts.map +1 -0
  192. package/dts/docs/types.d.ts +137 -0
  193. package/dts/docs/types.d.ts.map +1 -0
  194. package/dts/index.d.ts +21 -0
  195. package/dts/index.d.ts.map +1 -0
  196. package/dts/jobs/assets.d.ts +157 -0
  197. package/dts/jobs/assets.d.ts.map +1 -0
  198. package/dts/jobs/browser/config.d.ts +67 -0
  199. package/dts/jobs/browser/config.d.ts.map +1 -0
  200. package/dts/jobs/browser/index.d.ts +10 -0
  201. package/dts/jobs/browser/index.d.ts.map +1 -0
  202. package/dts/jobs/browser/unauthorized.d.ts +9 -0
  203. package/dts/jobs/browser/unauthorized.d.ts.map +1 -0
  204. package/dts/jobs/index.d.ts +24 -0
  205. package/dts/jobs/index.d.ts.map +1 -0
  206. package/dts/jobs/jobsUi.d.ts +63 -0
  207. package/dts/jobs/jobsUi.d.ts.map +1 -0
  208. package/dts/jobs/shell.d.ts +77 -0
  209. package/dts/jobs/shell.d.ts.map +1 -0
  210. package/dts/jobs/types.d.ts +146 -0
  211. package/dts/jobs/types.d.ts.map +1 -0
  212. package/dts/kit/browser/shared.d.ts +10 -0
  213. package/dts/kit/browser/shared.d.ts.map +1 -0
  214. package/dts/kit/browser/unauthorized.d.ts +74 -0
  215. package/dts/kit/browser/unauthorized.d.ts.map +1 -0
  216. package/dts/kit/unauthorized.d.ts +47 -0
  217. package/dts/kit/unauthorized.d.ts.map +1 -0
  218. package/guides/docs.md +170 -0
  219. package/guides/jobs.md +1120 -0
  220. package/lib/docs/assets.ts +427 -0
  221. package/lib/docs/browser/config.ts +66 -0
  222. package/lib/docs/browser/index.ts +24 -0
  223. package/lib/docs/docsUi.ts +623 -0
  224. package/lib/docs/hosted.ts +31 -0
  225. package/lib/docs/index.ts +37 -0
  226. package/lib/docs/shell.ts +156 -0
  227. package/lib/docs/types.ts +149 -0
  228. package/lib/index.ts +27 -0
  229. package/lib/jobs/assets.ts +448 -0
  230. package/lib/jobs/browser/config.ts +65 -0
  231. package/lib/jobs/browser/index.ts +9 -0
  232. package/lib/jobs/browser/unauthorized.ts +11 -0
  233. package/lib/jobs/index.ts +41 -0
  234. package/lib/jobs/jobsUi.ts +709 -0
  235. package/lib/jobs/shell.ts +205 -0
  236. package/lib/jobs/types.ts +162 -0
  237. package/lib/kit/browser/shared.ts +11 -0
  238. package/lib/kit/browser/unauthorized.ts +114 -0
  239. package/lib/kit/unauthorized.ts +222 -0
  240. package/package.json +198 -4
@@ -0,0 +1,25 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["app/jobs/hooks/useUrlTab.ts", "app/jobs/components/Tabs.tsx", "app/jobs/screens/queues/jobFilters.ts", "app/jobs/screens/queues/jobWalk.ts", "app/jobs/screens/queues/queueFormat.ts", "app/jobs/screens/queues/JobsTable.tsx", "app/jobs/screens/workers/live.ts", "app/jobs/screens/workers/actions/explain.ts", "app/jobs/screens/workers/actions/gating.ts", "app/jobs/screens/workers/actions/StopWorkerDialog.tsx", "app/jobs/screens/workers/actions/settingHints.ts", "app/jobs/screens/workers/actions/WorkerConfigDialog.tsx", "app/jobs/screens/workers/actions/index.tsx", "app/jobs/screens/workers/summon.ts", "app/jobs/screens/workers/target.ts", "app/jobs/screens/workers/WorkerTable.tsx"],
4
+ "sourcesContent": [
5
+ "import { useRouter } from \"../routing\";\n\n/**\n * The selected tab of a `UrlTabs`, read from the URL: the value of\n * `param` when it is one of `tabs`, else `defaultValue`. Use it in the\n * screen to pick the data for the panel.\n */\nexport function useUrlTab<V extends string>(\n param: string,\n tabs: readonly { value: V }[],\n defaultValue: V,\n): V {\n const { location } = useRouter();\n const raw = new URLSearchParams(location.search).get(param);\n return tabs.find((tab) => tab.value === raw)?.value ?? defaultValue;\n}\n",
6
+ "import type { KeyboardEvent, ReactNode } from \"react\";\nimport { useCallback, useId, useRef } from \"react\";\nimport { useUrlTab } from \"../hooks/useUrlTab\";\nimport { useRouter } from \"../routing\";\nimport { cx } from \"./classNames\";\n\n/** One tab of {@link Tabs}. */\nexport interface TabItem<V extends string> {\n /** The tab's value, e.g. a job state. */\n value: V;\n /** The text on the tab. */\n label: ReactNode;\n /** A count shown as a badge beside the label; omitted when `undefined`. */\n count?: number;\n /** Not selectable. */\n disabled?: boolean;\n /** A tooltip on the tab (its `title`), e.g. what a state's label means. Omitted when `undefined`. */\n title?: string;\n}\n\n/** Props of {@link Tabs}. */\nexport interface TabsProps<V extends string> {\n /** The tabs, in order. */\n tabs: readonly TabItem<V>[];\n /** The selected tab's value. */\n value: V;\n /** Called with the tab the user picked. */\n onChange: (value: V) => void;\n /** Accessible name of the tab list, e.g. \"Job states\". */\n label: string;\n /** The selected tab's content, rendered in the single tab panel. Omit to render only the tab list. */\n children?: ReactNode;\n /** `\"auto\"` selects on arrow-key focus; `\"manual\"` waits for Enter/Space. Defaults to `\"auto\"`. */\n activation?: \"auto\" | \"manual\";\n /** Extra class names on the wrapper. */\n className?: string;\n}\n\n/** Formats a tab count compactly: `1234` → `1.2K`. */\nconst countFormat = new Intl.NumberFormat(undefined, { notation: \"compact\" });\n\n/**\n * Controlled tabs following the WAI-ARIA tabs pattern: `tablist`/`tab`/\n * `tabpanel`, one tab in the tab order (roving `tabIndex`), Left/Right to\n * move (wrapping), Home/End to jump. Disabled tabs are skipped.\n */\nexport function Tabs<V extends string>({\n tabs,\n value,\n onChange,\n label,\n children,\n activation = \"auto\",\n className,\n}: TabsProps<V>) {\n const baseId = useId();\n const listRef = useRef<HTMLDivElement>(null);\n const tabId = (index: number) => `${baseId}-tab-${index}`;\n const panelId = `${baseId}-panel`;\n const selectedIndex = tabs.findIndex((tab) => tab.value === value);\n\n const onKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {\n const enabled = tabs\n .map((tab, index) => ({ tab, index }))\n .filter(({ tab }) => !tab.disabled);\n if (enabled.length === 0) {\n return;\n }\n const current = enabled.findIndex(\n ({ index }) => tabId(index) === event.currentTarget.id,\n );\n let next: number | null = null;\n switch (event.key) {\n case \"ArrowRight\":\n next = (current + 1) % enabled.length;\n break;\n case \"ArrowLeft\":\n next = (current - 1 + enabled.length) % enabled.length;\n break;\n case \"Home\":\n next = 0;\n break;\n case \"End\":\n next = enabled.length - 1;\n break;\n default:\n return;\n }\n event.preventDefault();\n const target = enabled[next];\n listRef.current?.ownerDocument.getElementById(tabId(target.index))?.focus();\n if (activation === \"auto\") {\n onChange(target.tab.value);\n }\n };\n\n return (\n <div className={cx(\"tabs\", className)}>\n <div\n ref={listRef}\n className=\"tabs-list\"\n role=\"tablist\"\n aria-label={label}\n >\n {tabs.map((tab, index) => {\n const selected = index === selectedIndex;\n // With nothing selected, the first enabled tab takes the tab stop.\n const tabStop =\n selected ||\n (selectedIndex === -1 &&\n index === tabs.findIndex((item) => !item.disabled));\n return (\n <button\n key={tab.value}\n id={tabId(index)}\n type=\"button\"\n role=\"tab\"\n className=\"tab\"\n aria-selected={selected}\n aria-controls={children !== undefined ? panelId : undefined}\n tabIndex={tabStop ? 0 : -1}\n disabled={tab.disabled}\n title={tab.title}\n onClick={() => onChange(tab.value)}\n onKeyDown={onKeyDown}\n >\n <span className=\"tab-label\">{tab.label}</span>\n {tab.count !== undefined && (\n <span\n className=\"tab-count\"\n title={String(tab.count)}\n >\n {countFormat.format(tab.count)}\n </span>\n )}\n </button>\n );\n })}\n </div>\n {children !== undefined && (\n <div\n id={panelId}\n className=\"tabs-panel\"\n role=\"tabpanel\"\n aria-labelledby={\n selectedIndex >= 0 ? tabId(selectedIndex) : undefined\n }\n tabIndex={0}\n >\n {children}\n </div>\n )}\n </div>\n );\n}\n\n/** Props of {@link UrlTabs}. */\nexport interface UrlTabsProps<V extends string> extends Omit<\n TabsProps<V>,\n \"value\" | \"onChange\"\n> {\n /** The query parameter holding the tab, e.g. `\"state\"`. */\n param: string;\n /** The tab when the parameter is absent or not one of `tabs`; selecting it removes the parameter. */\n defaultValue: V;\n /** Query parameters to remove when the tab changes, e.g. `[\"offset\"]` so paging restarts. */\n resetParams?: readonly string[];\n /** Called after the URL changed. */\n onChange?: (value: V) => void;\n}\n\n/**\n * {@link Tabs} whose selection lives in the query string (`?state=failed`),\n * so it survives reload and can be linked. Changes replace the history entry\n * (the router's `navigate(…, { replace: true })`), keep every other\n * parameter, and drop the `resetParams`.\n */\nexport function UrlTabs<V extends string>({\n param,\n defaultValue,\n resetParams = [],\n onChange,\n tabs,\n ...rest\n}: UrlTabsProps<V>) {\n const { location, navigate } = useRouter();\n const value = useUrlTab(param, tabs, defaultValue);\n const select = useCallback(\n (next: V) => {\n const params = new URLSearchParams(location.search);\n if (next === defaultValue) {\n params.delete(param);\n } else {\n params.set(param, next);\n }\n if (next !== value) {\n for (const key of resetParams) {\n params.delete(key);\n }\n }\n const query = params.toString();\n navigate(`${location.path ?? \"/\"}${query ? `?${query}` : \"\"}`, {\n replace: true,\n });\n onChange?.(next);\n },\n [\n location.search,\n location.path,\n defaultValue,\n param,\n value,\n resetParams,\n navigate,\n onChange,\n ],\n );\n return (\n <Tabs\n {...rest}\n tabs={tabs}\n value={value}\n onChange={select}\n />\n );\n}\n",
7
+ "import type { JobState } from \"../../api/contract\";\nimport type { JobListFilters } from \"../../api/queues\";\nimport { clampLimit, intParam, splitList } from \"./urlState\";\n\n/** The state tab that lists every state. */\nexport const ALL_STATES = \"all\";\n\n/** A state tab's value. */\nexport type StateTab = JobState | typeof ALL_STATES;\n\n/** Longest failure message shown in a row; the whole one is its tooltip. */\nexport const FAILURE_PREVIEW_CHARS = 80;\n\n/** Shortens text to `max` characters with an ellipsis. */\nexport function truncate(text: string, max: number): string {\n return text.length > max ? `${text.slice(0, max - 1)}…` : text;\n}\n\n/**\n * Reads the jobs-list filters from the URL, clamped to the API's limits,\n * plus `cursor` where the reader is walking the list rather than jumping\n * about in it.\n *\n * The cursor is the one thing here that is **not** a URL parameter: it is\n * opaque and up to 2 kB, and walking back needs the whole trail of them, so\n * the walk lives in the screen's own state and the URL keeps saying where\n * that walk started.\n */\nexport function readJobFilters(\n params: URLSearchParams,\n tab: StateTab,\n limits: { defaultPageSize: number; maxPageSize: number },\n cursor?: string,\n): JobListFilters {\n return {\n state: tab === ALL_STATES ? null : tab,\n offset: intParam(params, \"offset\", 0),\n ...(cursor === undefined ? {} : { cursor }),\n limit: clampLimit(\n intParam(params, \"limit\", limits.defaultPageSize),\n limits.maxPageSize,\n ),\n // Newest first by default: the most recent job is what a person looks\n // for. Only the exception, `order=asc`, is written into the URL.\n //\n // **Do not drop this override for the API's own default (`asc`).**\n // Paging a list that changes under you can lose rows, and the two\n // directions differ in whether anyone can tell. Measured by the offset\n // probe: `desc` lost no rows at all, and a moved window showed up as a\n // repeated row rather than a missing one; `asc` against a draining queue\n // lost rows in more than half its trials, and most of those losses were\n // invisible to the client. Asking for a total made some of them visible\n // and not others, which is why a total is not a safety net — a shifted\n // window can leave every field of the response unchanged. So this line\n // keeps a reader on the direction whose losses leave a trace.\n //\n // The list walks by cursor now, and a walk loses none of that in either\n // direction. It does not retire this line: a reader still *jumps* by\n // offset, every walk starts from an offset page, and the Active tab\n // cannot be walked at all. So the direction whose losses leave a trace is\n // still the one to start on.\n //\n // Deliberately no figures: a count in a comment is a count nothing\n // compares against its source, and the first version of this one carried\n // a trial total that was already wrong when it was written.\n order: params.get(\"order\") === \"asc\" ? \"asc\" : \"desc\",\n names: splitList(params.get(\"name\") ?? \"\"),\n search: params.get(\"search\") ?? \"\",\n total: params.get(\"total\") === \"1\",\n };\n}\n",
8
+ "import { useCallback, useState } from \"react\";\n\n/**\n * Walking the jobs list by cursor.\n *\n * `GET /queues/:queue/jobs` offers two ways to page, and they answer\n * different questions. An **offset** counts matching jobs, which is how a\n * reader jumps to page N — and how a draining queue loses rows out of the\n * window without anyone noticing. A **cursor** names the job the last page\n * ended at, so nothing shifts under it; the API mints one on every page,\n * including an offset page, so the two mix: jump to where you want to be,\n * then walk on from there without losing a row.\n *\n * Three facts decide the shape of the state below.\n *\n * - **A cursor only goes forward.** The page after one is `page.next`;\n * there is no `page.previous`. So walking back means remembering the cursor\n * that produced each page — the trail here.\n * - **A cursor is opaque and up to 2 kB.** It cannot go in the URL (a trail\n * of them certainly cannot), and nothing may build, parse or compare one.\n * So the URL keeps saying where the walk *started*, as an offset, and a\n * reload re-samples from there rather than resuming a walk it cannot carry.\n * - **A cursor is bound to its walk** — the queue, the states, the sort and\n * the order. One belonging to another walk is a 400, not a silent restart.\n * So the trail is stored with the key of the walk it belongs to, and a key\n * that no longer matches makes it empty: a stale cursor cannot be sent,\n * because it is not there to send.\n */\n\n/** A walk: the cursors that produced each page past the one it started on. */\nexport interface WalkState {\n /** The walk this trail belongs to; a different one makes the trail empty. */\n key: string;\n /**\n * The cursor sent for each page walked past the first, oldest first. Empty\n * on the page the walk started from, which is an offset page.\n */\n trail: readonly string[];\n}\n\n/** A walk that has not left its first page. */\nexport function emptyWalk(key: string): WalkState {\n return { key, trail: [] };\n}\n\n/**\n * `state` if it still belongs to `key`, an empty walk otherwise. Derived, not\n * stored: the walk a changed filter invalidated must be gone on the render\n * that changes it, not one effect later — by then the request has gone out.\n */\nexport function walkFor(state: WalkState, key: string): WalkState {\n return state.key === key ? state : emptyWalk(key);\n}\n\n/** The walk one page on, with `cursor` (a page's `page.next`) as its new page. */\nexport function walkForward(state: WalkState, cursor: string): WalkState {\n return { key: state.key, trail: [...state.trail, cursor] };\n}\n\n/** The walk one page back; already at its first page, unchanged. */\nexport function walkBack(state: WalkState): WalkState {\n return state.trail.length === 0\n ? state\n : { key: state.key, trail: state.trail.slice(0, -1) };\n}\n\n/** The cursor of the page a walk is on, or `undefined` on the page it started from. */\nexport function walkCursor(state: WalkState): string | undefined {\n return state.trail.at(-1);\n}\n\n/** Walking the jobs list, as a screen uses it. */\nexport interface JobWalk {\n /**\n * The cursor that produced the page shown, or `undefined` on the page the\n * walk started from (an offset page). Pass it to `readJobFilters`.\n */\n cursor: string | undefined;\n /** Whether a page this walk has already shown can be returned to. */\n canPrev: boolean;\n /** Walks on, with the shown page's `page.next`. */\n forward: (cursor: string) => void;\n /** Walks back one page. */\n back: () => void;\n /** Abandons the walk, for a move that jumps (a page number, an offset). */\n reset: () => void;\n}\n\n/**\n * The walk of one jobs list, reset whenever `key` changes.\n *\n * `key` must name every parameter the cursor is bound to — queue, states,\n * sort, order — and, in practice, every filter whose change should restart\n * paging. It must *not* name the page size: the cursor that produced the page\n * shown is re-sent with the new size, so the page keeps its first row, which\n * is what the size control means everywhere else in the app.\n */\nexport function useJobWalk(key: string): JobWalk {\n const [stored, setStored] = useState<WalkState>(() => emptyWalk(key));\n const state = walkFor(stored, key);\n const forward = useCallback(\n (cursor: string) =>\n setStored((previous) => walkForward(walkFor(previous, key), cursor)),\n [key],\n );\n const back = useCallback(\n () => setStored((previous) => walkBack(walkFor(previous, key))),\n [key],\n );\n const reset = useCallback(\n () =>\n setStored((previous) =>\n walkFor(previous, key).trail.length === 0 ? previous : emptyWalk(key),\n ),\n [key],\n );\n return {\n cursor: walkCursor(state),\n canPrev: state.trail.length > 0,\n forward,\n back,\n reset,\n };\n}\n",
9
+ "import type { RepeatableDto } from \"../../api/types\";\nimport { formatNumber } from \"../../format\";\nimport { formatMs } from \"./duration\";\n\n/** Most ids listed in a result toast; the rest are summarised. */\nexport const MAX_LISTED_IDS = 10;\n\n/** Lists ids for a toast, capped at {@link MAX_LISTED_IDS}. */\nexport function listIds(ids: readonly string[]): string {\n const shown = ids.slice(0, MAX_LISTED_IDS).join(\", \");\n const rest = ids.length - MAX_LISTED_IDS;\n return rest > 0 ? `${shown} and ${formatNumber(rest)} more` : shown;\n}\n\n/** A series' schedule as text: `cron (tz)` or `every 5m`. */\nexport function describeSchedule(series: RepeatableDto): string {\n if (series.cron) {\n return series.tz ? `${series.cron} (${series.tz})` : series.cron;\n }\n if (series.every !== undefined) {\n return `every ${formatMs(series.every)}`;\n }\n return \"—\";\n}\n",
10
+ "import type { FormEvent, ReactNode } from \"react\";\nimport type { JobListFilters } from \"../../api/queues\";\nimport type {\n BulkPromoteResultDto,\n BulkRemoveResultDto,\n BulkRetryResultDto,\n JobCountsDto,\n JobDto,\n JobWorkerDto,\n} from \"../../api/types\";\nimport type { StateTab } from \"./jobFilters\";\nimport { keepPreviousData, useQuery } from \"@tanstack/react-query\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { JOB_STATES } from \"../../api/contract\";\nimport {\n bulkPromote,\n bulkRemove,\n bulkRetry,\n jobPath,\n listJobs,\n mutationInvalidations,\n queueKeys,\n sortsByCreation,\n} from \"../../api/queues\";\nimport { workerPath } from \"../../api/workers\";\nimport { Button } from \"../../components/Button\";\nimport { ConfirmDialog } from \"../../components/ConfirmDialog\";\nimport { CopyButton } from \"../../components/CopyButton\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { ErrorView } from \"../../components/ErrorView\";\nimport { Checkbox, Select, TextInput } from \"../../components/inputs\";\nimport { Pager } from \"../../components/Pager\";\nimport { RelativeTime } from \"../../components/RelativeTime\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { StateBadge } from \"../../components/StateBadge\";\nimport { Table } from \"../../components/Table\";\nimport { UrlTabs } from \"../../components/Tabs\";\nimport { useToast } from \"../../components/toast\";\nimport { useApiClient } from \"../../context\";\nimport {\n displayText,\n formatNumber,\n plural,\n STATE_HINTS,\n STATE_LABELS,\n} from \"../../format\";\nimport { useApiMutation } from \"../../hooks/useApiMutation\";\nimport { useUrlTab } from \"../../hooks/useUrlTab\";\nimport { SEARCH_SHORTCUT } from \"../../layout/shortcuts\";\nimport { useMeta } from \"../../meta/hooks\";\nimport { Link } from \"../../router\";\nimport { useWorkerPagesRouted } from \"../workers/routed\";\nimport { useCanMutate } from \"./gating\";\nimport {\n ALL_STATES,\n FAILURE_PREVIEW_CHARS,\n readJobFilters,\n truncate,\n} from \"./jobFilters\";\nimport { useJobWalk } from \"./jobWalk\";\nimport { useRefreshInterval } from \"./live\";\nimport { listIds } from \"./queueFormat\";\nimport { splitList, useUrlParams } from \"./urlState\";\n\n/** Query parameters that restart paging when they change. */\nconst PAGING_PARAMS = [\"offset\"] as const;\n\n/**\n * What walking this tab by cursor does and does not show, put on Previous and\n * Next while they walk.\n *\n * Both sentences are true of every tab; the Waiting one names promotion\n * because that is the tab a promoted job **arrives** on, and the queue\n * screen's own Promote selected is what puts it there. A job that leaves the\n * states being walked is missed by every paging scheme there is — nothing can\n * show a row that is no longer there — but a job that *joins* behind the point\n * a walk has reached is missed silently, which is the one thing the cursor\n * costs that the offset did not.\n */\nfunction walkHint(tab: StateTab): string {\n return tab === \"waiting\"\n ? \"Walks from where this page ended, so no waiting job is skipped by the queue draining. A job promoted or retried into the queue while you page can land behind this point, and is then not on any page of this walk.\"\n : \"Walks from where this page ended, so no job is skipped by the list changing under you. A job that joins these states behind the point you have reached is not on any page of this walk.\";\n}\n\n/** Props of {@link JobsTable}. */\nexport interface JobsTableProps {\n /** The queue listed. */\n queue: string;\n /** Counts per state for the tabs, when loaded. */\n counts: JobCountsDto | undefined;\n}\n\n/**\n * The queue's jobs: state tabs with counts, filters, the table, paging and\n * bulk actions. Every filter lives in the URL; changing the tab or a filter\n * restarts paging and clears the selection.\n */\nexport function JobsTable({ queue, counts }: JobsTableProps) {\n const api = useApiClient();\n const meta = useMeta();\n const [params, update] = useUrlParams();\n const tabs = [\n {\n value: ALL_STATES as StateTab,\n label: \"All\",\n count: counts\n ? JOB_STATES.reduce((sum, state) => sum + counts[state], 0)\n : undefined,\n },\n ...JOB_STATES.map((state) => ({\n value: state,\n label: STATE_LABELS[state],\n title: STATE_HINTS[state],\n count: counts?.[state],\n })),\n ];\n const tab = useUrlTab<StateTab>(\"state\", tabs, ALL_STATES);\n const createdOrder = meta.features.addedByState;\n const start = readJobFilters(params, tab, meta.limits);\n // Every parameter a cursor is bound to (the queue, the states, the sort and\n // the order), plus the filters and the offset the walk started from: change\n // one and this is a different walk, so its cursors are gone before the next\n // request goes out. The page size is deliberately not here — see\n // {@link useJobWalk}.\n const walk = useJobWalk(\n JSON.stringify([\n queue,\n start.state,\n start.offset,\n start.order,\n start.names,\n start.search,\n start.total,\n sortsByCreation(start, createdOrder),\n ]),\n );\n const filters = readJobFilters(params, tab, meta.limits, walk.cursor);\n const refetchInterval = useRefreshInterval(\"jobs\");\n\n const jobs = useQuery({\n queryKey: queueKeys.jobs(queue, filters),\n // Newest added first on every tab where the backend can sort by\n // `createdAt`; elsewhere each tab's natural order (the flag's absence\n // means the API refuses `sort=createdAt`).\n queryFn: ({ signal }) =>\n listJobs(api, queue, filters, signal, { createdOrder }),\n refetchInterval,\n placeholderData: keepPreviousData,\n });\n\n // The selection belongs to one filter set; a new set starts empty.\n const selectionKey = JSON.stringify([\n filters.state,\n filters.order,\n filters.names,\n filters.search,\n ]);\n\n return (\n <section\n className=\"jobs-section\"\n aria-label=\"Jobs\"\n >\n <UrlTabs\n tabs={tabs}\n param=\"state\"\n defaultValue={ALL_STATES}\n resetParams={PAGING_PARAMS}\n label=\"Job states\"\n />\n <JobFiltersBar\n key={`${params.get(\"name\") ?? \"\"}|${filters.search}`}\n filters={filters}\n searchIndexed={meta.features.search}\n createdOrder={meta.features.addedByState}\n onApply={(patch) => update({ ...patch, offset: null })}\n onOrder={(order) =>\n update({ order: order === \"asc\" ? \"asc\" : null, offset: null })\n }\n onTotal={(total) => update({ total: total ? \"1\" : null })}\n />\n {jobs.isPending ? (\n <Spinner\n label=\"Loading jobs\"\n showLabel\n />\n ) : jobs.isError ? (\n <ErrorView\n error={jobs.error}\n title=\"Could not load jobs\"\n onRetry={() => void jobs.refetch()}\n />\n ) : (\n <>\n <JobRows\n key={selectionKey}\n queue={queue}\n items={jobs.data.items}\n />\n <Pager\n label=\"Job pages\"\n // Where the API says the page sits, which on a walked page is\n // where the seek landed. Absent only on a walked page whose\n // backend did not count what precedes it (SQL and MongoDB, so\n // that a walk does not cost what the offset cost): `unnumbered`\n // then tells the pager to count the rows rather than number them.\n offset={jobs.data.page.offset ?? filters.offset}\n limit={filters.limit}\n total={jobs.data.page.total}\n itemCount={jobs.data.items.length}\n hasMore={jobs.data.page.hasMore}\n walk={{\n // The API mints `page.next` on every page it can be walked on,\n // offset pages included, and on none it cannot. The one walk it\n // refuses is the Active tab in lock-expiry order, which every\n // lock renewal rewrites; the same tab in creation order — what\n // this table asks for wherever the backend records it and\n // Count total is off — is immutable, and walks like any other.\n // So the presence of a cursor is the whole rule: no tab is\n // special-cased here, and none should be.\n canNext: typeof jobs.data.page.next === \"string\",\n canPrev: walk.canPrev,\n // Read from the page shown, not from the walk being asked for:\n // while a move is in flight the rows are still the previous\n // page's, and a range numbered from the new request would\n // number the wrong rows. Only a walked page omits its offset.\n unnumbered: jobs.data.page.offset === undefined,\n hint: walkHint(tab),\n }}\n maxPageSize={meta.limits.maxPageSize}\n disabled={jobs.isFetching && jobs.isPlaceholderData}\n onChange={(next) => {\n const cursor = jobs.data.page.next;\n if (next.step === \"next\" && typeof cursor === \"string\") {\n walk.forward(cursor);\n return;\n }\n if (next.step === \"prev\") {\n walk.back();\n return;\n }\n // Resizing keeps the walk: the cursor that produced this page\n // is re-sent with the new size, so the page keeps its first\n // row. Every other move is a jump, which abandons the walk —\n // an offset and a cursor cannot both decide where a page\n // starts, and the API ignores the offset when both are sent.\n if (next.resize === true && walk.cursor !== undefined) {\n update({ limit: String(next.limit) });\n return;\n }\n walk.reset();\n update({\n offset: next.offset > 0 ? String(next.offset) : null,\n limit: String(next.limit),\n });\n }}\n />\n </>\n )}\n </section>\n );\n}\n\n/** Props of {@link JobFiltersBar}. */\nexport interface JobFiltersBarProps {\n /** The filters in effect. */\n filters: JobListFilters;\n /** Whether the backend searches with an index (else the API scans). */\n searchIndexed: boolean;\n /**\n * Whether pages are sorted by creation time on every tab\n * (`features.addedByState`). With it on, the \"Count total\" toggle says that\n * a counted page keeps the tab's natural order instead. Defaults to `false`.\n */\n createdOrder?: boolean;\n /** Applies the name and search filters. */\n onApply: (patch: { name: string | null; search: string | null }) => void;\n /** Changes the order. */\n onOrder: (order: \"asc\" | \"desc\") => void;\n /**\n * Turns counting the total on or off. Omit it and the \"Count total\" toggle\n * is not offered (a worker page never counts: its range is what keeps the\n * read fast).\n */\n onTotal?: (total: boolean) => void;\n}\n\n/**\n * The name filter, the id/name search, the order and the total toggle. The\n * queue's jobs table and a worker page's jobs section share it.\n */\nexport function JobFiltersBar({\n filters,\n searchIndexed,\n createdOrder = false,\n onApply,\n onOrder,\n onTotal,\n}: JobFiltersBarProps) {\n const [names, setNames] = useState(filters.names.join(\", \"));\n const [search, setSearch] = useState(filters.search);\n const nameId = useId();\n const searchId = useId();\n const orderId = useId();\n const submit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n const list = splitList(names);\n onApply({\n name: list.length > 0 ? list.join(\",\") : null,\n search: search.trim() || null,\n });\n };\n return (\n <form\n className=\"jobs-filters\"\n role=\"search\"\n aria-label=\"Filter jobs\"\n onSubmit={submit}\n >\n <span className=\"jobs-filter\">\n <label htmlFor={nameId}>Names</label>\n <TextInput\n id={nameId}\n value={names}\n onChange={setNames}\n placeholder=\"exact, comma-separated\"\n maxLength={2000}\n />\n </span>\n <span className=\"jobs-filter\">\n <label htmlFor={searchId}>Search</label>\n <TextInput\n id={searchId}\n type=\"search\"\n value={search}\n onChange={setSearch}\n placeholder=\"id or name contains\"\n {...SEARCH_SHORTCUT}\n maxLength={512}\n title={\n searchIndexed\n ? undefined\n : \"This backend has no search index: the API scans the queue.\"\n }\n />\n </span>\n <Button\n type=\"submit\"\n size=\"sm\"\n >\n Apply\n </Button>\n <span className=\"jobs-filter\">\n <label htmlFor={orderId}>Order</label>\n <Select\n id={orderId}\n options={[\n { value: \"desc\", label: \"Newest first\" },\n { value: \"asc\", label: \"Oldest first\" },\n ]}\n value={filters.order}\n onChange={onOrder}\n />\n </span>\n {onTotal && (\n <Checkbox\n checked={filters.total}\n onChange={onTotal}\n label=\"Count total\"\n hint={\n createdOrder && filters.total\n ? \"While counting, a one-state tab keeps its own order, not creation order.\"\n : undefined\n }\n className=\"jobs-total\"\n />\n )}\n </form>\n );\n}\n\n/** Props of {@link JobRows}. */\nexport interface JobRowsProps {\n /** The queue listed. */\n queue: string;\n /** The page's jobs. */\n items: readonly JobDto[];\n /** The table's accessible name. Defaults to \"Jobs in <queue>\". */\n label?: string;\n /**\n * Whether each id links to its job screen. Defaults to `true`; pass\n * `false` where the job screen is not routed (no Queues nav entry).\n */\n linkJobs?: boolean;\n /** What an empty page shows. Defaults to \"No jobs\" for the queue's filters. */\n empty?: ReactNode;\n /**\n * Whether to offer the \"Processed by\" column (`JobDto.processedBy`, the\n * worker that ran each job's last attempt). Defaults to `true`; it shows\n * only where `features.jobAttribution` is also true. A worker page passes\n * `false`: every row there names that page's own key.\n */\n processedBy?: boolean;\n}\n\n/** Which bulk confirmation is open. */\ntype BulkDialog = \"remove\" | null;\n\n/**\n * The table, its selection, and the bulk actions over it. The queue's jobs\n * table and a worker page's jobs section share it.\n */\nexport function JobRows({\n queue,\n items,\n label,\n linkJobs = true,\n empty,\n processedBy = true,\n}: JobRowsProps) {\n const api = useApiClient();\n const meta = useMeta();\n const workersRouted = useWorkerPagesRouted();\n const showProcessedBy = processedBy && meta.features.jobAttribution;\n const toast = useToast();\n const canMutate = useCanMutate();\n const max = meta.limits.maxBulkIds;\n const [selected, setSelected] = useState<ReadonlySet<string>>(\n () => new Set(),\n );\n const [resetAttempts, setResetAttempts] = useState(true);\n const [dialog, setDialog] = useState<BulkDialog>(null);\n const selectAllRef = useRef<HTMLInputElement>(null);\n const bulkBarRef = useRef<HTMLDivElement>(null);\n\n const canRetry = canMutate(\"jobs.retry\");\n const canRemove = canMutate(\"jobs.remove\");\n const canPromote = canMutate(\"jobs.promote\");\n const bulk = canRetry || canRemove || canPromote;\n\n const pageIds = items.map((job) => job.id);\n const selectedOnPage = pageIds.filter((id) => selected.has(id)).length;\n const allOnPage = pageIds.length > 0 && selectedOnPage === pageIds.length;\n const atCap = selected.size >= max;\n\n // A bulk action that succeeds empties the selection, which disables the\n // button that had focus (and a dialog returns focus to it in vain): keep\n // keyboard users in the bulk bar instead of dropping them to <body>.\n const hadSelectionRef = useRef(false);\n useEffect(() => {\n if (selected.size > 0 || dialog !== null) {\n hadSelectionRef.current = true;\n return;\n }\n if (!hadSelectionRef.current) {\n return;\n }\n hadSelectionRef.current = false;\n const active = document.activeElement;\n const lost =\n active === null ||\n active === document.body ||\n (active instanceof HTMLButtonElement &&\n active.disabled &&\n bulkBarRef.current?.contains(active) === true);\n if (lost) {\n bulkBarRef.current?.focus();\n }\n }, [selected, dialog]);\n\n useEffect(() => {\n if (selectAllRef.current) {\n selectAllRef.current.indeterminate = selectedOnPage > 0 && !allOnPage;\n }\n }, [selectedOnPage, allOnPage]);\n\n const toggle = (id: string, on: boolean) => {\n setSelected((previous) => {\n const next = new Set(previous);\n if (on && next.size < max) {\n next.add(id);\n } else if (!on) {\n next.delete(id);\n }\n return next;\n });\n };\n const toggleAll = (on: boolean) => {\n setSelected((previous) => {\n const next = new Set(previous);\n for (const id of pageIds) {\n if (!on) {\n next.delete(id);\n } else if (next.size < max) {\n next.add(id);\n }\n }\n return next;\n });\n };\n\n /** Toasts a bulk result: the done count, the skipped count and (capped) ids. */\n const report = (\n verb: string,\n done: readonly string[],\n skipped: readonly string[],\n ) => {\n toast.success(\n `${verb} ${formatNumber(done.length)}, skipped ${formatNumber(skipped.length)}`,\n {\n description:\n skipped.length > 0 ? `Skipped: ${listIds(skipped)}` : undefined,\n },\n );\n setSelected(new Set());\n };\n const invalidate = mutationInvalidations(queue);\n const retry = useApiMutation({\n mutationFn: (ids: string[]) => bulkRetry(api, queue, ids, resetAttempts),\n onSuccess: (result: BulkRetryResultDto) =>\n report(\"Retried\", result.retried, result.skipped),\n errorTitle: \"Could not retry the jobs\",\n invalidate,\n });\n const promote = useApiMutation({\n mutationFn: (ids: string[]) => bulkPromote(api, queue, ids),\n onSuccess: (result: BulkPromoteResultDto) =>\n report(\"Promoted\", result.promoted, result.skipped),\n errorTitle: \"Could not promote the jobs\",\n invalidate,\n });\n const remove = useApiMutation({\n mutationFn: (ids: string[]) => bulkRemove(api, queue, ids),\n onSuccess: (result: BulkRemoveResultDto) =>\n report(\"Removed\", result.removed, result.skipped),\n invalidate,\n toastErrors: false,\n });\n const busy = retry.isPending || promote.isPending || remove.isPending;\n const ids = [...selected];\n\n if (items.length === 0) {\n return (\n empty ?? (\n <EmptyState\n title=\"No jobs\"\n description=\"No job in this queue matches the state and filters.\"\n />\n )\n );\n }\n\n return (\n <>\n {bulk && (\n <div\n ref={bulkBarRef}\n className=\"bulk-bar\"\n role=\"group\"\n tabIndex={-1}\n aria-label=\"Bulk actions\"\n >\n <span\n className=\"bulk-count\"\n aria-live=\"polite\"\n data-testid=\"bulk-count\"\n >\n {plural(selected.size, \"job\")} selected\n {atCap && ` (the most one action takes: ${formatNumber(max)})`}\n </span>\n {canRetry && (\n <>\n <Checkbox\n checked={resetAttempts}\n onChange={setResetAttempts}\n label=\"Reset attempts\"\n />\n <Button\n size=\"sm\"\n disabled={selected.size === 0 || busy}\n onClick={() => retry.mutate(ids)}\n >\n Retry selected\n </Button>\n </>\n )}\n {canPromote && (\n <Button\n size=\"sm\"\n disabled={selected.size === 0 || busy}\n onClick={() => promote.mutate(ids)}\n >\n Promote selected\n </Button>\n )}\n {canRemove && (\n <Button\n size=\"sm\"\n variant=\"danger\"\n disabled={selected.size === 0 || busy}\n onClick={() => setDialog(\"remove\")}\n >\n Remove selected…\n </Button>\n )}\n {selected.size > 0 && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => setSelected(new Set())}\n >\n Clear selection\n </Button>\n )}\n </div>\n )}\n <Table label={label ?? `Jobs in ${queue}`}>\n <thead>\n <tr>\n {bulk && (\n <th\n scope=\"col\"\n className=\"select-cell\"\n >\n <input\n ref={selectAllRef}\n type=\"checkbox\"\n aria-label=\"Select all jobs on this page\"\n checked={allOnPage}\n disabled={!allOnPage && atCap}\n onChange={(event) => toggleAll(event.target.checked)}\n />\n </th>\n )}\n <th scope=\"col\">Id</th>\n <th scope=\"col\">Name</th>\n <th scope=\"col\">State</th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Attempts\n </th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Priority\n </th>\n <th scope=\"col\">Created</th>\n <th scope=\"col\">Processed</th>\n {showProcessedBy && (\n <th\n scope=\"col\"\n className=\"job-processed-by-col\"\n title=\"The worker that ran the job's last attempt\"\n data-testid=\"jobs-processed-by-header\"\n >\n Processed by\n </th>\n )}\n <th scope=\"col\">Finished</th>\n <th scope=\"col\">Failure</th>\n </tr>\n </thead>\n <tbody>\n {items.map((job) => {\n const checked = selected.has(job.id);\n const failure = job.failedReason?.message;\n return (\n <tr\n key={job.id}\n data-testid={`job-row-${job.id}`}\n aria-selected={bulk ? checked : undefined}\n >\n {bulk && (\n <td className=\"select-cell\">\n <input\n type=\"checkbox\"\n aria-label={`Select job ${job.id}`}\n checked={checked}\n disabled={!checked && atCap}\n onChange={(event) => toggle(job.id, event.target.checked)}\n />\n </td>\n )}\n <th\n scope=\"row\"\n className=\"job-id\"\n >\n {linkJobs ? (\n <Link to={jobPath(queue, job.id)}>{job.id}</Link>\n ) : (\n job.id\n )}\n <CopyButton\n text={job.id}\n label=\"Copy\"\n ariaLabel={`Copy job id ${job.id}`}\n />\n </th>\n <td>{displayText(job.name)}</td>\n <td>\n <StateBadge state={job.state} />\n </td>\n <td className=\"num\">\n {formatNumber(job.attemptsMade)}/\n {formatNumber(job.maxAttempts)}\n </td>\n <td className=\"num\">{formatNumber(job.priority)}</td>\n <td>\n <RelativeTime value={job.createdAt} />\n </td>\n <td>\n <RelativeTime value={job.processedOn} />\n </td>\n {showProcessedBy && (\n <ProcessedByCell\n queue={queue}\n worker={job.processedBy}\n linked={workersRouted}\n />\n )}\n <td>\n <RelativeTime value={job.finishedOn} />\n </td>\n <td\n className=\"job-failure\"\n title={failure}\n >\n {failure ? truncate(failure, FAILURE_PREVIEW_CHARS) : \"—\"}\n </td>\n </tr>\n );\n })}\n </tbody>\n </Table>\n <ConfirmDialog\n open={dialog === \"remove\"}\n onClose={() => setDialog(null)}\n title={`Remove ${plural(ids.length, \"job\")}?`}\n description=\"Removes the selected jobs for good. Running jobs are skipped.\"\n variant=\"danger\"\n confirmLabel=\"Remove\"\n pendingLabel=\"Removing…\"\n onConfirm={() => remove.mutateAsync(ids)}\n />\n </>\n );\n}\n\n/** Props of {@link ProcessedByCell}. */\ninterface ProcessedByCellProps {\n /** The job's queue: a worker page is addressed by it and the key. */\n queue: string;\n /** `JobDto.processedBy`: the worker that ran the last attempt, or `null` when none is recorded. */\n worker: JobWorkerDto | null;\n /** Whether the key links to its worker page (the Workers pages are routed for this caller). */\n linked: boolean;\n}\n\n/**\n * One job's \"Processed by\" cell: the stable key (linked to its worker page\n * when those are routed), the incarnation id as plain text when only the id\n * was recorded, and \"—\" when no worker is.\n */\nfunction ProcessedByCell({ queue, worker, linked }: ProcessedByCellProps) {\n if (worker === null) {\n return (\n <td\n className=\"job-processed-by-col muted\"\n title=\"No worker recorded: the job was never claimed, or was claimed before this backend recorded who ran a job.\"\n data-testid=\"jobs-processed-by-none\"\n >\n —\n </td>\n );\n }\n if (worker.key === undefined) {\n return (\n <td\n className=\"job-processed-by-col\"\n title={`Worker incarnation ${worker.id} (no stable key recorded)`}\n >\n <code\n className=\"job-processed-by-text\"\n data-testid=\"jobs-processed-by-id\"\n >\n {worker.id}\n </code>\n </td>\n );\n }\n return (\n <td\n className=\"job-processed-by-col\"\n title={`${worker.key} (incarnation ${worker.id})`}\n >\n {linked ? (\n <Link\n to={workerPath(queue, worker.key)}\n className=\"job-processed-by-text\"\n data-testid=\"jobs-processed-by-key\"\n >\n {worker.key}\n </Link>\n ) : (\n <span\n className=\"job-processed-by-text\"\n data-testid=\"jobs-processed-by-key\"\n >\n {worker.key}\n </span>\n )}\n </td>\n );\n}\n",
11
+ "import { workerKeys } from \"../../api/workers\";\nimport { liveChannels, useLiveInvalidation, usePollInterval } from \"../../live\";\n\n/**\n * How the worker views stay current.\n *\n * Worker events travel on their own channels — `workers`, and\n * `queue/<queue>/workers` — deliberately kept off `all` and `queues` so a\n * dashboard does not receive control traffic. A worker event (a state change,\n * an adopted config, a recorded control) refreshes the worker reads; without\n * a socket, every worker read polls at {@link WORKER_REFRESH.list}.\n */\nexport const WORKER_REFRESH = {\n /** Every worker read: a state change should show up quickly. */\n list: 5_000,\n} as const satisfies Record<string, number | false>;\n\n/**\n * The Workers page's live updates: any worker event in the namespace\n * refreshes every worker read. `enabled` is the caller's `workers.list`.\n */\nexport function useWorkerListLive(enabled: boolean): void {\n useLiveInvalidation([liveChannels.workers], [workerKeys.all], { enabled });\n}\n\n/**\n * One queue's worker panel: only that queue's worker events, so a busy\n * namespace does not refresh a panel watching one queue.\n */\nexport function useQueueWorkersLive(queue: string, enabled: boolean): void {\n useLiveInvalidation(\n [liveChannels.queueWorkers(queue)],\n [workerKeys.ofQueue(queue), workerKeys.all],\n { enabled },\n );\n}\n\n/** How often worker reads poll: the base interval, relaxed while live updates carry the changes. */\nexport function useWorkerRefetchInterval(): number | false {\n return usePollInterval(WORKER_REFRESH.list);\n}\n",
12
+ "import { ApiError, isApiError } from \"../../../api/errors\";\n\n/** One thing the UI asks of a worker. */\nexport type WorkerAction = \"pause\" | \"resume\" | \"stop\" | \"start\" | \"configure\";\n\n/** How each action reads in a sentence about what was refused. */\nconst WHAT: Readonly<Record<WorkerAction, string>> = {\n pause: \"pause this worker\",\n resume: \"resume this worker\",\n stop: \"stop this worker\",\n start: \"start this worker\",\n configure: \"change this worker's settings\",\n};\n\n/**\n * A worker error in the user's terms, or `null` to leave the API's own text.\n * The codes come from the management API's worker routes.\n */\nexport function explainWorkerError(\n error: ApiError,\n action: WorkerAction,\n): string | null {\n switch (error.code) {\n case \"WORKER_NOT_FOUND\":\n return \"This worker is not in the registry: it was never there, or its record was removed. Refresh to see the workers reporting now.\";\n case \"WORKER_GONE\":\n return \"This worker's record lapsed before the instruction reached it — its process stopped reporting. Refresh; if it comes back it will have a new id.\";\n case \"WORKER_STATE_CONFLICT\":\n return `Could not ${WHAT[action]}: it is no longer in the state that action needs. Refresh and look at its state — a stopped worker is started, not resumed.`;\n case \"WORKER_NOT_CONTROLLABLE\":\n return \"This API cannot control this worker: remote control is off for it, or it runs an older release that does not listen for instructions.\";\n case \"WORKER_PERSISTENCE_NOT_ALLOWED\":\n return \"This deployment does not let a stop's persistence be chosen per request. Stop it with the behaviour the deployment configured.\";\n case \"CONTROL_CONTENDED\":\n return \"Somebody else changed this worker while the dialog was open, so nothing was written. Refresh to see their change, then apply yours again.\";\n case \"CONFIG_NOT_ALLOWED\":\n return \"The API refused these settings: a value is outside what it allows, or this deployment does not allow that setting to be overridden.\";\n default:\n return null;\n }\n}\n\n/**\n * The error to show in a dialog: an `ApiError` whose detail is the\n * explanation when there is one (its code, status and context kept), else the\n * error unchanged.\n */\nexport function explained(error: unknown, action: WorkerAction): unknown {\n if (!isApiError(error)) {\n return error;\n }\n const detail = explainWorkerError(error, action);\n if (detail === null) {\n return error;\n }\n return new ApiError({\n kind: error.kind,\n status: error.status,\n code: error.code,\n title: error.title,\n detail,\n type: error.type,\n issues: error.issues,\n context: error.context,\n instance: error.instance,\n cause: error,\n });\n}\n",
13
+ "import type { JobsApiAction } from \"../../../api/contract\";\nimport type { WorkerDto } from \"../../../api/types\";\nimport { isControllable, isStale, workerState } from \"../../../api/workers\";\nimport { useMeta } from \"../../../meta/hooks\";\nimport { useCanMutate } from \"../../queues/gating\";\n\n/**\n * Which worker actions are offered. Lifecycle actions target one running\n * worker (its per-incarnation `id`); Configure… targets the stable `key`, so\n * it needs one.\n */\nexport interface WorkerActionGates {\n /** `workers.pause`, on a running worker. */\n pause: boolean;\n /** `workers.resume`, on a paused worker. */\n resume: boolean;\n /** `workers.stop`, on a running or paused worker. */\n stop: boolean;\n /** `workers.start`, on a stopped worker. */\n start: boolean;\n /** `workers.configure` (opt-in), when the worker reports the stable key an override is stored against. */\n configure: boolean;\n /**\n * Why no lifecycle action is offered although the caller holds the\n * permissions: the record has lapsed, the worker is mid-transition, or the\n * API cannot reach it. `null` when there is nothing to explain.\n */\n blocked: \"stale\" | \"transitioning\" | \"uncontrollable\" | null;\n}\n\n/**\n * The actions to offer for `worker`, given a predicate that is true only for\n * a permitted action on a writable API (`useCanMutate`).\n *\n * A worker in `stopping` or `restarting` is between states, so it is offered\n * nothing until it settles; a lapsed record (`stale`) means nobody is there\n * to act on the instruction; and an API that reports `control.enabled: false`\n * — remote control switched off, or a worker from an older release — cannot\n * be controlled at all. Configure… stays available while stale, because an\n * override is stored for whichever replica comes up next.\n */\nexport function workerActionGates(\n worker: WorkerDto,\n canMutate: (action: JobsApiAction) => boolean,\n now = Date.now(),\n): WorkerActionGates {\n const state = workerState(worker);\n const stale = isStale(worker, now);\n const controllable = isControllable(worker);\n const transitioning = state === \"stopping\" || state === \"restarting\";\n const live = controllable && !stale && !transitioning;\n return {\n pause: live && state === \"running\" && canMutate(\"workers.pause\"),\n resume: live && state === \"paused\" && canMutate(\"workers.resume\"),\n stop:\n live &&\n (state === \"running\" || state === \"paused\") &&\n canMutate(\"workers.stop\"),\n start: live && state === \"stopped\" && canMutate(\"workers.start\"),\n configure:\n controllable &&\n worker.key !== undefined &&\n canMutate(\"workers.configure\"),\n blocked: !controllable\n ? \"uncontrollable\"\n : stale\n ? \"stale\"\n : transitioning\n ? \"transitioning\"\n : null,\n };\n}\n\n/**\n * Whether a row says why it offers no lifecycle action: the worker's state is\n * the reason (`gates.blocked`) **and** the caller holds the lifecycle\n * permission, so the gap is the worker's doing, not the caller's lack of\n * permission (which is the ordinary case, and says nothing). Independent of\n * Settings…, which stays offered while a worker is stale or mid-transition.\n */\nexport function explainsBlock(\n gates: WorkerActionGates,\n canMutate: (action: JobsApiAction) => boolean,\n): boolean {\n return gates.blocked !== null && canMutate(\"workers.pause\");\n}\n\n/** Why a worker offers no lifecycle action, as a sentence for a title attribute. */\nexport const WORKER_BLOCKED_HINT: Readonly<\n Record<NonNullable<WorkerActionGates[\"blocked\"]>, string>\n> = {\n stale:\n \"This worker stopped reporting, so nothing would receive the instruction. Its record lapses shortly.\",\n transitioning:\n \"This worker is between states. Its actions come back once it settles.\",\n uncontrollable:\n \"This API cannot control this worker: remote control is off for it, or it runs an older release that does not listen for instructions.\",\n};\n\n/**\n * Whether a worker mutation is offered here: the caller holds it, the API is\n * writable (`useCanMutate`), **and** the backend supports worker control\n * (`meta.features.workerControl`). Without that support the API prunes the\n * control routes altogether, so a button would only ever 404.\n */\nexport function useCanControlWorkers(): (action: JobsApiAction) => boolean {\n const meta = useMeta();\n const canMutate = useCanMutate();\n return (action) => meta.features.workerControl && canMutate(action);\n}\n",
14
+ "import type { WorkerStopPersistence } from \"../../../api/contract\";\nimport type { WorkerDto } from \"../../../api/types\";\nimport { useState } from \"react\";\nimport { WORKER_STOP_PERSISTENCE } from \"../../../api/contract\";\nimport { ConfirmDialog } from \"../../../components/ConfirmDialog\";\nimport { Field } from \"../../../components/Field\";\nimport { Select } from \"../../../components/inputs\";\n\n/** What each persistence choice does, as the dialog puts it. */\nconst PERSISTENCE_LABEL: Readonly<Record<WorkerStopPersistence, string>> = {\n process: \"Until this process restarts\",\n key: \"Until somebody starts it again\",\n};\n\n/** The sentence under each choice. */\nconst PERSISTENCE_HINT: Readonly<Record<WorkerStopPersistence, string>> = {\n process:\n \"A redeploy or a restart brings this worker back running, because the stop is recorded against this incarnation only.\",\n key: \"Recorded against the worker's stable key, so every replica of it — including one started later — comes up stopped.\",\n};\n\n/** Props of {@link StopWorkerDialog}. */\nexport interface StopWorkerDialogProps {\n /** Whether it is showing. */\n open: boolean;\n /** Asks to close. */\n onClose: () => void;\n /** The worker to stop, as the last read returned it. */\n worker: WorkerDto;\n /** Stops it; `persist` is sent only when the deployment lets it be chosen. */\n onConfirm: (persist?: WorkerStopPersistence) => unknown;\n}\n\n/**\n * Stop one worker: it finishes the jobs it holds and parks its loop, staying\n * in the registry so Start can bring it back.\n *\n * How long the stop survives is the deployment's setting\n * (`control.stopPersistence`). When it allows a per-stop choice\n * (`stopPersistenceOverridable`) this offers both, preselecting the\n * configured one; otherwise it states what will happen.\n */\nexport function StopWorkerDialog({\n open,\n onClose,\n worker,\n onConfirm,\n}: StopWorkerDialogProps) {\n const configured = worker.control?.stopPersistence ?? \"process\";\n const choosable = worker.control?.stopPersistenceOverridable === true;\n const [persist, setPersist] = useState<WorkerStopPersistence>(configured);\n const active = worker.active;\n return (\n <ConfirmDialog\n open={open}\n onClose={onClose}\n title={`Stop worker ${worker.id}?`}\n description={\n active > 0\n ? `It finishes the ${active === 1 ? \"job it is running\" : `${active} jobs it is running`} and takes no more. It stays listed, so you can start it again.`\n : \"It parks its loop and takes no jobs. It stays listed, so you can start it again.\"\n }\n confirmLabel=\"Stop worker\"\n pendingLabel=\"Stopping…\"\n variant=\"danger\"\n onConfirm={() => onConfirm(choosable ? persist : undefined)}\n >\n {choosable ? (\n <Field\n label=\"How long it stays stopped\"\n hint={PERSISTENCE_HINT[persist]}\n >\n <Select\n value={persist}\n onChange={setPersist}\n options={WORKER_STOP_PERSISTENCE.map((value) => ({\n value,\n label: PERSISTENCE_LABEL[value],\n }))}\n />\n </Field>\n ) : (\n <p className=\"muted\">{PERSISTENCE_HINT[configured]}</p>\n )}\n </ConfirmDialog>\n );\n}\n",
15
+ "import type { WorkerConfigKey } from \"../../../api/contract\";\n\n/** What each setting is, in one line: under its input in the Settings dialog, and as the title of its row on the worker page. */\nexport const SETTING_HINT: Readonly<Record<WorkerConfigKey, string>> = {\n concurrency: \"How many jobs this worker runs at once.\",\n pollInterval: \"How often it looks for work when nothing woke it, in ms.\",\n maxBlock: \"How long one wait for work may block, in ms.\",\n lockDuration:\n \"How long a job's lock lasts before another worker may claim it, in ms.\",\n heartbeatInterval:\n \"How often a held lock is renewed, in ms. Must be at most half the lock duration.\",\n stalledInterval: \"How often stalled jobs are looked for, in ms.\",\n maxStalledCount: \"How often one job may stall before it is failed.\",\n reportInterval:\n \"How often this worker reports itself to the registry, in ms.\",\n drainDelay: \"How long it waits before declaring the queue drained, in ms.\",\n};\n",
16
+ "import type { WorkerConfigBody, WorkerConfigKey } from \"../../../api/contract\";\nimport type { WorkerDto } from \"../../../api/types\";\nimport { useState } from \"react\";\nimport {\n WORKER_CONFIG_BOUNDS,\n WORKER_CONFIG_KEYS,\n} from \"../../../api/contract\";\nimport {\n configureWorkers,\n resetWorkerConfig,\n workerInvalidations,\n} from \"../../../api/workers\";\nimport { Button } from \"../../../components/Button\";\nimport { Dialog } from \"../../../components/Dialog\";\nimport { Field } from \"../../../components/Field\";\nimport { NumberInput } from \"../../../components/inputs\";\nimport { ProblemBanner } from \"../../../components/ProblemBanner\";\nimport { useApiClient } from \"../../../context\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { explained } from \"./explain\";\nimport { SETTING_HINT } from \"./settingHints\";\n\n/** The form's value for each setting: a number, or `undefined` while the box is empty. */\ntype Draft = Partial<Record<WorkerConfigKey, number | undefined>>;\n\n/** Props of {@link WorkerConfigDialog}. */\nexport interface WorkerConfigDialogProps {\n /** The worker whose settings are edited; its `key` is what the override is stored against. */\n worker: WorkerDto;\n /** Asks to close. */\n onClose: () => void;\n}\n\n/**\n * Edit the settings of every worker sharing one stable key.\n *\n * An override is stored against the worker's `key`, not its `id`, so it\n * reaches every live replica carrying that key and every replica started\n * later, until it is reset. The dialog says so, and each input shows what the\n * process's own code asked for, so an override is recognisable as one.\n *\n * Changes are applied in place: a worker does not drain or restart, and the\n * jobs it holds keep their locks.\n */\nexport function WorkerConfigDialog({\n worker,\n onClose,\n}: WorkerConfigDialogProps) {\n const api = useApiClient();\n const config = worker.config;\n const key = worker.key ?? worker.id;\n const [draft, setDraft] = useState<Draft>({});\n const [error, setError] = useState<unknown>(null);\n\n /** The value shown for a setting: what has been typed, else what the worker runs with. */\n const valueOf = (setting: WorkerConfigKey): number | undefined =>\n setting in draft ? draft[setting] : config?.effective[setting];\n\n /** Only the settings whose value differs from what the worker runs with, as a merge patch. */\n const patch = (): WorkerConfigBody => {\n const body: WorkerConfigBody = {};\n for (const setting of WORKER_CONFIG_KEYS) {\n if (!(setting in draft)) {\n continue;\n }\n const next = draft[setting];\n if (next === undefined) {\n // An emptied box clears that setting's override.\n if (config?.overridden.includes(setting)) {\n body[setting] = null;\n }\n continue;\n }\n if (next !== config?.effective[setting]) {\n body[setting] = next;\n }\n }\n if (config) {\n body.expectedSeq = config.seq;\n }\n return body;\n };\n\n const save = useApiMutation({\n mutationFn: () => configureWorkers(api, worker.queue, key, patch()),\n invalidate: workerInvalidations(worker.queue),\n successMessage: (result) =>\n result.instances.length > 1\n ? `Saved the settings of ${key}; ${result.instances.length} workers carry it`\n : `Saved the settings of ${key}`,\n toastErrors: false,\n onSuccess: onClose,\n onError: (cause) => setError(explained(cause, \"configure\")),\n });\n\n const reset = useApiMutation({\n mutationFn: () => resetWorkerConfig(api, worker.queue, key),\n invalidate: workerInvalidations(worker.queue),\n successMessage: `Reset ${key} to the values in its code`,\n toastErrors: false,\n onSuccess: onClose,\n onError: (cause) => setError(explained(cause, \"configure\")),\n });\n\n const pending = save.isPending || reset.isPending;\n const changes = Object.keys(patch()).filter(\n (field) => field !== \"expectedSeq\",\n );\n\n return (\n <Dialog\n open\n onClose={onClose}\n closeOnEscape={!pending}\n size=\"lg\"\n title={`Settings of ${key}`}\n description={\n <>\n Stored against the worker&rsquo;s key, so this reaches every worker\n carrying <code>{key}</code> — including ones started later — until you\n reset it. Changes apply in place: nothing drains or restarts.\n </>\n }\n footer={\n <>\n <Button\n onClick={onClose}\n disabled={pending}\n >\n Cancel\n </Button>\n {config && config.overridden.length > 0 && (\n <Button\n variant=\"danger\"\n onClick={() => reset.mutate()}\n disabled={pending}\n aria-busy={reset.isPending || undefined}\n >\n {reset.isPending ? \"Resetting…\" : \"Reset to code values\"}\n </Button>\n )}\n <Button\n variant=\"primary\"\n onClick={() => save.mutate()}\n disabled={pending || changes.length === 0}\n aria-busy={save.isPending || undefined}\n >\n {save.isPending ? \"Saving…\" : \"Save settings\"}\n </Button>\n </>\n }\n >\n {error !== null && <ProblemBanner error={error} />}\n {worker.control?.pending === true && (\n <p\n className=\"worker-config-note\"\n data-testid=\"worker-config-pending\"\n >\n A change is waiting to be taken up, so the values below are the ones\n this worker still runs with. Saving now replaces the waiting change.\n </p>\n )}\n {config === undefined ? (\n <p className=\"worker-config-note\">\n This worker does not report its settings, so there is nothing to show\n yet. It reports them at its next heartbeat.\n </p>\n ) : (\n <div className=\"worker-config-form\">\n {WORKER_CONFIG_KEYS.map((setting) => {\n const bounds = WORKER_CONFIG_BOUNDS[setting];\n const overridden = config.overridden.includes(setting);\n const code = config.code[setting];\n return (\n <div\n className=\"worker-config-row\"\n key={setting}\n >\n <Field\n label={setting}\n hint={\n <>\n {SETTING_HINT[setting]} {bounds.min}–{bounds.max}.{\" \"}\n {overridden\n ? `Overridden; its code asks for ${code}.`\n : `Its code asks for ${code}.`}\n </>\n }\n >\n <NumberInput\n value={valueOf(setting)}\n min={bounds.min}\n max={bounds.max}\n onChange={(next) =>\n setDraft((current) => ({ ...current, [setting]: next }))\n }\n />\n </Field>\n {overridden && (\n <Button\n size=\"sm\"\n onClick={() =>\n setDraft((current) => ({ ...current, [setting]: code }))\n }\n disabled={pending}\n >\n Use code value\n </Button>\n )}\n </div>\n );\n })}\n </div>\n )}\n </Dialog>\n );\n}\n",
17
+ "/**\n * One worker's actions: pause, resume, stop, start and Settings…, with their\n * dialogs. The Workers screen and the queue screen's Workers panel both mount\n * {@link WorkerActions} in a row's last cell, so a worker is controlled the\n * same way wherever it is listed.\n *\n * Lifecycle actions target one running worker (its per-incarnation `id`);\n * Settings… targets the stable `key`, so it reaches every replica carrying it\n * (see {@link WorkerConfigDialog}).\n */\nimport type { WorkerStopPersistence } from \"../../../api/contract\";\nimport type { WorkerControlResultDto, WorkerDto } from \"../../../api/types\";\nimport { useState } from \"react\";\nimport { isApiError } from \"../../../api/errors\";\nimport {\n controlWorker,\n workerInvalidations,\n workerState,\n} from \"../../../api/workers\";\nimport { Button } from \"../../../components/Button\";\nimport { useToast } from \"../../../components/toast\";\nimport { useApiClient } from \"../../../context\";\nimport { useApiMutation } from \"../../../hooks/useApiMutation\";\nimport { explainWorkerError } from \"./explain\";\nimport {\n explainsBlock,\n useCanControlWorkers,\n WORKER_BLOCKED_HINT,\n workerActionGates,\n} from \"./gating\";\nimport { StopWorkerDialog } from \"./StopWorkerDialog\";\nimport { WorkerConfigDialog } from \"./WorkerConfigDialog\";\nimport \"./workerActions.css\";\n\n/** Props of {@link WorkerActions}. */\nexport interface WorkerActionsProps {\n /** The worker as the last worker read returned it. */\n worker: WorkerDto;\n}\n\n/** Which dialog is open. */\ntype OpenDialog = \"stop\" | \"settings\" | null;\n\n/** What a successful instruction says, by whether the worker acknowledged it. */\nfunction outcome(applied: boolean, done: string, queued: string): string {\n return applied ? done : queued;\n}\n\n/**\n * What a successful Stop says. `applied` means the worker **accepted** the\n * instruction, not that it has finished stopping: a worker holding jobs reads\n * `stopping` (with `control.pending`) until they finish. So the re-read\n * worker the answer carries decides when present — stopped, or accepted with\n * no job to drain — and `applied` only without it. Pause needs no such care: a paused worker stops claiming at once and\n * reads `paused` in the same answer, while the jobs it holds run on.\n */\nfunction stopOutcome(id: string, result: WorkerControlResultDto): string {\n const replicas =\n result.persisted === \"key\"\n ? \"; every replica of it stays stopped until started\"\n : \"\";\n const worker = result.worker;\n // \"It finishes its jobs first\" is true only of a worker holding jobs. On a\n // driver that polls for instructions the re-read the answer carries can be\n // taken before an IDLE worker has finished parking — it reads `stopping`,\n // pending — though it has nothing to drain and is stopped a moment later.\n // So a worker holding no job counts as stopped once the stop was accepted.\n const stopped =\n worker === undefined\n ? result.applied\n : (workerState(worker) === \"stopped\" &&\n worker.control?.pending !== true) ||\n (result.applied && worker.active === 0);\n return stopped\n ? `Stopped ${id}${replicas}`\n : `Asked ${id} to stop; it finishes its jobs first${replicas}`;\n}\n\n/** The action buttons (and dialogs) for one worker, each present only when its permission is held and the worker is in a state that takes it. */\nexport function WorkerActions({ worker }: WorkerActionsProps) {\n const api = useApiClient();\n const toast = useToast();\n const canMutate = useCanControlWorkers();\n const [open, setOpen] = useState<OpenDialog>(null);\n const close = () => setOpen(null);\n const gates = workerActionGates(worker, canMutate);\n\n /** Pause, resume and start: one button each, no dialog (all are reversible). */\n const control = useApiMutation({\n mutationFn: (action: \"pause\" | \"resume\" | \"start\") =>\n controlWorker(api, worker.queue, worker.id, action).then((result) => ({\n action,\n result,\n })),\n invalidate: workerInvalidations(worker.queue),\n successMessage: ({ action, result }) =>\n action === \"pause\"\n ? outcome(\n result.applied,\n `Paused ${worker.id}`,\n `Asked ${worker.id} to pause; it stops taking jobs shortly`,\n )\n : action === \"resume\"\n ? outcome(\n result.applied,\n `Resumed ${worker.id}`,\n `Asked ${worker.id} to resume; it takes jobs again shortly`,\n )\n : outcome(\n result.applied,\n `Started ${worker.id}`,\n `Asked ${worker.id} to start; it takes jobs again shortly`,\n ),\n toastErrors: false,\n onError: (error, action) => {\n toast.error(`Could not ${action} ${worker.id}`, {\n description: isApiError(error)\n ? (explainWorkerError(error, action) ?? error.detail ?? error.title)\n : error.message,\n });\n },\n });\n\n const stop = useApiMutation({\n mutationFn: (persist: WorkerStopPersistence | undefined) =>\n controlWorker(api, worker.queue, worker.id, \"stop\", { persist }),\n invalidate: workerInvalidations(worker.queue),\n successMessage: (result) => stopOutcome(worker.id, result),\n toastErrors: false,\n });\n\n const anyLifecycle = gates.pause || gates.resume || gates.stop || gates.start;\n // Say why the lifecycle actions are missing only when it is the worker's\n // doing, not the caller's lack of permission (the ordinary case) — and say\n // it even while Settings… is offered, which it stays mid-transition.\n const hint =\n gates.blocked !== null && explainsBlock(gates, canMutate) ? (\n <span\n className=\"worker-actions-hint muted\"\n data-testid={`worker-blocked-${worker.id}`}\n >\n {WORKER_BLOCKED_HINT[gates.blocked]}\n </span>\n ) : null;\n if (!anyLifecycle && !gates.configure) {\n return hint;\n }\n\n return (\n <div\n className=\"worker-actions\"\n role=\"group\"\n aria-label={`Actions for worker ${worker.id}`}\n >\n {gates.pause && (\n <Button\n size=\"sm\"\n onClick={() => control.mutate(\"pause\")}\n disabled={control.isPending}\n aria-busy={control.isPending || undefined}\n >\n Pause\n </Button>\n )}\n {gates.resume && (\n <Button\n size=\"sm\"\n onClick={() => control.mutate(\"resume\")}\n disabled={control.isPending}\n aria-busy={control.isPending || undefined}\n >\n Resume\n </Button>\n )}\n {gates.start && (\n <Button\n size=\"sm\"\n variant=\"primary\"\n onClick={() => control.mutate(\"start\")}\n disabled={control.isPending}\n aria-busy={control.isPending || undefined}\n >\n Start\n </Button>\n )}\n {gates.stop && (\n <Button\n size=\"sm\"\n variant=\"danger\"\n onClick={() => setOpen(\"stop\")}\n >\n Stop…\n </Button>\n )}\n {gates.configure && (\n <Button\n size=\"sm\"\n onClick={() => setOpen(\"settings\")}\n >\n Settings…\n </Button>\n )}\n {hint}\n <StopWorkerDialog\n open={open === \"stop\"}\n onClose={close}\n worker={worker}\n onConfirm={(persist) => stop.mutateAsync(persist)}\n />\n {open === \"settings\" && (\n <WorkerConfigDialog\n worker={worker}\n onClose={close}\n />\n )}\n </div>\n );\n}\n",
18
+ "import type { WorkerSummonProvenanceDto } from \"../../api/types\";\nimport { displayText } from \"../../format\";\n\n/**\n * How a worker's `summon` reads: which summon attempt started it, and what\n * the summoner asked of it.\n *\n * Three rules from the contract shape every function below:\n *\n * - **Absent says nothing.** A worker with no `summon` was not summoned, or\n * is too old to say which, so it gets no badge and no row — never a\n * \"not summoned\" claim the record does not make.\n * - **`mode` and `deadlineAt` are what the summoner requested**, not what the\n * worker is doing: they are always labelled \"requested\", and a missing one\n * is left out rather than defaulted.\n * - **A mode this build does not know is shown as its raw string**, since a\n * later server may add one. The labels are an exhaustive record of the\n * modes the contract lists, so a mode added there is a compile error here\n * until it is named.\n */\n\n/** A summon mode the contract lists. */\nexport type SummonMode = NonNullable<WorkerSummonProvenanceDto[\"mode\"]>;\n\n/** Each known requested mode in plain words, and what it asks. */\nconst MODE_TEXT: Readonly<Record<SummonMode, { label: string; hint: string }>> =\n {\n \"exit-on-idle\": {\n label: \"Exit when idle\",\n hint: \"The summoner asked it to exit once its queue has no work for it.\",\n },\n \"until-stopped\": {\n label: \"Run until stopped\",\n hint: \"The summoner asked it to keep running until something stops it.\",\n },\n \"in-invocation\": {\n label: \"Within one invocation\",\n hint: \"The summoner asked it to run inside a single platform invocation, and end with it.\",\n },\n };\n\n/** Said of every summon: it is how the worker was started, not a setting. */\nexport const SUMMON_NOTE =\n \"How this worker was started, as its summoner recorded it. The mode and deadline are what the summoner requested, not a report of what the worker is doing.\";\n\n/** Whether `mode` is one this build names (an own key, so `\"toString\"` is not). */\nfunction isKnownMode(mode: unknown): mode is SummonMode {\n return typeof mode === \"string\" && Object.hasOwn(MODE_TEXT, mode);\n}\n\n/** A requested mode in plain words, or the raw string for a mode this build does not know. */\nexport function summonModeLabel(mode: SummonMode): string {\n return isKnownMode(mode) ? MODE_TEXT[mode].label : displayText(mode);\n}\n\n/** What a requested mode asks, in a sentence; for a mode this build does not know, says so. */\nexport function summonModeHint(mode: SummonMode): string {\n return isKnownMode(mode)\n ? MODE_TEXT[mode].hint\n : `The summoner requested a mode this version of the UI does not know (\"${displayText(mode)}\").`;\n}\n\n/**\n * What a worker table's summon badge reads: \"Summoned\", and the summoner's\n * kind when it gave one (\"Summoned by ecs\").\n */\nexport function summonBadgeLabel(summon: WorkerSummonProvenanceDto): string {\n return summon.kind === undefined\n ? \"Summoned\"\n : `Summoned by ${displayText(summon.kind)}`;\n}\n\n/**\n * What a worker table's unit badge reads for a worker in a summon group's\n * shared unit (`summon.group`): \"Unit of media\". Shown only when the record\n * names a group; a unit summoned for one queue has none.\n */\nexport function summonUnitLabel(group: string): string {\n return `Unit of ${displayText(group)}`;\n}\n\n/**\n * The unit badge's tooltip: that the worker runs in the one process summoned\n * for the whole group, serving each of its member queues.\n */\nexport function summonUnitHint(group: string): string {\n return `This worker runs in the shared unit summoned for the summon group ${displayText(group)}: one process serving its member queues.`;\n}\n\n/**\n * The summon badge's tooltip: the attempt, and whatever the summoner\n * requested, each named as a request. A deadline is given as an absolute\n * instant, since a tooltip does not tick.\n */\nexport function summonHint(summon: WorkerSummonProvenanceDto): string {\n const requested = [\n summon.mode !== undefined &&\n `requested mode: ${summonModeLabel(summon.mode)}`,\n // `toISOString` throws on a value that is not a finite number, and the\n // listing is not shape-checked.\n typeof summon.deadlineAt === \"number\" &&\n Number.isFinite(summon.deadlineAt) &&\n `requested deadline: ${new Date(summon.deadlineAt).toISOString()}`,\n ].filter((part): part is string => part !== false);\n return [\n `Started by a summoner, attempt ${displayText(summon.id)}.`,\n // What it actually does, unlike the requests after it.\n summon.resolvedMode === undefined\n ? \"\"\n : `Runs as: ${summonModeLabel(summon.resolvedMode)}.`,\n requested.length > 0 ? `The summoner's ${requested.join(\"; \")}.` : \"\",\n \"How it was started, not a setting.\",\n ]\n .filter((part) => part !== \"\")\n .join(\" \");\n}\n",
19
+ "import type { WorkerTargetKind } from \"../../api/contract\";\nimport type { WorkerDto, WorkerTargetInfoDto } from \"../../api/types\";\nimport { displayText } from \"../../format\";\n\n/**\n * How a worker's `target` reads: where its attempts run, and whether they run\n * a function or a processor file.\n *\n * It is a fact about how the worker was **built**, not a setting — the field\n * is not among `WORKER_CONFIG_KEYS`, and changing where code runs is a\n * rebuild — so nothing here offers to change it.\n *\n * Two rules from the contract shape every function below:\n *\n * - **Absent is not \"in process\".** A worker too old to report the field has\n * said nothing; `\"in-process\"` is merely the default, so reading absence as\n * it would mislabel every worker that has not been upgraded.\n * - **A kind this build does not know is shown as its raw string**, since a\n * later server may add one. The labels are an exhaustive record of the\n * kinds the contract lists, so a kind added there is a compile error here\n * until it is named.\n *\n * The words are the worker's own (\"worker thread\", \"child process\"), never a\n * runner's `executionMode` vocabulary: the two are different fields.\n */\n\n/** Each known target kind in plain words, and what it means. */\nconst KIND_TEXT: Readonly<\n Record<WorkerTargetKind, { label: string; hint: string }>\n> = {\n \"in-process\": {\n label: \"In process\",\n hint: \"Its attempts run in the worker's own process, on the thread that claims them.\",\n },\n \"worker-thread\": {\n label: \"Worker thread\",\n hint: \"Each attempt runs on a fresh worker thread (a Web Worker) inside the worker's process.\",\n },\n \"child-process\": {\n label: \"Child process\",\n hint: \"Each attempt runs in a fresh child process, which can be killed if it ignores its signal.\",\n },\n container: {\n label: \"Container\",\n hint: \"Each attempt runs in a fresh container: no network, none of the worker's environment, a read-only filesystem.\",\n },\n custom: {\n label: \"Custom\",\n hint: \"Its attempts run on a target the application supplied.\",\n },\n};\n\n/** Each known processor form in plain words. */\nconst PROCESSOR_LABEL: Readonly<\n Record<WorkerTargetInfoDto[\"processor\"], string>\n> = {\n function: \"Function\",\n file: \"File\",\n};\n\n/** Said of every target: it comes from the code, so no dialog changes it. */\nconst FIXED_NOTE =\n \"Set in the worker's code when it was built; changing it is a redeploy, not a setting.\";\n\n/** Whether `kind` is one this build names (an own key, so `\"toString\"` is not). */\nfunction isKnownKind(kind: unknown): kind is WorkerTargetKind {\n return typeof kind === \"string\" && Object.hasOwn(KIND_TEXT, kind);\n}\n\n/**\n * A target kind in plain words: \"In process\", \"Worker thread\", \"Child\n * process\", \"Custom\" — or the raw string for a kind this build does not know.\n */\nexport function targetKindLabel(kind: WorkerTargetInfoDto[\"kind\"]): string {\n return isKnownKind(kind) ? KIND_TEXT[kind].label : displayText(kind);\n}\n\n/**\n * The processor form in plain words, \"Function\" or \"File\" — or the raw string\n * for a form this build does not know.\n */\nexport function processorLabel(\n processor: WorkerTargetInfoDto[\"processor\"],\n): string {\n return typeof processor === \"string\" &&\n Object.hasOwn(PROCESSOR_LABEL, processor)\n ? PROCESSOR_LABEL[processor]\n : displayText(processor);\n}\n\n/**\n * What a worker table's target badge reads: the kind in plain words, and for\n * a `custom` target its own `name` (plain \"Custom\" when it gave none).\n */\nexport function targetBadgeLabel(target: WorkerTargetInfoDto): string {\n if (target.kind === \"custom\" && target.name !== undefined) {\n return displayText(target.name);\n }\n return targetKindLabel(target.kind);\n}\n\n/** What a target kind means, in a sentence; for a kind this build does not know, says so. */\nexport function targetKindHint(kind: WorkerTargetInfoDto[\"kind\"]): string {\n return isKnownKind(kind)\n ? KIND_TEXT[kind].hint\n : `Its attempts run on a target this version of the UI does not know (\"${displayText(kind)}\").`;\n}\n\n/** The target badge's tooltip: what the kind means, and that it is not a setting. */\nexport function targetHint(target: WorkerTargetInfoDto): string {\n const meaning = targetKindHint(target.kind);\n const custom =\n target.kind === \"custom\" && target.name !== undefined\n ? ` Named \"${displayText(target.name)}\" by the application.`\n : \"\";\n return `${meaning}${custom} ${FIXED_NOTE}`;\n}\n\n/** The note under a Target card's kind. */\nexport const TARGET_FIXED_NOTE = FIXED_NOTE;\n\n/**\n * One way the instances of a worker key run, with the instances that report\n * it. `target` is `undefined` for the instances too old to say.\n */\nexport interface TargetGroup {\n /** The target these instances report, or `undefined` for those that report none. */\n target: WorkerTargetInfoDto | undefined;\n /** Their incarnation ids, in the order the listing gave them. */\n ids: string[];\n}\n\n/** A stable identity for a target: every field the API sent, in order. */\nfunction signature(target: WorkerTargetInfoDto | undefined): string {\n return target === undefined\n ? \"\"\n : JSON.stringify([target.kind, target.processor, target.name, target.file]);\n}\n\n/**\n * The instances of one key grouped by the target they report, in the order\n * each target first appears.\n *\n * Instances of a key are normally built from one piece of code and report\n * one target; two groups means they differ — a redeploy still rolling out, or\n * replicas deployed from different code — and a card showing only the first\n * instance's would hide that. A path the API withheld cannot tell two\n * instances apart, which is right: nothing on screen would differ either.\n */\nexport function groupTargets(instances: readonly WorkerDto[]): TargetGroup[] {\n const groups = new Map<string, TargetGroup>();\n for (const worker of instances) {\n const id = signature(worker.target);\n const group = groups.get(id);\n if (group === undefined) {\n groups.set(id, { target: worker.target, ids: [worker.id] });\n } else {\n group.ids.push(worker.id);\n }\n }\n return [...groups.values()];\n}\n",
20
+ "import type { WorkerState } from \"../../api/contract\";\nimport type { WorkerDto } from \"../../api/types\";\nimport type { BadgeTone } from \"../../components/Badge\";\nimport { queuePath } from \"../../api/queues\";\nimport { isStale, workerPath, workerState } from \"../../api/workers\";\nimport { Badge } from \"../../components/Badge\";\nimport { Pager } from \"../../components/Pager\";\nimport { RelativeTime } from \"../../components/RelativeTime\";\nimport { Table } from \"../../components/Table\";\nimport { useClientPage } from \"../../components/useClientPage\";\nimport { formatBytes, formatNumber } from \"../../format\";\nimport { Link } from \"../../router\";\nimport { WorkerActions } from \"./actions\";\nimport {\n explainsBlock,\n useCanControlWorkers,\n workerActionGates,\n} from \"./actions/gating\";\nimport {\n summonBadgeLabel,\n summonHint,\n summonUnitHint,\n summonUnitLabel,\n} from \"./summon\";\nimport { targetBadgeLabel, targetHint } from \"./target\";\n\n/**\n * The one worker table, shared by the Workers screen (grouped by server) and\n * the queue screen's Workers panel, so a worker looks and behaves the same\n * wherever it is listed — controls included.\n */\n\n/** How each state shows, and what it means. */\nconst STATE_BADGE: Readonly<\n Record<WorkerState, { label: string; tone: BadgeTone; hint: string }>\n> = {\n running: {\n label: \"Running\",\n tone: \"success\",\n hint: \"Taking jobs from its queue.\",\n },\n paused: {\n label: \"Paused\",\n tone: \"warning\",\n hint: \"Its loop runs, but it takes no new jobs until it is resumed.\",\n },\n stopping: {\n label: \"Stopping\",\n tone: \"neutral\",\n hint: \"Finishing the jobs it holds; it takes no more.\",\n },\n stopped: {\n label: \"Stopped\",\n tone: \"neutral\",\n hint: \"Parked, and still listed, so it can be started again.\",\n },\n restarting: {\n label: \"Restarting\",\n tone: \"accent\",\n hint: \"Taking up changed settings; this lasts milliseconds.\",\n },\n};\n\n/** Props of {@link WorkerTable}. */\nexport interface WorkerTableProps {\n /** The workers to list, already ordered. */\n workers: readonly WorkerDto[];\n /** The table's accessible name, e.g. `Workers of emails`. */\n label: string;\n /** Whether to show the queue column (the Workers screen spans queues; a queue's panel does not). Defaults to `true`. */\n showQueue?: boolean;\n /** Whether a queue name links to its screen (`queues.list`). Defaults to `false`. */\n linkQueues?: boolean;\n /** Whether to show the host and pid columns (a server's group already names them). Defaults to `false`. */\n showHost?: boolean;\n /**\n * Whether a worker's stable key links to its worker page\n * (`/workers/:queue/:key`) — pass `useWorkerPagesRouted()`. Every instance\n * of a key links to the same page; a worker reporting no `key` gets no\n * link. Defaults to `false` (the worker page itself lists one key).\n */\n linkKeys?: boolean;\n /**\n * Whether to offer the Memory column (the Workers page and a worker page's\n * Instances table ask for it; a queue's Workers panel is a narrow control\n * surface and does not). Asking for it is not enough: like the counters, the\n * column exists only when some worker here reports `rssBytes`. Defaults to\n * `false`.\n */\n showMemory?: boolean;\n /**\n * Rows per page, and a pager under the table once the workers outnumber it.\n * Omitted (the default), every worker is a row and there is no pager.\n *\n * **Paging is the table's, not the caller's**, although the caller chooses\n * the size: which columns exist — Memory, Completed/Failed, Actions — is\n * decided from the workers this table was given, so a caller that sliced the\n * rows itself would make a column appear on one page and vanish on the next.\n * The columns are decided over every row; only the rows are cut.\n */\n pageSize?: number;\n}\n\n/**\n * The Memory column's header tooltip. It says the two things a reader can\n * only get wrong once: whose memory it is, and that the figures do not add up\n * to a host's.\n */\nconst MEMORY_HINT =\n \"Resident memory of the process this worker runs in, at its last report — not the worker's own. Workers sharing a pid repeat the same figure, so do not add these up; to size a host, take one row per pid.\";\n\n/** One shared formatter for a heartbeat round trip: a sub-millisecond sample keeps its decimal. */\nconst rttFormat = new Intl.NumberFormat(undefined, {\n maximumFractionDigits: 1,\n});\n\n/**\n * The tooltip for a worker's Heartbeat cell, or `undefined` when it reports\n * no round trip (an older worker, and every worker's first report).\n *\n * It is the *previous* write's sample — a write cannot time itself — and it\n * measures the driver round trip, so it is worded to be mistaken for neither\n * an average nor a network ping.\n */\nfunction heartbeatHint(worker: WorkerDto): string | undefined {\n const rtt = worker.heartbeatRttMs;\n if (rtt === undefined) {\n return undefined;\n }\n return `Last write took ${rttFormat.format(rtt)} ms (the previous report's round trip to the driver, not a network ping).`;\n}\n\n/**\n * A worker's state, plus a note when its record has lapsed, and where its\n * attempts run.\n *\n * The target is a badge here rather than a column of its own because of what\n * it must do when it is **absent**: a worker too old to report it has said\n * nothing, and this table says nothing either — no badge, never \"In process\"\n * (the default, which would mislabel every worker not yet upgraded). A column\n * would have to put something in that row's cell: this table's columns show\n * an absent field as a \"—\" explained as predating it (Completed, Memory),\n * which for the target belongs on the worker page's Target card, or an\n * unexplained blank. The State cell's other badges already follow the\n * present-or-nothing rule, and a badge costs a queue's narrow Workers panel no\n * column. It sits after them, in the neutral tone, and its tooltip says it is\n * how the worker was built, so it does not read as a condition.\n *\n * A summoned worker's badge comes last, under the same present-or-nothing\n * rule: a worker with no `summon` was not summoned or is too old to say, so\n * it shows nothing rather than a \"not summoned\" it never claimed. It names\n * the summoner's kind when there is one, and its tooltip gives what the\n * summoner requested, called a request. A worker in a summon group's shared\n * unit (`summon.group`) gets a second neutral badge right after it, \"Unit of\n * <group>\"; one summoned for a single queue gets none.\n *\n * The state badge carries `data-testid=\"worker-state\"`, so a test or example\n * reads the state by name. Picking it by position would silently read\n * whichever badge a later change put first.\n */\nfunction StateCell({ worker }: { worker: WorkerDto }) {\n const badge = STATE_BADGE[workerState(worker)];\n return (\n <>\n <Badge\n tone={badge.tone}\n title={badge.hint}\n testId=\"worker-state\"\n >\n {badge.label}\n </Badge>{\" \"}\n {worker.control?.pending === true && (\n <Badge\n tone=\"accent\"\n title=\"A change was recorded and this worker has not taken it up yet. It applies within seconds.\"\n >\n Change pending\n </Badge>\n )}{\" \"}\n {isStale(worker) && (\n <Badge\n tone=\"danger\"\n title=\"This worker stopped reporting. Its record lapses unless it reports again.\"\n >\n Not reporting\n </Badge>\n )}{\" \"}\n {worker.target !== undefined && (\n <Badge\n className=\"worker-target\"\n title={targetHint(worker.target)}\n >\n <span className=\"visually-hidden\">Runs in: </span>\n {targetBadgeLabel(worker.target)}\n </Badge>\n )}{\" \"}\n {worker.summon !== undefined && (\n <Badge\n className=\"worker-summon\"\n title={summonHint(worker.summon)}\n testId=\"worker-summon-badge\"\n >\n {summonBadgeLabel(worker.summon)}\n </Badge>\n )}{\" \"}\n {worker.summon?.group !== undefined && (\n <Badge\n className=\"worker-summon\"\n title={summonUnitHint(worker.summon.group)}\n testId=\"worker-summon-unit\"\n >\n {summonUnitLabel(worker.summon.group)}\n </Badge>\n )}\n </>\n );\n}\n\n/**\n * One of a worker's cumulative counters, or a muted dash when it reports none.\n *\n * **Absent is not zero.** A worker older than the counters reports neither\n * field; showing `0` would claim it did nothing. The figure is per\n * incarnation — a restart starts it again — which the column header says.\n */\nfunction CounterCell({\n value,\n}: {\n /** `WorkerDto.completed` or `.failed`: absent on a worker that does not report it. */\n value: number | undefined;\n}) {\n return (\n <td className=\"num\">\n {value === undefined ? (\n <span\n className=\"muted\"\n title=\"This worker does not report its counts (it predates them).\"\n >\n —\n </span>\n ) : (\n formatNumber(value)\n )}\n </td>\n );\n}\n\n/**\n * The memory of the process a worker runs in, or a muted dash when it reports\n * none.\n *\n * **Absent is not zero**, as in {@link CounterCell}: a worker older than the\n * field reports nothing, and `0 B` would claim an empty process. The figure is\n * the *process's* — every worker in one process repeats it — which the column\n * header says.\n */\nfunction MemoryCell({\n value,\n}: {\n /** `WorkerDto.rssBytes`: absent on a worker that does not report it. */\n value: number | undefined;\n}) {\n return (\n <td className=\"num\">\n {value === undefined ? (\n <span\n className=\"muted\"\n title=\"This worker does not report its process memory (it predates it).\"\n >\n —\n </span>\n ) : (\n formatBytes(value)\n )}\n </td>\n );\n}\n\n/**\n * Live workers: id, queue, state (with where its attempts run, when it says),\n * load, what this incarnation completed and failed, its process's memory, the\n * times it reported, and its actions.\n */\nexport function WorkerTable({\n workers,\n label,\n showQueue = true,\n linkQueues = false,\n showHost = false,\n linkKeys = false,\n showMemory: offerMemory = false,\n pageSize,\n}: WorkerTableProps) {\n const canMutate = useCanControlWorkers();\n const page = useClientPage(workers, pageSize ?? null);\n // Asked for, and reported by somebody: the figure rides the heartbeat, so it\n // costs no read, but a table of workers that predate it would be a column of\n // dashes.\n const showMemory =\n offerMemory && workers.some((worker) => worker.rssBytes !== undefined);\n // The counters ride the heartbeat, so they cost no read; the columns exist\n // only when some worker here reports them, and an older worker that does\n // not shows a dash rather than a zero it never claimed.\n const showCounts = workers.some(\n (worker) => worker.completed !== undefined || worker.failed !== undefined,\n );\n // The actions column exists only when some worker here offers something,\n // or says why it offers nothing: a caller with no worker mutations sees the\n // table it saw before.\n const showActions = workers.some((worker) => {\n const gates = workerActionGates(worker, canMutate);\n return (\n gates.pause ||\n gates.resume ||\n gates.stop ||\n gates.start ||\n gates.configure ||\n explainsBlock(gates, canMutate)\n );\n });\n return (\n <>\n <Table label={label}>\n <thead>\n <tr>\n <th scope=\"col\">Worker</th>\n {showQueue && <th scope=\"col\">Queue</th>}\n {showHost && <th scope=\"col\">Host</th>}\n {showHost && (\n <th\n scope=\"col\"\n className=\"num\"\n >\n Pid\n </th>\n )}\n <th scope=\"col\">State</th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Active / concurrency\n </th>\n {showCounts && (\n <th\n scope=\"col\"\n className=\"num\"\n title=\"Jobs this incarnation completed since it started; a restart starts it again.\"\n >\n Completed\n </th>\n )}\n {showCounts && (\n <th\n scope=\"col\"\n className=\"num\"\n title=\"Attempts this incarnation failed since it started; a restart starts it again.\"\n >\n Failed\n </th>\n )}\n {showMemory && (\n <th\n scope=\"col\"\n className=\"num\"\n title={MEMORY_HINT}\n >\n Memory\n </th>\n )}\n <th scope=\"col\">Started</th>\n <th scope=\"col\">Heartbeat</th>\n {showActions && <th scope=\"col\">Actions</th>}\n </tr>\n </thead>\n <tbody>\n {page.rows.map((worker) => (\n <tr\n key={`${worker.queue}/${worker.id}`}\n data-testid={`worker-row-${worker.id}`}\n >\n <th scope=\"row\">\n <code>{worker.id}</code>\n {linkKeys && worker.key !== undefined && (\n <div className=\"worker-key\">\n <Link\n to={workerPath(worker.queue, worker.key)}\n title={`Every instance of ${worker.key}, its settings and its numbers`}\n data-testid={`worker-key-link-${worker.id}`}\n >\n {worker.key}\n </Link>\n </div>\n )}\n </th>\n {showQueue && (\n <td>\n {linkQueues ? (\n <Link to={queuePath(worker.queue)}>{worker.queue}</Link>\n ) : (\n worker.queue\n )}\n </td>\n )}\n {showHost && <td>{worker.host ?? \"—\"}</td>}\n {showHost && <td className=\"num\">{worker.pid ?? \"—\"}</td>}\n <td>\n <StateCell worker={worker} />\n </td>\n <td className=\"num\">\n {formatNumber(worker.active)} /{\" \"}\n {formatNumber(worker.concurrency)}\n </td>\n {showCounts && <CounterCell value={worker.completed} />}\n {showCounts && <CounterCell value={worker.failed} />}\n {showMemory && <MemoryCell value={worker.rssBytes} />}\n <td>\n <RelativeTime value={worker.startedAt} />\n </td>\n <td>\n <RelativeTime\n value={worker.heartbeatAt}\n hint={heartbeatHint(worker)}\n />\n </td>\n {showActions && (\n <td>\n <WorkerActions worker={worker} />\n </td>\n )}\n </tr>\n ))}\n </tbody>\n </Table>\n {page.paged && (\n <Pager\n label={`Pages of ${label}`}\n offset={page.offset}\n limit={page.limit}\n total={page.total}\n itemCount={page.rows.length}\n onChange={page.onChange}\n />\n )}\n </>\n );\n}\n"
21
+ ],
22
+ "mappings": "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",
23
+ "debugId": "5D33D4D714AC86E364756E2164756E21",
24
+ "names": []
25
+ }
@@ -0,0 +1,5 @@
1
+ import{o,h,u}from"./main-a250ffk7.js";var g={started:{label:"Started",tone:"info",hint:"The summoner started compute. It counts as a worker on its way until one registers or the boot budget runs out."},deduped:{label:"Deduplicated",tone:"neutral",hint:"The platform recognised a retried call and started nothing new."},"already-running":{label:"Already running",tone:"neutral",hint:"The platform reported the compute already running."},registered:{label:"Registered",tone:"success",hint:"A summoned worker registered: the attempt did its job."},unavailable:{label:"Unavailable",tone:"warning",hint:"The platform could not start one now: no capacity, a throttled call or a spent quota. It counts towards the backoff and the circuit, except a throttle, which only waits the platform's retry time."},failed:{label:"Failed",tone:"danger",hint:"The summoner's call failed. Counts towards the backoff and the circuit, and an auth or misconfiguration error opens the circuit at once. A call that timed out stays on its way, and counts only if no worker registers in time."},lost:{label:"Lost",tone:"danger",hint:"No worker registered within the boot budget. Counts towards the backoff and the circuit."},"budget-exhausted":{label:"Budget exhausted",tone:"warning",hint:"The hourly or daily attempt budget is used up: nothing is summoned until it resets."},released:{label:"Released",tone:"neutral",hint:"A scale-style summoner was set back to zero."}},f={add:"a job was added",event:"another process's event",poll:"the periodic check",schedule:"a schedule",manual:"a manual summon",timer:"a delayed job coming due"},p={served:"enough workers are running or on their way",pending:"attempts already on their way cover the demand",cooldown:"the cooldown since the last attempt has not passed",backoff:"it is backing off after a failure","circuit-open":"the circuit is open after repeated failures",budget:"the attempt budget is used up",contended:"another controller summoned at the same moment",closed:"the controller is closing",inert:"the controller is inert"},b={"summoned-process":"The API's own process was summoned (or is a runner child), and the policy does not allow summoning from one, so this controller summons nothing.","newer-marker":"A newer version of bun-jobs wrote this queue's summon state: this version leaves it alone, summons nothing for it and cannot read its budget.","unreadable-marker":"This queue's summon state cannot be read: it is not in a shape any version of bun-jobs writes, so its attempts, failures and budget are unknown here."},S={launch:"Launch: starts new units",scale:"Scale: sets a count",wake:"Wake: starts one of a fixed pool"};function s(e,n){return typeof n==="string"&&Object.hasOwn(e,n)}function vt(e){return s(g,e)?g[e]:{label:u(e),tone:"neutral",hint:"An outcome this version of the UI does not know."}}function R(e){return s(f,e)?f[e]:u(e)}function x(e){return s(p,e)?p[e]:u(e)}function En(e){if(e===void 0)return"This controller is inert: it summons nothing.";return s(b,e)?b[e]:`This controller is inert ("${u(e)}"): it summons nothing.`}function wr(e){return e.inertReason!=="unreadable-marker"}function qo(e){return s(S,e)?S[e]:u(e)}function c(e){return Array.isArray(e)?e.map((n)=>u(n)):[]}function Jo(e){let n=vt(e.outcome).label,m=typeof e.count==="number"?` ${h(e.count,"worker")}`:"",i=e.kind?` (${u(e.kind)})`:"",a=e.reason===void 0?"":`, because ${R(e.reason)}`,t=c(e.queues),r=c(e.missing),d=t.length>0?`, for ${t.join(", ")}`:"",w=r.length>0?`; missing ${r.join(", ")}`:"",l=y(e.detail,r);return`${n}${m}${i}${a}${d}${w}${l===void 0?"":`: ${l}`}`}function D(e){return typeof e==="string"&&(e==="partial"||e.startsWith("partial:"))}function y(e,n){if(e===void 0||n.length>0&&D(e))return;return u(e)}function kr(e){return c(e.missing)}function Ho(e){return y(e.detail,kr(e))}function Go(e){return`Partial: no worker registered on ${e.join(", ")}`}var Qo="A shared unit that registers on only some of its queues counts as a failed attempt, so the controller backs off rather than summoning again and again.",T="No worker registered on this queue before the attempt's boot budget ran out.",jo="Coverage",Ko="—",k={registered:(e)=>({label:`✓ ${e}`,tone:"success",title:void 0}),pending:(e)=>({label:`${e} pending`,tone:"neutral",title:void 0}),missing:(e)=>({label:`✗ ${e} missing`,tone:"danger",title:T})};function $o(e,n,m){if(typeof e!=="object"||e===null||Array.isArray(e))return;let i=n??[],a=(t)=>{let r=i.indexOf(t);return r===-1?i.length:r};return Object.keys(e).sort((t,r)=>a(t)-a(r)||(t<r?-1:t>r?1:0)).map((t)=>{let r=e[t],d=s(k,r)?k[r](t):{label:`${t} ${u(r)}`,tone:"neutral",title:void 0};return{queue:t,...d,current:t===m}})}function Wo(e){return e.some((n)=>n.coverage!==void 0)}function Yo(e){return typeof e==="object"&&e!==null&&typeof e.outcome==="string"&&typeof e.kind==="string"}function zo(e){switch(e.action){case"summoned":return`${e.outcome===void 0?"Summoned":vt(e.outcome).label}: attempt ${u(e.id??"")}`;case"skipped":return`Nothing summoned: ${e.reason===void 0?"a guard held it back":x(e.reason)}`;case"released":return"Released: the summoner was set back to zero";case"none":return e.demand===void 0?"Nothing to summon":`Nothing to summon: demand on ${u(e.demand.queue)} is ${o(e.demand.demand)}`;default:return`Summon check: ${u(e.action)}`}}var Zo="This needs queues.summon on every queue of the shared unit, including any you cannot see.";function Xo(e,n){return n&&e.local}
2
+ export{vt,En,wr,qo,Jo,kr,Ho,Go,Qo,jo,Ko,$o,Wo,Yo,zo,Zo,Xo};
3
+
4
+ //# debugId=06E5FA4A83ECD65A64756E2164756E21
5
+ //# sourceMappingURL=main-w93x1dzs.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["app/jobs/screens/queues/panels/summonText.ts"],
4
+ "sourcesContent": [
5
+ "import type {\n PendingSummonDto,\n SummonCheckDto,\n SummonEventDto,\n SummonLastOutcomeDto,\n SummonOutcomeKind,\n SummonReason,\n SummonSkipReason,\n SummonStatusDto,\n} from \"../../../api/types\";\nimport type { BadgeTone } from \"../../../components/Badge\";\nimport { displayText, formatNumber, plural } from \"../../../format\";\n\n/**\n * A queue's summoning in plain words: attempt outcomes, why a check ran, why\n * one did not summon, and why a controller is inert.\n *\n * Each table is an exhaustive record of the values the contract lists, so a\n * value added there is a compile error here until it is named; and each\n * lookup shows a value this build does not know as its raw string, since the\n * contract says a later server may add one.\n */\n\n/** What an outcome means and how it is shown. */\ninterface OutcomeText {\n /** In plain words. */\n label: string;\n /** Its badge colour. */\n tone: BadgeTone;\n /** What it means, for a tooltip. */\n hint: string;\n}\n\n/** Each summon attempt outcome. */\nconst OUTCOMES: Readonly<Record<SummonOutcomeKind, OutcomeText>> = {\n started: {\n label: \"Started\",\n tone: \"info\",\n hint: \"The summoner started compute. It counts as a worker on its way until one registers or the boot budget runs out.\",\n },\n deduped: {\n label: \"Deduplicated\",\n tone: \"neutral\",\n hint: \"The platform recognised a retried call and started nothing new.\",\n },\n \"already-running\": {\n label: \"Already running\",\n tone: \"neutral\",\n hint: \"The platform reported the compute already running.\",\n },\n registered: {\n label: \"Registered\",\n tone: \"success\",\n hint: \"A summoned worker registered: the attempt did its job.\",\n },\n unavailable: {\n label: \"Unavailable\",\n tone: \"warning\",\n hint: \"The platform could not start one now: no capacity, a throttled call or a spent quota. It counts towards the backoff and the circuit, except a throttle, which only waits the platform's retry time.\",\n },\n failed: {\n label: \"Failed\",\n tone: \"danger\",\n hint: \"The summoner's call failed. Counts towards the backoff and the circuit, and an auth or misconfiguration error opens the circuit at once. A call that timed out stays on its way, and counts only if no worker registers in time.\",\n },\n lost: {\n label: \"Lost\",\n tone: \"danger\",\n hint: \"No worker registered within the boot budget. Counts towards the backoff and the circuit.\",\n },\n \"budget-exhausted\": {\n label: \"Budget exhausted\",\n tone: \"warning\",\n hint: \"The hourly or daily attempt budget is used up: nothing is summoned until it resets.\",\n },\n released: {\n label: \"Released\",\n tone: \"neutral\",\n hint: \"A scale-style summoner was set back to zero.\",\n },\n};\n\n/** Why a check ran. */\nconst REASONS: Readonly<Record<SummonReason, string>> = {\n add: \"a job was added\",\n event: \"another process's event\",\n poll: \"the periodic check\",\n schedule: \"a schedule\",\n manual: \"a manual summon\",\n timer: \"a delayed job coming due\",\n};\n\n/** Why a check did not summon. */\nconst SKIPS: Readonly<Record<SummonSkipReason, string>> = {\n served: \"enough workers are running or on their way\",\n pending: \"attempts already on their way cover the demand\",\n cooldown: \"the cooldown since the last attempt has not passed\",\n backoff: \"it is backing off after a failure\",\n \"circuit-open\": \"the circuit is open after repeated failures\",\n budget: \"the attempt budget is used up\",\n contended: \"another controller summoned at the same moment\",\n closed: \"the controller is closing\",\n inert: \"the controller is inert\",\n};\n\n/** Why a controller is inert. */\nconst INERT: Readonly<\n Record<NonNullable<SummonStatusDto[\"inertReason\"]>, string>\n> = {\n \"summoned-process\":\n \"The API's own process was summoned (or is a runner child), and the policy does not allow summoning from one, so this controller summons nothing.\",\n \"newer-marker\":\n \"A newer version of bun-jobs wrote this queue's summon state: this version leaves it alone, summons nothing for it and cannot read its budget.\",\n \"unreadable-marker\":\n \"This queue's summon state cannot be read: it is not in a shape any version of bun-jobs writes, so its attempts, failures and budget are unknown here.\",\n};\n\n/** A summoner's style, from its capabilities (known once the summoner is ready). */\ntype SummonStyle = NonNullable<\n NonNullable<SummonStatusDto[\"summoner\"]>[\"capabilities\"]\n>[\"style\"];\n\n/** Each summoner style in plain words. */\nconst STYLES: Readonly<Record<SummonStyle, string>> = {\n launch: \"Launch: starts new units\",\n scale: \"Scale: sets a count\",\n wake: \"Wake: starts one of a fixed pool\",\n};\n\n/** Whether `key` is an own key of `table` (so `\"toString\"` is not). */\nfunction known<K extends string>(\n table: Readonly<Record<K, unknown>>,\n key: unknown,\n): key is K {\n return typeof key === \"string\" && Object.hasOwn(table, key);\n}\n\n/** An outcome in plain words, with its tone and meaning; a raw string for one this build does not know. */\nexport function summonOutcome(outcome: SummonOutcomeKind): OutcomeText {\n return known(OUTCOMES, outcome)\n ? OUTCOMES[outcome]\n : {\n label: displayText(outcome),\n tone: \"neutral\",\n hint: \"An outcome this version of the UI does not know.\",\n };\n}\n\n/** Why a check ran, in words; a raw string for a reason this build does not know. */\nexport function summonReason(reason: SummonReason): string {\n return known(REASONS, reason) ? REASONS[reason] : displayText(reason);\n}\n\n/** Why a check did not summon, in words; a raw string for one this build does not know. */\nexport function summonSkip(reason: SummonSkipReason): string {\n return known(SKIPS, reason) ? SKIPS[reason] : displayText(reason);\n}\n\n/** Why a controller is inert, in a sentence. */\nexport function summonInert(reason: SummonStatusDto[\"inertReason\"]): string {\n if (reason === undefined) {\n return \"This controller is inert: it summons nothing.\";\n }\n return known(INERT, reason)\n ? INERT[reason]\n : `This controller is inert (\"${displayText(reason)}\"): it summons nothing.`;\n}\n\n/**\n * Whether a status's summon state was read, so its attempts on their way,\n * consecutive failures, backoff, circuit and budget are facts worth showing.\n *\n * Not for `inertReason: \"unreadable-marker\"`: the server could not read the\n * queue's summon state at all, and answers `pending: []` and `failures: 0`\n * as placeholders, not as counts. Shown, they would read as \"nothing on its\n * way, no failures\", which nobody knows. Every other status, inert or not,\n * carries what was read.\n */\nexport function summonStateKnown(\n status: Pick<SummonStatusDto, \"inertReason\">,\n): boolean {\n return status.inertReason !== \"unreadable-marker\";\n}\n\n/** A summoner style in words; a raw string for one this build does not know. */\nexport function summonStyle(style: SummonStyle): string {\n return known(STYLES, style) ? STYLES[style] : displayText(style);\n}\n\n/**\n * A list of names from an event payload, which arrives unchecked: each made\n * safe as text, `[]` for anything but an array.\n */\nfunction nameList(value: unknown): string[] {\n return Array.isArray(value) ? value.map((name) => displayText(name)) : [];\n}\n\n/**\n * A `summon` event's payload in one line, e.g. \"Started 1 worker (ecs),\n * because a job was added\". A shared unit's event names the queues it\n * serves (`queues`, as many as the connection may see) and the ones its\n * attempt registered no worker on (`missing`): \"Registered 1 worker (ecs),\n * for renders, thumbs; missing previews\". The `partial:<queues>` detail\n * such an event carries says what `missing` says, so it is left out then.\n */\nexport function summonEventSummary(event: SummonEventDto): string {\n const outcome = summonOutcome(event.outcome).label;\n const workers =\n typeof event.count === \"number\" ? ` ${plural(event.count, \"worker\")}` : \"\";\n const kind = event.kind ? ` (${displayText(event.kind)})` : \"\";\n const reason =\n event.reason === undefined ? \"\" : `, because ${summonReason(event.reason)}`;\n const queues = nameList(event.queues);\n const missing = nameList(event.missing);\n const unit = queues.length > 0 ? `, for ${queues.join(\", \")}` : \"\";\n const absent = missing.length > 0 ? `; missing ${missing.join(\", \")}` : \"\";\n const detail = shownDetail(event.detail, missing);\n return `${outcome}${workers}${kind}${reason}${unit}${absent}${\n detail === undefined ? \"\" : `: ${detail}`\n }`;\n}\n\n/** Whether a detail is a shared unit's `partial:<queues>` (or bare `partial`). */\nfunction isPartialDetail(detail: unknown): boolean {\n return (\n typeof detail === \"string\" &&\n (detail === \"partial\" || detail.startsWith(\"partial:\"))\n );\n}\n\n/**\n * The detail worth printing beside an outcome, made safe as text: none for a\n * `partial:<queues>` detail when `missing` names those queues already (it is\n * kept on the wire for readers that parse it), and none without a detail.\n */\nfunction shownDetail(\n detail: unknown,\n missing: readonly string[],\n): string | undefined {\n if (detail === undefined || (missing.length > 0 && isPartialDetail(detail))) {\n return undefined;\n }\n return displayText(detail);\n}\n\n/**\n * The queues a summon status's last outcome registered no worker on: a\n * shared unit's partial attempt (`last.missing`), each made safe as text.\n * Empty for any other outcome, and for one an older bun-jobs wrote, whose\n * `partial:<queues>` detail is then shown as it is.\n */\nexport function summonLastMissing(\n last: Pick<SummonLastOutcomeDto, \"missing\">,\n): string[] {\n return nameList(last.missing);\n}\n\n/**\n * The detail the \"Last outcome\" row prints after the outcome: the same rule\n * as {@link summonEventSummary}, so a partial attempt's `partial:<queues>` is\n * left out where `missing` says it in words. `undefined` when there is none\n * to print.\n */\nexport function summonLastDetail(\n last: Pick<SummonLastOutcomeDto, \"detail\" | \"missing\">,\n): string | undefined {\n return shownDetail(last.detail, summonLastMissing(last));\n}\n\n/** A partial attempt in words: \"Partial: no worker registered on previews, thumbs\". */\nexport function summonPartialText(missing: readonly string[]): string {\n return `Partial: no worker registered on ${missing.join(\", \")}`;\n}\n\n/** What a partial attempt means for the controller, under the \"Last outcome\" row. */\nexport const SUMMON_PARTIAL_HINT =\n \"A shared unit that registers on only some of its queues counts as a failed attempt, so the controller backs off rather than summoning again and again.\";\n\n/** A shared unit's progress on one of its queues (`PendingSummonDto.coverage`). */\ntype CoverageState = NonNullable<PendingSummonDto[\"coverage\"]>[string];\n\n/** One queue of an attempt's coverage, as the \"On their way\" table's badge shows it. */\nexport interface CoverageBadge {\n /** The queue. */\n queue: string;\n /** The badge's text: \"✓ renders\", \"renders pending\" or \"✗ renders missing\". */\n label: string;\n /** Its colour: success, neutral or danger. */\n tone: BadgeTone;\n /** Its tooltip: why a missing queue is missing; `undefined` otherwise. */\n title: string | undefined;\n /** Whether it is the queue whose Summon tab this is, marked \"(this queue)\". */\n current: boolean;\n}\n\n/** The tooltip of a queue an attempt's unit registered no worker on in time. */\nexport const COVERAGE_MISSING_HINT =\n \"No worker registered on this queue before the attempt's boot budget ran out.\";\n\n/** The \"On their way\" table's Coverage header. */\nexport const COVERAGE_HEADER = \"Coverage\";\n\n/** What a Coverage cell shows for an attempt with no coverage: one queue's, or a failed read. */\nexport const COVERAGE_NONE = \"—\";\n\n/** Each coverage state's badge, by state, for a queue `queue`. */\nconst COVERAGE: Readonly<\n Record<\n CoverageState,\n (queue: string) => Pick<CoverageBadge, \"label\" | \"tone\" | \"title\">\n >\n> = {\n registered: (queue) => ({\n label: `✓ ${queue}`,\n tone: \"success\",\n title: undefined,\n }),\n pending: (queue) => ({\n label: `${queue} pending`,\n tone: \"neutral\",\n title: undefined,\n }),\n missing: (queue) => ({\n label: `✗ ${queue} missing`,\n tone: \"danger\",\n title: COVERAGE_MISSING_HINT,\n }),\n};\n\n/**\n * An attempt's coverage as the \"On their way\" table's badges: its queues in\n * the order the status lists the unit's (`status.queues`), then any others\n * by name, the queue whose tab this is marked. A state this build does not\n * know reads \"<queue> <state>\" in the neutral tone, as the contract asks.\n *\n * `undefined` where the attempt has no coverage: one queue's controller, or\n * a status whose reads of it failed. The cell then shows\n * {@link COVERAGE_NONE}; so does an empty one.\n */\nexport function attemptCoverage(\n coverage: PendingSummonDto[\"coverage\"],\n order: readonly string[] | undefined,\n queue: string,\n): CoverageBadge[] | undefined {\n if (\n typeof coverage !== \"object\" ||\n coverage === null ||\n Array.isArray(coverage)\n ) {\n return undefined;\n }\n const listed = order ?? [];\n const rank = (name: string) => {\n const index = listed.indexOf(name);\n return index === -1 ? listed.length : index;\n };\n return Object.keys(coverage)\n .sort((a, b) => rank(a) - rank(b) || (a < b ? -1 : a > b ? 1 : 0))\n .map((name) => {\n // A key is always a string; its state arrives unchecked.\n const state: unknown = coverage[name];\n const badge = known(COVERAGE, state)\n ? COVERAGE[state](name)\n : {\n label: `${name} ${displayText(state)}`,\n tone: \"neutral\" as const,\n title: undefined,\n };\n return { queue: name, ...badge, current: name === queue };\n });\n}\n\n/**\n * Whether the \"On their way\" table has a Coverage column: where at least one\n * attempt carries coverage, which only a shared unit's do.\n */\nexport function showCoverage(\n pending: readonly Pick<PendingSummonDto, \"coverage\">[],\n): boolean {\n return pending.some((attempt) => attempt.coverage !== undefined);\n}\n\n/** Whether a queue event's payload is a summon event's. */\nexport function isSummonEvent(payload: unknown): payload is SummonEventDto {\n return (\n typeof payload === \"object\" &&\n payload !== null &&\n typeof (payload as { outcome?: unknown }).outcome === \"string\" &&\n typeof (payload as { kind?: unknown }).kind === \"string\"\n );\n}\n\n/** What \"summon now\" did, for its toast. */\nexport function summonCheckSummary(result: SummonCheckDto): string {\n switch (result.action) {\n case \"summoned\": {\n const outcome =\n result.outcome === undefined\n ? \"Summoned\"\n : summonOutcome(result.outcome).label;\n return `${outcome}: attempt ${displayText(result.id ?? \"\")}`;\n }\n case \"skipped\":\n return `Nothing summoned: ${\n result.reason === undefined\n ? \"a guard held it back\"\n : summonSkip(result.reason)\n }`;\n case \"released\":\n return \"Released: the summoner was set back to zero\";\n case \"none\":\n // The reading names its queue: for a shared unit, its most-starved\n // queue's, which may not be the queue the check was asked about.\n return result.demand === undefined\n ? \"Nothing to summon\"\n : `Nothing to summon: demand on ${displayText(result.demand.queue)} is ${formatNumber(result.demand.demand)}`;\n default:\n return `Summon check: ${displayText(result.action)}`;\n }\n}\n\n/**\n * The error toast of a 403 on \"summon now\" or reset of a shared unit: both\n * need `queues.summon` on every queue the unit serves, and the server's\n * refusal names none, since some may be hidden from the caller.\n */\nexport const SHARED_UNIT_FORBIDDEN =\n \"This needs queues.summon on every queue of the shared unit, including any you cannot see.\";\n\n/**\n * Whether \"summon now\" and reset are offered: the caller may (`queues.summon`,\n * not read-only) **and** the controller runs in the API's process. A status\n * read from another process's controller (`local: false`) is read-only, since\n * both actions need a controller here.\n */\nexport function summonActionsOffered(\n status: Pick<SummonStatusDto, \"local\">,\n canSummon: boolean,\n): boolean {\n return canSummon && status.local;\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "06E5FA4A83ECD65A64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1 @@
1
+ .range-picker{display:flex;align-items:flex-end;gap:var(--space-3);flex-wrap:wrap}.range-picker>.field{min-width:0}.range-scope{margin-top:var(--space-2)}.range-caption{font-size:var(--text-sm);color:var(--text-muted);margin:0}.range-not-retained{padding:var(--space-3);border:1px solid var(--border);border-radius:var(--radius);color:var(--text-muted)}.range-not-retained-title{margin:0 0 var(--space-1);color:var(--text);font-weight:600}.range-not-retained-detail{font-size:var(--text-sm);margin:0}.analytics-section .analytics-headline{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--space-4);margin-bottom:var(--space-4);flex-wrap:wrap}.analytics-section .analytics-charts{display:flex;gap:var(--space-2);flex-direction:column;min-width:0}.analytics-section .analytics-note{font-size:var(--text-sm);color:var(--text-muted);max-width:34rem;margin:0}.overview-jobs-chart{margin-top:var(--space-3)}
@@ -0,0 +1,4 @@
1
+ import{r,Xn,n,e,t,i,y,a,Q,E,H,d,p,Ve,R,o,h,I,ne,qe,Wr,Yr,$e,ks,Xt,bt,st,it,Se,at}from"./main-a250ffk7.js";import{mt}from"./main-gngzht43.js";import{He,Oe,Co}from"./main-kwmqkjfx.js";var _="Runs that threw. Timeouts and kills are counted apart, so this is not the runner's lifetime failed count.",L=30000;function w({label:f,value:s,hint:u}){return t("div",{className:"stat",children:[e("dt",{className:"stat-label",children:f}),e("dd",{className:"stat-value",children:s}),u&&e("dd",{className:"stat-hint",children:u})]})}function M(f,s){let[u,k]=r(()=>({offset:0,limit:s}));return{offset:u.offset<f?u.offset:0,limit:Math.max(1,Math.min(u.limit,s)),setWindow:k}}function ye({range:f,picker:s}){let u=i(),k=at("runners"),S=Co(),{analytics:D,key:C,request:W}=it(f),T=D?.maxSeries??Xn,F=H(Q),b=a({queryKey:qe.runners(C),queryFn:({signal:c})=>Wr(u,W(),void 0,c),enabled:k,refetchInterval:F}),m=b.data?.rows??[],g=M(m.length,T),v=m.slice(g.offset,g.offset+g.limit),A=v.map((c)=>c.runner),P=a({queryKey:qe.runnerSeries(A,C),queryFn:({signal:c})=>Wr(u,W(),A,c),enabled:k&&A.length>0,refetchInterval:L,staleTime:L-5000}),q=new Map((P.data?.seriesByRunner??[]).map((c)=>[c.runner,c]));if(!k)return null;return e(R,{title:"Runners",actions:s,className:"analytics-section",children:t("div",{"data-testid":"runners-analytics",children:[e(st,{range:b.data?.series.range,testId:"runners-range-caption"}),b.isPending?e(p,{label:"Loading runner analytics",showLabel:!0}):b.isError?e(bt,{error:b.error,title:"Could not load runner analytics",onRetry:()=>void b.refetch(),testId:"runners-range-not-retained"}):m.length===0?e(d,{title:"No runners",description:"This API reaches no runners, so there is nothing to chart. A runner with nothing in the range still gets a row of zeros."}):t(n,{children:[e(j,{data:b.data}),t(I,{label:"Runners",children:[e("thead",{children:t("tr",{children:[e("th",{scope:"col",children:"Runner"}),e("th",{scope:"col",className:"num",children:"Started"}),e("th",{scope:"col",className:"num",children:"Succeeded"}),e("th",{scope:"col",className:"num",title:_,children:"Failed"}),e("th",{scope:"col",className:"num",children:"Timed out / killed"}),e("th",{scope:"col",className:"num",children:"Running now"}),e("th",{scope:"col",children:"Runs over time"})]})}),e("tbody",{children:v.map((c)=>t("tr",{"data-testid":`runner-analytics-row-${c.runner}`,children:[e("th",{scope:"row",className:"queue-name",children:S?e(E,{to:mt(c.runner),title:`${c.runner}: its runs, history and settings`,children:c.runner}):c.runner}),e("td",{className:"num",children:o(c.totals.started)}),e("td",{className:"num",children:o(c.totals.succeeded)}),e("td",{className:"num",children:o(c.totals.failed)}),t("td",{className:"num",children:[o(c.totals.timeout)," /"," ",o(c.totals.killed)]}),e("td",{className:"num",children:o(c.runningNow)}),e("td",{children:e(K,{failed:P.isError?P.error:null,entry:q.get(c.runner),render:(l)=>e(Se,{values:l.runs.buckets.map((N)=>N.started),secondary:l.runs.buckets.map((N)=>N.failed),label:`${c.runner}: ${o(l.runs.totals.started)} started, ${o(l.runs.totals.failed)} failed, ${$e(l.runs.range)}`})})})]},c.runner))})]}),m.length>g.limit&&e(ne,{label:"Runner pages",offset:g.offset,limit:g.limit,total:m.length,itemCount:v.length,maxPageSize:T,onChange:g.setWindow}),b.data.truncated&&t("p",{className:"notice",role:"note","data-testid":"runners-truncated",children:["Showing the ",h(m.length,"busiest runner")," of"," ",o(b.data.totalRows),", ranked by runs started in this range."]})]})]})})}function j({data:f}){let{series:s,runningNow:u,truncated:k}=f;return t("div",{className:"analytics-headline",children:[t("dl",{className:"stat-grid","aria-label":"Runner totals",children:[e(w,{label:"Started",value:o(s.totals.started)}),e(w,{label:"Succeeded",value:o(s.totals.succeeded)}),e(w,{label:"Failed",value:o(s.totals.failed),hint:_}),e(w,{label:"Timed out / killed",value:`${o(s.totals.timeout)} / ${o(s.totals.killed)}`}),e(w,{label:"Running now",value:o(u),hint:k?"From the locks, right now — not a figure about the range. Beyond the runners listed it counts only this process's own.":"From the locks, right now — not a figure about the range."})]}),t("div",{className:"analytics-charts",children:[e(Se,{values:s.buckets.map((S)=>S.started),secondary:s.buckets.map((S)=>S.failed),label:`Every runner: ${o(s.totals.started)} started, ${o(s.totals.failed)} failed, ${$e(s.range)}`,width:240}),e(Se,{values:ks(s.buckets),label:`Runs in flight (derived from started minus finished, and drifting across the range boundary), ${$e(s.range)}`,width:240}),t("p",{className:"analytics-note",children:["The second line is ",e("strong",{children:"derived"})," from started minus finished; it drifts across the range boundary. “Running now” above is the honest instantaneous figure."]})]})]})}function ge({range:f,picker:s}){let u=i(),k=at("workers"),S=Oe(),D=y("queues.list"),{analytics:C,key:W,request:T}=it(f),F=C?.maxSeries??Xn,b=H(Q),m=a({queryKey:qe.workers(W),queryFn:({signal:l})=>Yr(u,T(),void 0,l),enabled:k,refetchInterval:b}),g=m.data?.rows??[],v=M(g.length,F),A=g.slice(v.offset,v.offset+v.limit),P=A.map((l)=>l.key),q=a({queryKey:qe.workerSeries(P,W),queryFn:({signal:l})=>Yr(u,T(),P,l),enabled:k&&P.length>0,refetchInterval:L,staleTime:L-5000}),c=new Map((q.data?.seriesByKey??[]).map((l)=>[l.key,l]));if(!k)return null;return e(R,{title:"Workers",actions:s,className:"analytics-section",children:t("div",{"data-testid":"workers-analytics",children:[e(st,{range:m.data?.series.range,testId:"workers-range-caption"}),m.isPending?e(p,{label:"Loading worker analytics",showLabel:!0}):m.isError?e(bt,{error:m.error,title:"Could not load worker analytics",onRetry:()=>void m.refetch(),testId:"workers-range-not-retained"}):g.length===0?e(d,{title:"No workers to show",description:"No worker is live, and none completed or failed anything over the range shown."}):t(n,{children:[e(z,{data:m.data}),t(I,{label:"Workers",children:[e("thead",{children:t("tr",{children:[e("th",{scope:"col",children:"Worker"}),e("th",{scope:"col",children:"Queue"}),e("th",{scope:"col",className:"num",children:"Completed"}),e("th",{scope:"col",className:"num",children:"Failed"}),e("th",{scope:"col",className:"num",children:"Busy"}),e("th",{scope:"col",children:"Jobs over time"})]})}),e("tbody",{children:A.map((l)=>t("tr",{"data-testid":`worker-analytics-row-${l.key}`,children:[e("th",{scope:"row",className:"queue-name",children:S?e(E,{to:He(l.queue,l.key),title:`Every instance of ${l.key}, its settings and its numbers`,children:l.key}):l.key}),e("td",{children:D?e(E,{to:Ve(l.queue),children:l.queue}):l.queue}),e("td",{className:"num",children:o(l.totals.completed)}),e("td",{className:"num",children:o(l.totals.failed)}),e("td",{className:"num",children:l.busyness?l.busyness.samples===0?e("span",{className:"muted",title:"No heartbeat landed in this range, so nothing was sampled — not that it was idle.",children:"—"}):`${l.busyness.activeMean.toFixed(1)} / ${o(l.busyness.concurrency)}`:e("span",{className:"muted",children:"—"})}),e("td",{children:e(K,{failed:q.isError?q.error:null,entry:c.get(l.key),render:(N)=>e(Se,{values:N.jobs.buckets.map((x)=>x.completed),secondary:N.jobs.buckets.map((x)=>x.failed),label:`${l.key}: ${o(N.jobs.totals.completed)} completed, ${o(N.jobs.totals.failed)} failed, ${$e(N.jobs.range)}`})})})]},l.key))})]}),g.length>v.limit&&e(ne,{label:"Worker pages",offset:v.offset,limit:v.limit,total:g.length,itemCount:A.length,maxPageSize:F,onChange:v.setWindow}),m.data.truncated&&t("p",{className:"notice",role:"note","data-testid":"workers-truncated",children:["Showing the ",h(g.length,"busiest worker key")," of"," ",o(m.data.totalRows),", ranked by jobs completed in this range."]})]})]})})}function z({data:f}){let{series:s}=f;return t("div",{className:"analytics-headline",children:[t("dl",{className:"stat-grid","aria-label":"Worker totals",children:[e(w,{label:"Completed",value:o(s.totals.completed)}),e(w,{label:"Failed",value:o(s.totals.failed)}),e(w,{label:"Worker keys",value:o(f.totalRows),hint:"Stable keys, not incarnations: a redeploy keeps one row."})]}),t("div",{className:"analytics-charts",children:[e(Se,{values:s.buckets.map((u)=>u.completed),secondary:s.buckets.map((u)=>u.failed),label:`Every worker: ${o(s.totals.completed)} completed, ${o(s.totals.failed)} failed, ${$e(s.range)}`,width:240}),e("p",{className:"analytics-note","data-testid":"workers-rows-note",children:"A row is a worker key that did work in this range or is live now — a stopped worker keeps its row while its counts are in range, and a live one with nothing to show has a row of zeros. It is not a list of running workers."})]})]})}function K({entry:f,failed:s,render:u}){return e("span",{className:"sparkline-cell",children:f?u(f):s?e("span",{className:"muted",title:s.message,children:Xt(s)?"not kept":"unavailable"}):e("span",{className:"sparkline-placeholder","aria-hidden":"true"})})}export{ye as RunnersSection,ge as WorkersSection};
2
+
3
+ //# debugId=9C0665A8E8CD2DED64756E2164756E21
4
+ //# sourceMappingURL=sections-0y7w4fb4.js.map
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["app/jobs/screens/overview/sections.tsx"],
4
+ "sourcesContent": [
5
+ "import type { ReactNode } from \"react\";\nimport type { TimeRange } from \"../../analytics/range\";\nimport type {\n RunnerAnalyticsRowDto,\n RunnerAnalyticsSeriesDto,\n RunnersAnalyticsDto,\n WorkerAnalyticsRowDto,\n WorkerAnalyticsSeriesDto,\n WorkersAnalyticsDto,\n} from \"../../api/types\";\nimport type { PageWindow } from \"../../components/pagerState\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useState } from \"react\";\nimport { AnalyticsError } from \"../../analytics/AnalyticsError\";\nimport { RangeCaption } from \"../../analytics/RangeCaption\";\nimport { useAnalyticsRange } from \"../../analytics/useAnalyticsRange\";\nimport {\n analyticsKeys,\n derivedInFlight,\n describeAxis,\n getRunnersAnalytics,\n getWorkersAnalytics,\n isRangeNotRetained,\n} from \"../../api/analytics\";\nimport { MAX_ANALYTICS_SERIES } from \"../../api/contract\";\nimport { queuePath } from \"../../api/queues\";\nimport { runnerPath } from \"../../api/runners\";\nimport { workerPath } from \"../../api/workers\";\nimport { Card } from \"../../components/Card\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { Pager } from \"../../components/Pager\";\nimport { Sparkline } from \"../../components/Sparkline\";\nimport { Spinner } from \"../../components/Spinner\";\nimport { Table } from \"../../components/Table\";\nimport { useApiClient } from \"../../context\";\nimport { formatNumber, plural } from \"../../format\";\nimport { usePollInterval } from \"../../live\";\nimport { useCan } from \"../../meta/hooks\";\nimport { POLL_INTERVAL_MS } from \"../../queryClient\";\nimport { Link } from \"../../router\";\nimport { useRunnerPagesRouted, useWorkerPagesRouted } from \"../workers/routed\";\nimport { useAnalyticsGate } from \"./gate\";\nimport \"./sections.css\";\n\n/**\n * The Overview's Runners and Workers sections.\n *\n * **Each section costs two requests, whatever the entity count.** The roll-up\n * read brings back one summed series plus a page of scalar rows, and a single\n * batch read brings back the sparklines of the rows *actually on screen* —\n * `ids=` / `keys=`, capped at `meta.analytics.maxSeries`, which is also the\n * page size, so one page is always exactly one batch request. A namespace with\n * 200 workers therefore costs three series (jobs, the runners roll-up, the\n * workers roll-up) and one page of sparklines, not 200 series.\n *\n * Every axis and every caption is written from the resolution the **response**\n * reports, never the one the request asked for.\n */\n\n/**\n * What a runner series' `failed` counts, said wherever it is shown: a run that\n * **threw**. A timeout and a kill are their own counters, so this is not the\n * lifetime `stat:failed` (which lumps all three) and must never be totalled\n * against it.\n */\nconst FAILED_MEANS =\n \"Runs that threw. Timeouts and kills are counted apart, so this is not the runner's lifetime failed count.\";\n\n/** How often a section's sparklines are re-read; no event announces a bucket, so they poll even while live. */\nconst ANALYTICS_REFETCH_MS = 30_000;\n\n/** What every Overview section takes: the range it shows, and the control to render in its header. */\nexport interface SectionProps {\n /** The range this section is read over. */\n range: TimeRange;\n /** This section's own range control, or `null` while the page-wide one is in force. */\n picker: ReactNode;\n}\n\n/** Props of {@link Figure}. */\ninterface FigureProps {\n /** What it counts. */\n label: string;\n /** The figure, already formatted. */\n value: string;\n /** A qualifier below it, e.g. that a number is derived. */\n hint?: string;\n}\n\n/** One headline figure of a section. */\nfunction Figure({ label, value, hint }: FigureProps) {\n return (\n <div className=\"stat\">\n <dt className=\"stat-label\">{label}</dt>\n <dd className=\"stat-value\">{value}</dd>\n {hint && <dd className=\"stat-hint\">{hint}</dd>}\n </div>\n );\n}\n\n/** What {@link usePage} works out. */\ninterface PageState extends PageWindow {\n /** Sets the window the pager asked for. */\n setWindow: (next: PageWindow) => void;\n}\n\n/**\n * The page of rows on screen, and the pager driving it.\n *\n * The page size is capped at `meta.analytics.maxSeries` so the sparklines of\n * one page are always exactly one batch request; the rows themselves came back\n * with the roll-up, so turning a page costs no further roll-up read.\n */\nfunction usePage(rowCount: number, maxSeries: number): PageState {\n const [window, setWindow] = useState<PageWindow>(() => ({\n offset: 0,\n limit: maxSeries,\n }));\n return {\n // A shrinking list (a narrower range, an entity gone) must not leave the\n // pager stranded past the end with nothing to show.\n offset: window.offset < rowCount ? window.offset : 0,\n limit: Math.max(1, Math.min(window.limit, maxSeries)),\n setWindow,\n };\n}\n\n/** The Overview's Runners section: one summed series, a headline, and a page of runners. */\nexport function RunnersSection({ range, picker }: SectionProps) {\n const api = useApiClient();\n const enabled = useAnalyticsGate(\"runners\");\n // A runner links to its page only where those pages are routed for this\n // caller; otherwise the name is plain text. A row is built from what was\n // recorded in the range, so it can name a runner the API no longer knows —\n // a fleet host reporting counts for ids it holds no record of. The link\n // then lands on the runner screen's own \"no longer exists\" state, which is\n // the honest answer. (Measured by the examples session: `runners.remove()`\n // is *not* a route to that — a removed runner stays in the registry and\n // still answers `GET /runners/:id`. Workers are the opposite: close one and\n // it leaves `GET /workers` while its analytics row remains.)\n const linkRunners = useRunnerPagesRouted();\n const { analytics, key, request } = useAnalyticsRange(range);\n const maxSeries = analytics?.maxSeries ?? MAX_ANALYTICS_SERIES;\n const refetchInterval = usePollInterval(POLL_INTERVAL_MS);\n\n const rollup = useQuery({\n queryKey: analyticsKeys.runners(key),\n queryFn: ({ signal }) =>\n getRunnersAnalytics(api, request(), undefined, signal),\n enabled,\n refetchInterval,\n });\n\n const rows: readonly RunnerAnalyticsRowDto[] = rollup.data?.rows ?? [];\n const page = usePage(rows.length, maxSeries);\n const visible = rows.slice(page.offset, page.offset + page.limit);\n const ids = visible.map((row) => row.runner);\n\n const series = useQuery({\n queryKey: analyticsKeys.runnerSeries(ids, key),\n queryFn: ({ signal }) => getRunnersAnalytics(api, request(), ids, signal),\n enabled: enabled && ids.length > 0,\n refetchInterval: ANALYTICS_REFETCH_MS,\n staleTime: ANALYTICS_REFETCH_MS - 5_000,\n });\n const byId = new Map<string, RunnerAnalyticsSeriesDto>(\n (series.data?.seriesByRunner ?? []).map((entry) => [entry.runner, entry]),\n );\n\n if (!enabled) {\n return null;\n }\n return (\n <Card\n title=\"Runners\"\n actions={picker}\n className=\"analytics-section\"\n >\n <div data-testid=\"runners-analytics\">\n <RangeCaption\n range={rollup.data?.series.range}\n testId=\"runners-range-caption\"\n />\n {rollup.isPending ? (\n <Spinner\n label=\"Loading runner analytics\"\n showLabel\n />\n ) : rollup.isError ? (\n <AnalyticsError\n error={rollup.error}\n title=\"Could not load runner analytics\"\n onRetry={() => void rollup.refetch()}\n testId=\"runners-range-not-retained\"\n />\n ) : rows.length === 0 ? (\n <EmptyState\n title=\"No runners\"\n description=\"This API reaches no runners, so there is nothing to chart. A runner with nothing in the range still gets a row of zeros.\"\n />\n ) : (\n <>\n <RunnersHeadline data={rollup.data} />\n <Table label=\"Runners\">\n <thead>\n <tr>\n <th scope=\"col\">Runner</th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Started\n </th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Succeeded\n </th>\n <th\n scope=\"col\"\n className=\"num\"\n title={FAILED_MEANS}\n >\n Failed\n </th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Timed out / killed\n </th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Running now\n </th>\n <th scope=\"col\">Runs over time</th>\n </tr>\n </thead>\n <tbody>\n {visible.map((row) => (\n <tr\n key={row.runner}\n data-testid={`runner-analytics-row-${row.runner}`}\n >\n <th\n scope=\"row\"\n className=\"queue-name\"\n >\n {linkRunners ? (\n <Link\n to={runnerPath(row.runner)}\n title={`${row.runner}: its runs, history and settings`}\n >\n {row.runner}\n </Link>\n ) : (\n row.runner\n )}\n </th>\n <td className=\"num\">{formatNumber(row.totals.started)}</td>\n <td className=\"num\">\n {formatNumber(row.totals.succeeded)}\n </td>\n <td className=\"num\">{formatNumber(row.totals.failed)}</td>\n <td className=\"num\">\n {formatNumber(row.totals.timeout)} /{\" \"}\n {formatNumber(row.totals.killed)}\n </td>\n <td className=\"num\">{formatNumber(row.runningNow)}</td>\n <td>\n <RowSparkline\n failed={series.isError ? series.error : null}\n entry={byId.get(row.runner)}\n render={(entry) => (\n <Sparkline\n values={entry.runs.buckets.map(\n (bucket) => bucket.started,\n )}\n secondary={entry.runs.buckets.map(\n (bucket) => bucket.failed,\n )}\n label={`${row.runner}: ${formatNumber(entry.runs.totals.started)} started, ${formatNumber(entry.runs.totals.failed)} failed, ${describeAxis(entry.runs.range)}`}\n />\n )}\n />\n </td>\n </tr>\n ))}\n </tbody>\n </Table>\n {rows.length > page.limit && (\n <Pager\n label=\"Runner pages\"\n offset={page.offset}\n limit={page.limit}\n total={rows.length}\n itemCount={visible.length}\n maxPageSize={maxSeries}\n onChange={page.setWindow}\n />\n )}\n {rollup.data.truncated && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"runners-truncated\"\n >\n Showing the {plural(rows.length, \"busiest runner\")} of{\" \"}\n {formatNumber(rollup.data.totalRows)}, ranked by runs started in\n this range.\n </p>\n )}\n </>\n )}\n </div>\n </Card>\n );\n}\n\n/** The Runners section's headline: the summed series, its totals, and the instantaneous `runningNow`. */\nfunction RunnersHeadline({\n data,\n}: {\n /** The roll-up response. */\n data: RunnersAnalyticsDto;\n}) {\n const { series, runningNow, truncated } = data;\n return (\n <div className=\"analytics-headline\">\n <dl\n className=\"stat-grid\"\n aria-label=\"Runner totals\"\n >\n <Figure\n label=\"Started\"\n value={formatNumber(series.totals.started)}\n />\n <Figure\n label=\"Succeeded\"\n value={formatNumber(series.totals.succeeded)}\n />\n <Figure\n label=\"Failed\"\n value={formatNumber(series.totals.failed)}\n hint={FAILED_MEANS}\n />\n <Figure\n label=\"Timed out / killed\"\n value={`${formatNumber(series.totals.timeout)} / ${formatNumber(series.totals.killed)}`}\n />\n <Figure\n label=\"Running now\"\n value={formatNumber(runningNow)}\n hint={\n truncated\n ? \"From the locks, right now — not a figure about the range. Beyond the runners listed it counts only this process's own.\"\n : \"From the locks, right now — not a figure about the range.\"\n }\n />\n </dl>\n <div className=\"analytics-charts\">\n <Sparkline\n values={series.buckets.map((bucket) => bucket.started)}\n secondary={series.buckets.map((bucket) => bucket.failed)}\n label={`Every runner: ${formatNumber(series.totals.started)} started, ${formatNumber(series.totals.failed)} failed, ${describeAxis(series.range)}`}\n width={240}\n />\n <Sparkline\n values={derivedInFlight(series.buckets)}\n label={`Runs in flight (derived from started minus finished, and drifting across the range boundary), ${describeAxis(series.range)}`}\n width={240}\n />\n <p className=\"analytics-note\">\n The second line is <strong>derived</strong> from started minus\n finished; it drifts across the range boundary. “Running now” above is\n the honest instantaneous figure.\n </p>\n </div>\n </div>\n );\n}\n\n/** The Overview's Workers section: one summed series, a headline, and a page of worker keys. */\nexport function WorkersSection({ range, picker }: SectionProps) {\n const api = useApiClient();\n const enabled = useAnalyticsGate(\"workers\");\n // The same gates the Workers page uses: a key links only where the worker\n // pages are routed for this caller, and a queue only with `queues.list`.\n // A row whose key is not linkable still reads as plain text.\n const linkKeys = useWorkerPagesRouted();\n const canQueues = useCan(\"queues.list\");\n const { analytics, key, request } = useAnalyticsRange(range);\n const maxSeries = analytics?.maxSeries ?? MAX_ANALYTICS_SERIES;\n const refetchInterval = usePollInterval(POLL_INTERVAL_MS);\n\n const rollup = useQuery({\n queryKey: analyticsKeys.workers(key),\n queryFn: ({ signal }) =>\n getWorkersAnalytics(api, request(), undefined, signal),\n enabled,\n refetchInterval,\n });\n\n const rows: readonly WorkerAnalyticsRowDto[] = rollup.data?.rows ?? [];\n const page = usePage(rows.length, maxSeries);\n const visible = rows.slice(page.offset, page.offset + page.limit);\n // The stable key, never the per-incarnation id: a rolling redeploy would\n // otherwise end one series and start another.\n const keys = visible.map((row) => row.key);\n\n const series = useQuery({\n queryKey: analyticsKeys.workerSeries(keys, key),\n queryFn: ({ signal }) => getWorkersAnalytics(api, request(), keys, signal),\n enabled: enabled && keys.length > 0,\n refetchInterval: ANALYTICS_REFETCH_MS,\n staleTime: ANALYTICS_REFETCH_MS - 5_000,\n });\n const byKey = new Map<string, WorkerAnalyticsSeriesDto>(\n (series.data?.seriesByKey ?? []).map((entry) => [entry.key, entry]),\n );\n\n if (!enabled) {\n return null;\n }\n return (\n <Card\n title=\"Workers\"\n actions={picker}\n className=\"analytics-section\"\n >\n <div data-testid=\"workers-analytics\">\n <RangeCaption\n range={rollup.data?.series.range}\n testId=\"workers-range-caption\"\n />\n {rollup.isPending ? (\n <Spinner\n label=\"Loading worker analytics\"\n showLabel\n />\n ) : rollup.isError ? (\n <AnalyticsError\n error={rollup.error}\n title=\"Could not load worker analytics\"\n onRetry={() => void rollup.refetch()}\n testId=\"workers-range-not-retained\"\n />\n ) : rows.length === 0 ? (\n <EmptyState\n title=\"No workers to show\"\n description=\"No worker is live, and none completed or failed anything over the range shown.\"\n />\n ) : (\n <>\n <WorkersHeadline data={rollup.data} />\n <Table label=\"Workers\">\n <thead>\n <tr>\n <th scope=\"col\">Worker</th>\n <th scope=\"col\">Queue</th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Completed\n </th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Failed\n </th>\n <th\n scope=\"col\"\n className=\"num\"\n >\n Busy\n </th>\n <th scope=\"col\">Jobs over time</th>\n </tr>\n </thead>\n <tbody>\n {visible.map((row) => (\n <tr\n key={row.key}\n data-testid={`worker-analytics-row-${row.key}`}\n >\n <th\n scope=\"row\"\n className=\"queue-name\"\n >\n {linkKeys ? (\n <Link\n to={workerPath(row.queue, row.key)}\n title={`Every instance of ${row.key}, its settings and its numbers`}\n >\n {row.key}\n </Link>\n ) : (\n row.key\n )}\n </th>\n <td>\n {canQueues ? (\n <Link to={queuePath(row.queue)}>{row.queue}</Link>\n ) : (\n row.queue\n )}\n </td>\n <td className=\"num\">\n {formatNumber(row.totals.completed)}\n </td>\n <td className=\"num\">{formatNumber(row.totals.failed)}</td>\n <td className=\"num\">\n {row.busyness ? (\n row.busyness.samples === 0 ? (\n <span\n className=\"muted\"\n title=\"No heartbeat landed in this range, so nothing was sampled — not that it was idle.\"\n >\n —\n </span>\n ) : (\n `${row.busyness.activeMean.toFixed(1)} / ${formatNumber(row.busyness.concurrency)}`\n )\n ) : (\n <span className=\"muted\">—</span>\n )}\n </td>\n <td>\n <RowSparkline\n failed={series.isError ? series.error : null}\n entry={byKey.get(row.key)}\n render={(entry) => (\n <Sparkline\n values={entry.jobs.buckets.map(\n (bucket) => bucket.completed,\n )}\n secondary={entry.jobs.buckets.map(\n (bucket) => bucket.failed,\n )}\n label={`${row.key}: ${formatNumber(entry.jobs.totals.completed)} completed, ${formatNumber(entry.jobs.totals.failed)} failed, ${describeAxis(entry.jobs.range)}`}\n />\n )}\n />\n </td>\n </tr>\n ))}\n </tbody>\n </Table>\n {rows.length > page.limit && (\n <Pager\n label=\"Worker pages\"\n offset={page.offset}\n limit={page.limit}\n total={rows.length}\n itemCount={visible.length}\n maxPageSize={maxSeries}\n onChange={page.setWindow}\n />\n )}\n {rollup.data.truncated && (\n <p\n className=\"notice\"\n role=\"note\"\n data-testid=\"workers-truncated\"\n >\n Showing the {plural(rows.length, \"busiest worker key\")} of{\" \"}\n {formatNumber(rollup.data.totalRows)}, ranked by jobs completed\n in this range.\n </p>\n )}\n </>\n )}\n </div>\n </Card>\n );\n}\n\n/** The Workers section's headline: the summed throughput series and its totals. */\nfunction WorkersHeadline({\n data,\n}: {\n /** The roll-up response. */\n data: WorkersAnalyticsDto;\n}) {\n const { series } = data;\n return (\n <div className=\"analytics-headline\">\n <dl\n className=\"stat-grid\"\n aria-label=\"Worker totals\"\n >\n <Figure\n label=\"Completed\"\n value={formatNumber(series.totals.completed)}\n />\n <Figure\n label=\"Failed\"\n value={formatNumber(series.totals.failed)}\n />\n <Figure\n label=\"Worker keys\"\n value={formatNumber(data.totalRows)}\n hint=\"Stable keys, not incarnations: a redeploy keeps one row.\"\n />\n </dl>\n <div className=\"analytics-charts\">\n <Sparkline\n values={series.buckets.map((bucket) => bucket.completed)}\n secondary={series.buckets.map((bucket) => bucket.failed)}\n label={`Every worker: ${formatNumber(series.totals.completed)} completed, ${formatNumber(series.totals.failed)} failed, ${describeAxis(series.range)}`}\n width={240}\n />\n <p\n className=\"analytics-note\"\n data-testid=\"workers-rows-note\"\n >\n A row is a worker key that did work in this range or is live now — a\n stopped worker keeps its row while its counts are in range, and a live\n one with nothing to show has a row of zeros. It is not a list of\n running workers.\n </p>\n </div>\n </div>\n );\n}\n\n/** Props of {@link RowSparkline}. */\ninterface RowSparklineProps<TEntry> {\n /** The batch entry for this row, or `undefined` while the batch read is in flight. */\n entry: TEntry | undefined;\n /** The batch read's error, or `null` — a failed batch leaves the rows and only loses their sparklines. */\n failed: Error | null;\n /** Draws the sparkline once the entry arrived. */\n render: (entry: TEntry) => ReactNode;\n}\n\n/** One row's sparkline cell: the chart, \"unavailable\" when the batch failed, or a placeholder while it loads. */\nfunction RowSparkline<TEntry>({\n entry,\n failed,\n render,\n}: RowSparklineProps<TEntry>) {\n return (\n <span className=\"sparkline-cell\">\n {entry ? (\n render(entry)\n ) : failed ? (\n <span\n className=\"muted\"\n title={failed.message}\n >\n {isRangeNotRetained(failed) ? \"not kept\" : \"unavailable\"}\n </span>\n ) : (\n <span\n className=\"sparkline-placeholder\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n );\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "9C0665A8E8CD2DED64756E2164756E21",
9
+ "names": []
10
+ }
@@ -0,0 +1,5 @@
1
+ import{j,Ht,Ge,Gt,Qe,Kn}from"./main-jxre7w93.js";import{G}from"./main-a250ffk7.js";class R{lastEventId;retry;#a;#e="";#i=!1;#n=!0;#t="";#s=[];#r=!1;constructor(e=""){this.lastEventId=e,this.#a=e}push(e){let t=[];if(e.length===0)return t;let n=0;if(this.#n){if(this.#n=!1,e.charCodeAt(0)===65279)n=1}if(this.#i){if(this.#i=!1,e.charCodeAt(n)===10)n++}for(let s=n;s<e.length;s++){let r=e.charCodeAt(s);if(r!==10&&r!==13)continue;if(this.#o(this.#e+e.slice(n,s),t),this.#e="",r===13){if(s+1===e.length)this.#i=!0;else if(e.charCodeAt(s+1)===10)s++}n=s+1}return this.#e+=e.slice(n),t}#o(e,t){if(e.length===0){if(this.lastEventId=this.#a,this.#r)t.push({name:this.#t===""?"message":this.#t,data:this.#s.length===0?void 0:this.#s.join(`
2
+ `),id:this.lastEventId});this.#t="",this.#s=[],this.#r=!1;return}if(e.charCodeAt(0)===58)return;let n=e.indexOf(":"),s=n===-1?e:e.slice(0,n),r=n===-1?"":e.slice(n+1);if(r.charCodeAt(0)===32)r=r.slice(1);if(s==="event")this.#t=r,this.#r=!0;else if(s==="data")this.#s.push(r),this.#r=!0;else if(s==="id"){if(!r.includes("\x00"))this.#a=r}else if(s==="retry"){if(/^\d+$/.test(r))this.retry=Number(r)}}}class F extends j{status;response;constructor(e,t,n){super(`${e}: the event stream ended: ${n}`,{key:e});this.name="StreamClosedError",this.status=t.status,this.response=t}}var Z=Symbol.for("@kingsleyweb/bun-client"),x=Symbol("closed"),ee,te,V=new WeakMap;function ne(e){return typeof e==="object"&&Object.entries(e).some(([t,n])=>t!=="onFailure"&&n!==!1&&n!==void 0)}function se(e){if(e.mode!==void 0)return e.mode==="development";try{return!1}catch{return!0}}function re(e,t){return new Promise((n)=>{let s,r=()=>{clearTimeout(s),t.removeEventListener("abort",r),n()};s=setTimeout(r,e),t.addEventListener("abort",r,{once:!0})})}function ie(e,t){if(t==="text"||e===void 0)return{value:e,parsed:!0};if(e==="")return{value:void 0,parsed:!1};try{return{value:JSON.parse(e),parsed:!0}}catch{return{value:e,parsed:!1}}}class u extends Error{}class _{lastEventId;closed;#a;#e;#i;#n;#t=new AbortController;#s=new Map;#r=[];#o=[];#c;#l;#u=!1;#f=!1;#d=!1;#h;#g;constructor(e,t,n){this.#a=e,this.#e=t,this.#i=n,this.#n=e[Z]??{},this.lastEventId=n.lastEventId??"",this.closed=new Promise((r,m)=>{this.#g=(f)=>f===void 0?r():m(f.value)}),this.closed.catch(()=>{});let s=n.signal;if(s?.aborted)this.#v(s.reason);else s?.addEventListener("abort",()=>this.#v(s.reason),{once:!0})}on(e,t){let n=this.#s.get(e);if(n===void 0)this.#s.set(e,n=new Set);if(n.add(t),!this.#f)this.#f=!0,(async()=>{while(!this.#d)await this.#p()})();return()=>void n.delete(t)}close(){this.#v(x)}[Symbol.asyncIterator](){if(this.#u)throw new j(`${this.#e}: an event stream is iterated once`,{key:this.#e});return this.#u=!0,{next:async()=>{for(;;){let e=this.#r.shift();if(e!==void 0)return{value:e,done:!1};if(this.#d){let t=this.#h;if(this.#h=void 0,t!==void 0)throw t.value;return{value:void 0,done:!0}}if(this.#f)await new Promise((t)=>this.#o.push(t));else await this.#p()}},return:()=>(this.close(),Promise.resolve({value:void 0,done:!0}))}}#v(e){if(this.#d||this.#t.signal.aborted)return;if(this.#t.abort(e),this.#r.length=0,this.#l===void 0)this.#c?.return(),this.#m(e===x?void 0:{value:e})}#m(e){if(this.#d)return;this.#d=!0,this.#h=e,this.#g(e);for(let t of this.#o.splice(0))t()}#p(){return this.#c??=this.#b(),this.#l??=this.#c.next().then((e)=>{if(this.#l=void 0,e.done){let t=this.#t.signal.reason;this.#m(this.#t.signal.aborted&&t!==x?{value:t}:void 0)}else this.#w(e.value)},(e)=>{this.#l=void 0,this.#m({value:e})})}#w(e){if(this.#t.signal.aborted)return;if(this.#y(e.event,e.data,e),e.event==="gap")this.#y("$gap",e.data,e);if(this.#u)this.#r.push(e);for(let t of this.#o.splice(0))t()}#y(e,...t){for(let n of this.#s.get(e)??[])try{n(...t)}catch(s){(this.#n.logger??console).error(`${this.#e}: an event listener threw`,s)}}async#E(){let e=this.#n,t=se(e),n=[e.validate,e.routes?.[this.#e]?.validate,this.#i.validate];if(!(t&&e.schemas!==void 0)&&!n.some(ne))return;let s=V.get(e);if(s===void 0)s=(ee??=(G("ysjfy072"),import("./validation-ysjfy072.js"))).then((f)=>f.createChecks({development:t,source:e.schemas,logger:e.logger??console,errors:{ClientError:j,ValidationError:Qe}})),V.set(e,s);let[r,m]=await Promise.all([s,te??=(G("t7tcm7ra"),import("./sseChecks-t7tcm7ra.js"))]);return{calls:r(this.#e,n),checkEvent:m.checkEvent}}async*#b(){let e=this.#e,t=this.#t.signal,{lastEventId:n,reconnect:s,heartbeatMs:r,fatalStatuses:m,data:f,validate:h,middleware:q,...J}=this.#i,I=this.#n.manifest?.[e],D=f??I?.sseData??"json",H=(s||{}).initialMs??3000,B=(s||{}).maxMs??30000,W=(a)=>m?.includes(a)??(a>=400&&a<500&&a!==408&&a!==429),z=typeof h==="object"&&typeof h.onFailure==="object"&&h.onFailure.response==="drop"?{...h,onFailure:{...h.onFailure,response:"warn"}}:h,N=I?.statuses,P=await this.#E(),Q=(a,y)=>{let v=a.request.headers;if(!v.has("accept"))v.set("accept","text/event-stream");if(this.lastEventId!=="")v.set("last-event-id",this.lastEventId);return y()},O,w=0;for(;;){let a,y=0;try{let i=await this.#a.request(e,{...J,signal:t,validate:z,retry:!1,throwOnError:!1,undeclaredStatus:"result",middleware:[...q??[],Q]}),{response:d,status:o}=i;if(!i.ok){if(W(o)||s===!1)throw N===void 0||N.includes(o)?new Ht(o,i.error,d,e):new F(e,d,`status ${o}`);throw a=Kn(d.headers.get("retry-after")),new u(`status ${o}`)}if(o===204)return;let A=d.headers.get("content-type")??"";if(!/^text\/event-stream\b/i.test(A)||d.body===null)throw await d.body?.cancel().catch(()=>{}),new F(e,d,`status ${o} is not a text/event-stream (${A||"no content-type"})`);y=Date.now();let c=await P?.calls.events(o),E=d.body.getReader(),X=new TextDecoder("utf-8",{ignoreBOM:!0}),g=new R(this.lastEventId),T=!1,b,K=()=>{if(r===void 0)return;clearTimeout(b),b=setTimeout(()=>{T=!0,E.cancel().catch(()=>{})},r*2+5000)},k=()=>void E.cancel().catch(()=>{});t.addEventListener("abort",k,{once:!0});try{K();for(;;){let C;try{C=await E.read()}catch(l){if(t.aborted)return;throw new u("the event stream failed",{cause:l})}if(t.aborted)return;if(T)throw new u(`no byte for ${r*2+5000} ms (heartbeatMs ${r})`);if(C.done){if(s===!1)return;throw new u("the server ended the stream")}K();for(let l of g.push(X.decode(C.value,{stream:!0}))){let M=ie(l.data,D),S=l.id===""?{event:l.name,data:M.value}:{event:l.name,data:M.value,id:l.id};if(c!==void 0){let{value:Y,issues:L}=await P.checkEvent(c.route,o,c.plan,S,{raw:l.data,parsed:M.parsed});if(L.length===0)S=Y;else{let p=new Qe(e,"response",L,o);if(c.mode==="throw")throw p;if(c.mode==="drop"){this.#y("$invalid",p);continue}if(c.mode==="warn")(this.#n.logger??console).warn(p.message);else c.mode(p)}}if(this.lastEventId=l.id,yield S,t.aborted)return}if(this.lastEventId=g.lastEventId,g.retry!==void 0)O=g.retry}}finally{clearTimeout(b),t.removeEventListener("abort",k),k()}}catch(i){if(t.aborted)return;if(!(i instanceof u||i instanceof Ge||i instanceof Gt))throw i;if(s===!1)throw i instanceof u?new Ge(`${e}: ${i.message}`,{key:e,cause:i.cause}):i}if(y!==0&&Date.now()-y>=60000)w=0;w++;let v=O??H,U=Math.min(v*2**(w-1),Math.max(B,v));if(await re(a??U*(0.5+Math.random()*0.5),t),t.aborted)return}}}function ce(e,t,n={}){return new _(e,t,n)}export{ce as sse};
3
+
4
+ //# debugId=0EED57D7DF821B5864756E2164756E21
5
+ //# sourceMappingURL=sse-mn0t0fh7.js.map