rvlib-shipkit 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +1 -1
- package/package.json +3 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +40 -2
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
- package/src/checks/SK024-identity-colors.check.ts +54 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +23 -5
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +30 -2
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/attach.ts +14 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +80 -30
- package/src/core/discover-checks.ts +38 -18
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +222 -0
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +12 -4
- package/src/core/run-ci.ts +46 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +87 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +235 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +174 -0
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +365 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +150 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +175 -0
- package/src/home/supervise.ts +361 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +102 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +22 -3
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +92 -28
- package/src/ledger/store.ts +245 -103
- package/src/ledger/world-cache.ts +64 -15
- package/src/plan/cadence.ts +253 -0
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +428 -0
- package/src/plan/repo-source.ts +212 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +61 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +3 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +8 -2
- package/src/serve/hub-app/header-bar.tsx +102 -95
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +278 -0
- package/src/serve/hub-app/home-strip.css +113 -0
- package/src/serve/hub-app/home-strip.tsx +376 -0
- package/src/serve/hub-app/home.html +21 -0
- package/src/serve/hub-app/hub-app.tsx +221 -177
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +112 -52
- package/src/serve/hub-app/icons.tsx +34 -0
- package/src/serve/hub-app/index.html +7 -1
- package/src/serve/hub-app/keys.ts +60 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +35 -72
- package/src/serve/hub-app/machine-st.ts +333 -0
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +92 -0
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +107 -0
- package/src/serve/hub-app/planned-view.tsx +114 -0
- package/src/serve/hub-app/planned.css +42 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +119 -0
- package/src/serve/hub-app/project-list.tsx +266 -0
- package/src/serve/hub-app/projects-svc.ts +253 -23
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +340 -0
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +41 -14
- package/src/serve/hub-app/timeline-chart.tsx +55 -39
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +7 -6
- package/src/serve/hub-app/timeline-view.tsx +33 -16
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +550 -0
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +121 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +155 -52
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +130 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/checks/contracts.check.ts +0 -20
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/hub-st.ts +0 -250
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
package/src/serve/serve-hub.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// the hub's routes: ONE page (the hub app, a Bun HTML import: media studio,
|
|
2
|
+
// asks, creds, posts, health, wikis as sections), the studio route table as
|
|
3
|
+
// the media API under /media/*, graph canvases as drill-down pages. hubHandler
|
|
4
|
+
// is the routing alone, under a base path; serveHub puts it behind a
|
|
5
|
+
// Bun.serve of its own. Resolving WHAT to mount from repo.config is
|
|
6
|
+
// hub-runtime.ts's job.
|
|
6
7
|
import { existsSync } from 'fs'
|
|
7
8
|
import { join } from 'path'
|
|
8
9
|
import { positionsFile } from 'rvlib-shipkit/src/wiki/graph-positions.ts'
|
|
@@ -19,21 +20,27 @@ import type { ProseView } from '../prose/prose-view.ts'
|
|
|
19
20
|
import type { Timeline } from '../ledger/run-timeline.ts'
|
|
20
21
|
import type { TestCheckView, TestsRootView } from '../collections/test-time.ts'
|
|
21
22
|
import type { LiveView } from '../ledger/live-timeline.ts'
|
|
23
|
+
import type { HouseView } from '../ledger/house-runs.ts'
|
|
22
24
|
|
|
23
25
|
export type { LiveView }
|
|
24
|
-
import type {
|
|
26
|
+
import type { PlanRow } from '../plan/planner.ts'
|
|
25
27
|
import type { HubProblems } from './hub-problems.ts'
|
|
26
28
|
import hubIndex from './hub-app/index.html'
|
|
29
|
+
import { homePort, homeUp } from '../home/home-paths.ts'
|
|
30
|
+
import { RUN_EXIT } from '../home/home-words.ts'
|
|
31
|
+
import type { RepoProcesses } from './repo-processes.ts'
|
|
27
32
|
import { projectGutter } from './project-gutter-route.ts'
|
|
28
33
|
import { sameOrigin } from './same-origin.ts'
|
|
29
34
|
import { LAYOUT } from '../core/layout.ts'
|
|
35
|
+
import { WarmCell } from './warm-cell.ts'
|
|
36
|
+
import { readmeRoutes } from './readme-route.ts'
|
|
30
37
|
|
|
31
38
|
export { sameOrigin }
|
|
32
39
|
|
|
33
40
|
export type HubTool = { path: string; label: string; desc: string }
|
|
34
41
|
|
|
35
42
|
/** what a running gate streams back to the page */
|
|
36
|
-
export type CheckTap = { line: (l: string) => void; check: (e: CheckEvent) => void }
|
|
43
|
+
export type CheckTap = { line: (l: string) => void; check: (e: CheckEvent) => void; job?: (id: string) => void }
|
|
37
44
|
/** the comfy panel, as the hub needs it: the child process it embeds and the
|
|
38
45
|
* box those workflows generate on (src/comfy/comfy-panel.ts owns both) */
|
|
39
46
|
export type ComfyDeps = {
|
|
@@ -45,6 +52,12 @@ export type ComfyDeps = {
|
|
|
45
52
|
export type HubDeps = {
|
|
46
53
|
root: string
|
|
47
54
|
name: string
|
|
55
|
+
/** the machine resources the repo declares in serve.uses */
|
|
56
|
+
uses?: string[]
|
|
57
|
+
/** the long running processes the repo declares in serve.processes */
|
|
58
|
+
processes?: RepoProcesses
|
|
59
|
+
/** a terminal's `shipkit ci` or `check`, run in this process; resolves to its exit code */
|
|
60
|
+
runCli?: (kind: 'ci' | 'check', opts: Record<string, unknown>, print: (l: string) => void) => Promise<number>
|
|
48
61
|
/** 0 = random (tests); real launches pass repo.config/default */
|
|
49
62
|
port: number
|
|
50
63
|
/** every md graph this hub serves, in card order: /graph/<id>. run-serve
|
|
@@ -62,20 +75,23 @@ export type HubDeps = {
|
|
|
62
75
|
* into a tree someone is editing. run-serve owns the routing between the
|
|
63
76
|
* two engines. */
|
|
64
77
|
runChecks: (tap: CheckTap, o?: { only?: string[]; gate?: boolean; by?: InvokedBy; auto?: boolean }) => Promise<number>
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
|
|
78
|
+
/** this repo's rows of the plan (plan/planner.ts): the page reads them to
|
|
79
|
+
* say when a check runs next and why; absent = no plan */
|
|
80
|
+
planned?: () => PlanRow[]
|
|
81
|
+
/** a click: this check runs next, through the plan. false = the plan
|
|
82
|
+
* does not know it */
|
|
83
|
+
ask?: (check: string) => boolean
|
|
68
84
|
/** every run on record, shaped for the timeline page. Absent = the page
|
|
69
85
|
* says there is no record rather than drawing an empty one */
|
|
70
86
|
timeline?: () => Promise<Timeline>
|
|
71
87
|
/** where the test checks' time goes: the `tests` page (check null) or one
|
|
72
88
|
* test check's page (collections/test-time.ts). Absent = 404 */
|
|
73
89
|
tests?: (check: string | null) => Promise<TestsRootView | TestCheckView>
|
|
74
|
-
/**
|
|
75
|
-
*
|
|
90
|
+
/** this repo's live view: the recent runs and the ones in flight, shaped
|
|
91
|
+
* on the server clock */
|
|
76
92
|
live?: () => LiveView
|
|
77
|
-
/**
|
|
78
|
-
|
|
93
|
+
/** the dock's view: every house repo's recent runs (house-runs.ts) */
|
|
94
|
+
house?: () => HouseView
|
|
79
95
|
/** every human-action item, red/yellow, gathered per request from the
|
|
80
96
|
* collection providers (run-serve builds it) */
|
|
81
97
|
needs: () => Promise<NeedItem[]>
|
|
@@ -99,11 +115,40 @@ export type HubDeps = {
|
|
|
99
115
|
* file, so every surface built on the config asks for this first, or a
|
|
100
116
|
* toggle reads as a toggle that did nothing until the next restart */
|
|
101
117
|
refresh?: () => Promise<void>
|
|
102
|
-
/**
|
|
118
|
+
/** a cheap fingerprint of what the rows and the run record read (file
|
|
119
|
+
* stats): a new one marks the warm answers stale. Absent = only a save
|
|
120
|
+
* and their age refresh them */
|
|
121
|
+
stamp?: () => string
|
|
122
|
+
/** RED-only ping tick, called on an interval by the hub runtime (null = off) */
|
|
103
123
|
pingTick?: ((needs: NeedItem[]) => void) | null
|
|
104
124
|
}
|
|
105
125
|
export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; liveReload: boolean; stop: () => void }
|
|
106
126
|
|
|
127
|
+
/** one repo's request handling, apart from any server: `shipkit serve` puts it
|
|
128
|
+
* behind a Bun.serve of its own, the home behind /r/<repo>/ of its one */
|
|
129
|
+
export type HubHandler = {
|
|
130
|
+
tools: HubTool[]
|
|
131
|
+
/** a request whose path is relative to the hub's base ('/api/…', '/hub.json') */
|
|
132
|
+
handle: (req: Request, path: string, srv: Server) => Promise<Response>
|
|
133
|
+
/** a tracked file of the repo moved: every warm answer recomputes behind */
|
|
134
|
+
invalidate: () => void
|
|
135
|
+
/** compute the warm answers now, the rows first: the hub's load */
|
|
136
|
+
warm: () => Promise<void>
|
|
137
|
+
stop: () => void
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** past this age a warm answer recomputes in the background even when no
|
|
141
|
+
* save and no stamp said it moved: what no watch sees (a clock, a wiki in
|
|
142
|
+
* another repo, a file git ignores). Long, because a recompute holds the
|
|
143
|
+
* one process every repo answers from */
|
|
144
|
+
export const WARM_MAX_AGE_MS = 120_000
|
|
145
|
+
|
|
146
|
+
/** the one flat list of the repo, as /api/resources.json carries it before
|
|
147
|
+
* the plan rows and the hub problems, which are read live */
|
|
148
|
+
type Rows = { collections: { id: string; title: string; media: string }[]; resources: Awaited<ReturnType<typeof projectResources>> }
|
|
149
|
+
type Server = ReturnType<typeof Bun.serve>
|
|
150
|
+
type Gutter = ReturnType<typeof projectGutter>
|
|
151
|
+
|
|
107
152
|
/** the tab refreshes ITSELF through bun's hot reload client, which rides in
|
|
108
153
|
* the served page: it swaps a hub-app edit in, and reconnects across the
|
|
109
154
|
* process restart a source edit triggers under `bun --watch`. Bun puts that
|
|
@@ -124,27 +169,79 @@ export function hubTools(d: Pick<HubDeps, 'graphs'>): HubTool[] {
|
|
|
124
169
|
}
|
|
125
170
|
|
|
126
171
|
// drill-down pages (graph canvases) get one fixed home link back to THE page
|
|
127
|
-
const
|
|
128
|
-
background:#141619;border:1px solid #282c31;border-radius:3px;padding:5px 9px}</style><a class=skhome href="/" title="hub — THE page">⌂</a>`
|
|
129
|
-
const withHome = (html: string): string => html.replace(/<body([^>]*)>/, `<body$1>${
|
|
172
|
+
const homeLink = (base: string): string => `<style>.skhome{position:fixed;left:10px;top:10px;z-index:9999;font-size:17px;text-decoration:none;
|
|
173
|
+
background:#141619;border:1px solid #282c31;border-radius:3px;padding:5px 9px}</style><a class=skhome href="${base}/" title="hub — THE page">⌂</a>`
|
|
174
|
+
const withHome = (html: string, base: string): string => html.replace(/<body([^>]*)>/, `<body$1>${homeLink(base)}`)
|
|
130
175
|
|
|
131
|
-
|
|
176
|
+
/** the hub's routes under `base` ('' on a server of its own, '/r/<repo>' in
|
|
177
|
+
* the home). `projects` is the rail, shared when one process serves many */
|
|
178
|
+
export function hubHandler(d: HubDeps, o: { base?: string; projects?: Gutter } = {}): HubHandler {
|
|
179
|
+
const base = o.base ?? ''
|
|
132
180
|
const tools = hubTools(d)
|
|
133
|
-
const projects = projectGutter({ root: d.root })
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
181
|
+
const projects = o.projects ?? projectGutter({ root: d.root })
|
|
182
|
+
// the answers a page waits on when a repo shows, kept warm in memory: a
|
|
183
|
+
// request answers from here and never walks the repo (warm-cell.ts)
|
|
184
|
+
const warmOpts = { stamp: d.stamp, maxAgeMs: WARM_MAX_AGE_MS }
|
|
185
|
+
const rows = new WarmCell<Rows>(async () => {
|
|
186
|
+
await d.refresh?.()
|
|
187
|
+
const cols = d.collections()
|
|
188
|
+
return { collections: cols.map((c) => ({ id: c.id, title: c.title, media: c.media })), resources: await projectResources(cols, d.hubProblems?.collector) }
|
|
189
|
+
}, warmOpts)
|
|
190
|
+
// the run record and the config are all it reads, and the stamp sees both
|
|
191
|
+
const timeline = d.timeline == null ? null : new WarmCell(d.timeline, { stamp: d.stamp })
|
|
192
|
+
const graphs = new Map(d.graphs.map((g) => [g.id, new WarmCell(async () => g.load(), { maxAgeMs: WARM_MAX_AGE_MS })]))
|
|
193
|
+
const fresh = (req: Request): boolean => new URL(req.url).searchParams.get('fresh') === '1'
|
|
194
|
+
return {
|
|
195
|
+
tools,
|
|
196
|
+
invalidate: () => {
|
|
197
|
+
rows.invalidate()
|
|
198
|
+
timeline?.invalidate()
|
|
199
|
+
for (const g of graphs.values()) g.invalidate()
|
|
200
|
+
},
|
|
201
|
+
warm: async () => {
|
|
202
|
+
await rows.warm()
|
|
203
|
+
await timeline?.warm()
|
|
204
|
+
for (const g of graphs.values()) await g.warm()
|
|
205
|
+
},
|
|
206
|
+
stop: () => { d.comfy?.stop() },
|
|
207
|
+
async handle(req, path, srv): Promise<Response> {
|
|
141
208
|
if (!sameOrigin(req)) return new Response('refused: a request that changes something must come from the hub page', { status: 403 })
|
|
142
209
|
// identity probe: lets a second launch tell "same repo, another
|
|
143
210
|
// window" from "a DIFFERENT repo hashed onto my port"
|
|
144
211
|
// `root` rides with it so a page can put a file link into his
|
|
145
212
|
// editor: only the server knows where this repo sits on disk
|
|
146
|
-
|
|
213
|
+
// `uses` tells the home which machine resources this repo needs,
|
|
214
|
+
// `base` where this hub's page sits on the server that answers
|
|
215
|
+
if (path === '/hub.json') return Response.json({ hub: 'shipkit', name: d.name, root: d.root, base, tools, uses: d.uses ?? [] })
|
|
147
216
|
if (path.startsWith('/api/projects')) return (await projects.handle(req, path)) ?? new Response('not found', { status: 404 })
|
|
217
|
+
// where the machine's home serves, and whether it does: a rail
|
|
218
|
+
// click opens the home on that repo, or VS Code while none runs
|
|
219
|
+
if (path === '/api/home-url') return Response.json({ port: homePort(), up: await homeUp() })
|
|
220
|
+
if (path === '/api/run' && req.method === 'POST' && d.runCli != null) {
|
|
221
|
+
const b = (await req.json().catch(() => ({}))) as { kind?: string; opts?: Record<string, unknown> }
|
|
222
|
+
if (b.kind !== 'ci' && b.kind !== 'check') return new Response('kind must be ci or check', { status: 400 })
|
|
223
|
+
const runCli = d.runCli
|
|
224
|
+
// a gate can stay silent for minutes (queued, or one long tsc):
|
|
225
|
+
// Bun would cut the connection after its idle timeout
|
|
226
|
+
srv.timeout(req, 0)
|
|
227
|
+
// wire lines as they come, then one EXIT line the client ends on
|
|
228
|
+
const stream = new ReadableStream<string>({
|
|
229
|
+
start(ctl) {
|
|
230
|
+
const put = (l: string): void => { try { ctl.enqueue(`${l}\n`) } catch { /* the terminal went away: the run finishes anyway */ } }
|
|
231
|
+
void runCli(b.kind as 'ci' | 'check', b.opts ?? {}, put)
|
|
232
|
+
.then((code) => put(`${RUN_EXIT}${code}`), (e: Error) => { put(`FAIL: ${e.message}`); put(`${RUN_EXIT}1`) })
|
|
233
|
+
.finally(() => { try { ctl.close() } catch { /* closed */ } })
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
return new Response(stream, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
|
|
237
|
+
}
|
|
238
|
+
if (path === '/api/processes.json') return Response.json({ processes: d.processes?.views() ?? [] })
|
|
239
|
+
if (path === '/api/processes/act' && req.method === 'POST') {
|
|
240
|
+
const b = (await req.json().catch(() => ({}))) as { id?: string; act?: string }
|
|
241
|
+
if (b.act !== 'start' && b.act !== 'stop' && b.act !== 'restart') return new Response('act must be start, stop or restart', { status: 400 })
|
|
242
|
+
const v = d.processes?.act(b.id ?? '', b.act)
|
|
243
|
+
return v == null ? new Response(`no process '${b.id ?? ''}' in serve.processes`, { status: 404 }) : Response.json(v)
|
|
244
|
+
}
|
|
148
245
|
// the three surfaces built on the config read the config on disk
|
|
149
246
|
if (path === '/api/needs.json' || path === '/api/resources.json' || path === '/api/checks.json' || path === '/api/collections.json') await d.refresh?.()
|
|
150
247
|
if (path === '/api/needs.json') {
|
|
@@ -155,11 +252,11 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
155
252
|
// ONE flat list for the whole repo: the page groups it by
|
|
156
253
|
// path and by type, and a second endpoint would be a second
|
|
157
254
|
// answer to drift from this one
|
|
158
|
-
const
|
|
255
|
+
const r = await rows.get({ fresh: fresh(req) })
|
|
159
256
|
return Response.json({
|
|
160
|
-
collections:
|
|
161
|
-
resources:
|
|
162
|
-
|
|
257
|
+
collections: r.collections,
|
|
258
|
+
resources: r.resources,
|
|
259
|
+
planned: d.planned?.() ?? null,
|
|
163
260
|
hub: d.hubProblems?.list() ?? [],
|
|
164
261
|
})
|
|
165
262
|
}
|
|
@@ -228,17 +325,17 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
228
325
|
}
|
|
229
326
|
}
|
|
230
327
|
if (path === '/api/live.json') {
|
|
231
|
-
if (d.live == null) return Response.json({ now: Date.now(), historyMs: 0, cap: 0, busy: 0, invocations: [],
|
|
328
|
+
if (d.live == null) return Response.json({ now: Date.now(), historyMs: 0, cap: 0, busy: 0, invocations: [], runs: [], queued: 0, blocked: [] } satisfies LiveView)
|
|
232
329
|
return Response.json(d.live(), { headers: { 'cache-control': 'no-store' } })
|
|
233
330
|
}
|
|
234
|
-
if (path === '/api/
|
|
235
|
-
|
|
236
|
-
return Response.json(
|
|
331
|
+
if (path === '/api/house-runs.json') {
|
|
332
|
+
if (d.house == null) return new Response('this hub draws no house runs', { status: 404 })
|
|
333
|
+
return Response.json(d.house(), { headers: { 'cache-control': 'no-store' } })
|
|
237
334
|
}
|
|
238
335
|
if (path === '/api/timeline.json') {
|
|
239
|
-
if (
|
|
336
|
+
if (timeline == null) return new Response('this hub keeps no run record', { status: 404 })
|
|
240
337
|
try {
|
|
241
|
-
return Response.json(await
|
|
338
|
+
return Response.json(await timeline.get({ fresh: fresh(req) }))
|
|
242
339
|
} catch (e) {
|
|
243
340
|
return new Response(`the run record could not be shaped: ${(e as Error).message}`, { status: 500 })
|
|
244
341
|
}
|
|
@@ -252,6 +349,9 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
252
349
|
return new Response(`the test times could not be shaped: ${(e as Error).message}`, { status: 500 })
|
|
253
350
|
}
|
|
254
351
|
}
|
|
352
|
+
// the empty page shows the README, its images served from the repo
|
|
353
|
+
const readme = await readmeRoutes(d.root, path)
|
|
354
|
+
if (readme != null) return readme
|
|
255
355
|
if (path === '/api/status.json') {
|
|
256
356
|
const p = join(d.root, LAYOUT.status)
|
|
257
357
|
return Response.json({ statusMd: existsSync(p) ? await Bun.file(p).text() : null })
|
|
@@ -319,30 +419,33 @@ export function serveHub(d: HubDeps): Hub {
|
|
|
319
419
|
})
|
|
320
420
|
return new Response(stream, { headers: { 'content-type': 'application/x-ndjson; charset=utf-8', 'cache-control': 'no-store' } })
|
|
321
421
|
}
|
|
322
|
-
const page = (s: string): Response => new Response(withHome(s), { headers: { 'content-type': 'text/html; charset=utf-8' } })
|
|
422
|
+
const page = (s: string): Response => new Response(withHome(s, base), { headers: { 'content-type': 'text/html; charset=utf-8' } })
|
|
323
423
|
for (const g of d.graphs) {
|
|
324
424
|
if (!path.startsWith(`/graph/${g.id}`)) continue
|
|
325
|
-
|
|
425
|
+
// the graph as the warm cell holds it: a page open never rescans the wiki
|
|
426
|
+
const held = await graphs.get(g.id)!.get()
|
|
427
|
+
const routes = graphRoutes({ load: () => held, title: g.title, file: positionsFile(d.root, g.id), saveUrl: `${base}/graph/${g.id}/positions` })
|
|
326
428
|
if (path === `/graph/${g.id}`) return page(routes.page())
|
|
327
|
-
if (path === `/graph/${g.id}/graph.json`) return Response.json(
|
|
429
|
+
if (path === `/graph/${g.id}/graph.json`) return Response.json(held)
|
|
328
430
|
if (path === `/graph/${g.id}/positions`) return routes.save(req)
|
|
329
431
|
}
|
|
330
432
|
// the media API namespace — the studio route table, ONE prefix strip
|
|
331
433
|
if (d.studio != null && path.startsWith('/media/')) return d.studio.handle(req, path.slice('/media'.length))
|
|
332
434
|
// stale bookmarks (/assets/, /status, …) land here — name the way home
|
|
333
|
-
return new Response(
|
|
435
|
+
return new Response(`not found — the hub page is at ${base}/`, { status: 404 })
|
|
334
436
|
},
|
|
335
|
-
})
|
|
336
|
-
// RED-only pings ride the serving process: one tick now (state file
|
|
337
|
-
// guarantees a restart never re-fires), then every minute
|
|
338
|
-
let pingTimer: ReturnType<typeof setInterval> | null = null
|
|
339
|
-
if (d.pingTick != null) {
|
|
340
|
-
// tick failures are LOUD (console = the serve terminal), never swallowed
|
|
341
|
-
const tick = (): void => void d.needs().then((n) => d.pingTick?.(n)).catch((e: Error) => console.error(`WARN ping tick broke: ${e.message}`))
|
|
342
|
-
tick()
|
|
343
|
-
pingTimer = setInterval(tick, 60_000)
|
|
344
437
|
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function serveHub(d: HubDeps, h: HubHandler = hubHandler(d)): Hub {
|
|
441
|
+
const server: Server = Bun.serve({
|
|
442
|
+
hostname: '127.0.0.1',
|
|
443
|
+
port: d.port,
|
|
444
|
+
// `/` IS the page — the react app; there is no other surface
|
|
445
|
+
routes: { '/': hubIndex },
|
|
446
|
+
fetch: (req, srv) => h.handle(req, new URL(req.url).pathname, srv),
|
|
447
|
+
})
|
|
345
448
|
// the panel process belongs to this hub: leaving it behind means the next
|
|
346
449
|
// launch adopts a server holding the previous run's modules
|
|
347
|
-
return { server, tools, liveReload: canLiveReload(), stop: () => {
|
|
450
|
+
return { server, tools: h.tools, liveReload: canLiveReload(), stop: () => { h.stop(); server.stop(true) } }
|
|
348
451
|
}
|
|
@@ -20,6 +20,9 @@ export type StaleRestartOpts = {
|
|
|
20
20
|
/** why the file table leaked, null while it did not. Default: this
|
|
21
21
|
* process's own table (core/fd-table.ts) */
|
|
22
22
|
leak?: () => string | null
|
|
23
|
+
/** one line on source that moved under a process with no bun watch, null
|
|
24
|
+
* while nothing moved */
|
|
25
|
+
moved?: () => string | null
|
|
23
26
|
/** a run is in flight: a restart now would kill it */
|
|
24
27
|
busy: () => boolean
|
|
25
28
|
/** never returns when it works. `why` is one line on what went stale */
|
|
@@ -62,6 +65,8 @@ export class StaleRestart {
|
|
|
62
65
|
const disk = this.o.disk()
|
|
63
66
|
// a package.json half written by a bump reads as `?`, and is no verdict
|
|
64
67
|
if (disk !== '?' && disk !== this.o.boot) return { why: `this hub runs shipkit ${this.o.boot} but ${disk} is on disk`, leak: false }
|
|
68
|
+
const moved = this.o.moved?.() ?? null
|
|
69
|
+
if (moved != null) return { why: moved, leak: false }
|
|
65
70
|
const leak = (this.o.leak ?? fdTableLeak)()
|
|
66
71
|
return leak == null ? null : { why: leak, leak: true }
|
|
67
72
|
}
|
|
@@ -1,72 +1,13 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// never two runs at once: a request during a run queues exactly one more, run
|
|
8
|
-
// after it ends, however many saves landed meanwhile.
|
|
9
|
-
import { watch, type FSWatcher } from 'fs'
|
|
1
|
+
// the save watch: which files moving under a repo count as the owner's edit.
|
|
2
|
+
// It only says THAT something moved; the plan (plan/planner.ts) then asks the
|
|
3
|
+
// ledger which checks that moved, so a missed file event costs a stale
|
|
4
|
+
// problems pane until the next save, never a green.
|
|
5
|
+
import { statSync, watch, type FSWatcher } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
10
7
|
import { GENERATED_IGNORE_LINES } from '../rules/untrack-generated.ts'
|
|
11
8
|
import { gitBin } from '../core/git-bin.ts'
|
|
12
9
|
import { LAYOUT } from '../core/layout.ts'
|
|
13
10
|
|
|
14
|
-
export const QUIET_MS = 2000
|
|
15
|
-
|
|
16
|
-
export type Timers = {
|
|
17
|
-
set: (fn: () => void, ms: number) => unknown
|
|
18
|
-
clear: (handle: unknown) => void
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const realTimers: Timers = {
|
|
22
|
-
set: (fn, ms) => setTimeout(fn, ms),
|
|
23
|
-
clear: (h) => clearTimeout(h as ReturnType<typeof setTimeout>),
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class GateTrigger {
|
|
27
|
-
private timer: unknown = null
|
|
28
|
-
private running: Promise<void> | null = null
|
|
29
|
-
private pending = false
|
|
30
|
-
private readonly timers: Timers
|
|
31
|
-
private readonly quietMs: number
|
|
32
|
-
|
|
33
|
-
constructor(private readonly o: { run: () => Promise<unknown>; quietMs?: number; timers?: Timers; onError?: (e: Error) => void }) {
|
|
34
|
-
this.timers = o.timers ?? realTimers
|
|
35
|
-
this.quietMs = o.quietMs ?? QUIET_MS
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** a tracked file moved: the run starts once nothing has moved for the
|
|
39
|
-
* quiet window, so a save burst is one run */
|
|
40
|
-
touched(): void {
|
|
41
|
-
if (this.timer != null) this.timers.clear(this.timer)
|
|
42
|
-
this.timer = this.timers.set(() => {
|
|
43
|
-
this.timer = null
|
|
44
|
-
this.request()
|
|
45
|
-
}, this.quietMs)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** run now, or once more after the run in flight */
|
|
49
|
-
request(): void {
|
|
50
|
-
if (this.running != null) { this.pending = true; return }
|
|
51
|
-
this.running = (async () => {
|
|
52
|
-
do {
|
|
53
|
-
this.pending = false
|
|
54
|
-
try { await this.o.run() } catch (e) { this.o.onError?.(e instanceof Error ? e : new Error(String(e))) }
|
|
55
|
-
} while (this.pending)
|
|
56
|
-
})().finally(() => { this.running = null })
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** resolves when no run is in flight or queued */
|
|
60
|
-
async idle(): Promise<void> {
|
|
61
|
-
while (this.running != null) await this.running
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
stop(): void {
|
|
65
|
-
if (this.timer != null) this.timers.clear(this.timer)
|
|
66
|
-
this.timer = null
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
11
|
/** every path a run of the gate or the hub itself writes, whatever the repo
|
|
71
12
|
* ignores: the status files, the per run logs under the scratch dir, an atomic write's
|
|
72
13
|
* temp file, the comfy state the hub writes at boot. A save there is the
|
|
@@ -106,6 +47,13 @@ export async function gitIgnored(root: string, paths: string[]): Promise<Set<str
|
|
|
106
47
|
}
|
|
107
48
|
}
|
|
108
49
|
|
|
50
|
+
/** a commit, a fetch or a tag moved a ref: the one write inside .git a check
|
|
51
|
+
* keyed on the refs (SK009) must hear. Lock files are git mid write */
|
|
52
|
+
export function isRefMove(rel: string): boolean {
|
|
53
|
+
if (rel.endsWith('.lock')) return false
|
|
54
|
+
return rel === '.git/HEAD' || rel === '.git/packed-refs' || rel.startsWith('.git/refs/')
|
|
55
|
+
}
|
|
56
|
+
|
|
109
57
|
/** calls onChange whenever a file the owner edits moves under root: tracked,
|
|
110
58
|
* or untracked and not ignored. What the gate itself writes (GATE_WRITES,
|
|
111
59
|
* the ledger in .git) never counts, ignored or not, so a run never triggers
|
|
@@ -134,6 +82,7 @@ export function watchTracked(root: string, onChange: () => void, o: { ignored?:
|
|
|
134
82
|
w = watch(root, { recursive: true }, (_event, name) => {
|
|
135
83
|
if (name == null) return onChange()
|
|
136
84
|
const rel = String(name).replace(/\\/g, '/')
|
|
85
|
+
if (isRefMove(rel)) return onChange()
|
|
137
86
|
if (rel === '.git' || rel.startsWith('.git/') || rel.split('/').includes('node_modules') || isGateWrite(rel)) return
|
|
138
87
|
pending.add(rel)
|
|
139
88
|
void flush()
|
|
@@ -146,3 +95,26 @@ export function watchTracked(root: string, onChange: () => void, o: { ignored?:
|
|
|
146
95
|
}
|
|
147
96
|
return { close: () => w?.close(), error }
|
|
148
97
|
}
|
|
98
|
+
|
|
99
|
+
/** did the owner edit anything after `since`: a tracked file, or an untracked
|
|
100
|
+
* one git does not ignore, written later, or a ref that moved. What the gate
|
|
101
|
+
* writes never counts. A boot asks it once per repo, so a burst the previous
|
|
102
|
+
* image had not asked about is picked up (plan/repo-source.ts pending) */
|
|
103
|
+
export async function movedSince(root: string, since: number): Promise<boolean> {
|
|
104
|
+
let out = ''
|
|
105
|
+
try {
|
|
106
|
+
const p = Bun.spawn([gitBin(), 'ls-files', '-z', '--cached', '--others', '--exclude-standard'], { cwd: root, stdout: 'pipe', stderr: 'ignore' })
|
|
107
|
+
out = await new Response(p.stdout).text()
|
|
108
|
+
if ((await p.exited) !== 0) return false
|
|
109
|
+
} catch {
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
const later = (rel: string): boolean => {
|
|
113
|
+
try { return statSync(join(root, rel)).mtimeMs > since } catch { return false }
|
|
114
|
+
}
|
|
115
|
+
for (const rel of out.split('\0')) {
|
|
116
|
+
if (rel === '' || rel.split('/').includes('node_modules') || isGateWrite(rel)) continue
|
|
117
|
+
if (later(rel)) return true
|
|
118
|
+
}
|
|
119
|
+
return later('.git/HEAD') || later('.git/packed-refs')
|
|
120
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// one answer of a hub kept warm in memory: computed when the hub loads,
|
|
2
|
+
// recomputed in the background when something says it moved (a save, a
|
|
3
|
+
// stamp, its age), and served from memory. A request never pays for the
|
|
4
|
+
// walk behind it, except the very first one and one that asks for `fresh`.
|
|
5
|
+
//
|
|
6
|
+
// the one constraint a reader breaks first: `get()` answers what is in
|
|
7
|
+
// memory even when it is known stale. Whoever just changed the truth (an
|
|
8
|
+
// act, the end of a run) asks for `fresh`, which waits on a compute that
|
|
9
|
+
// started after its call.
|
|
10
|
+
|
|
11
|
+
export type WarmCellOpts = {
|
|
12
|
+
/** a cheap fingerprint of what the answer reads (file stats): a new one
|
|
13
|
+
* marks the answer stale */
|
|
14
|
+
stamp?: () => string
|
|
15
|
+
/** past this age the answer is recomputed in the background even when
|
|
16
|
+
* nothing said it moved: what no watch sees (a clock, another repo) */
|
|
17
|
+
maxAgeMs?: number
|
|
18
|
+
now?: () => number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class WarmCell<T> {
|
|
22
|
+
private value: { v: T; at: number; stamp: string } | null = null
|
|
23
|
+
private running: Promise<T> | null = null
|
|
24
|
+
/** bumped by invalidate: a compute that started before it is stale */
|
|
25
|
+
private gen = 0
|
|
26
|
+
private runGen = -1
|
|
27
|
+
|
|
28
|
+
constructor(private readonly compute: () => Promise<T>, private readonly o: WarmCellOpts = {}) {}
|
|
29
|
+
|
|
30
|
+
private now(): number {
|
|
31
|
+
return this.o.now?.() ?? Date.now()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** a compute, shared by every caller while it runs */
|
|
35
|
+
private run(): Promise<T> {
|
|
36
|
+
if (this.running != null) return this.running
|
|
37
|
+
const gen = this.gen
|
|
38
|
+
const stamp = this.o.stamp?.() ?? ''
|
|
39
|
+
this.runGen = gen
|
|
40
|
+
const p = (async (): Promise<T> => {
|
|
41
|
+
try {
|
|
42
|
+
// never synchronously: `running` is set before this settles
|
|
43
|
+
const v = await Promise.resolve().then(() => this.compute())
|
|
44
|
+
this.value = { v, at: this.now(), stamp }
|
|
45
|
+
return v
|
|
46
|
+
} finally {
|
|
47
|
+
this.running = null
|
|
48
|
+
// what said it moved while this ran is not in this answer
|
|
49
|
+
if (this.gen !== gen) this.kick()
|
|
50
|
+
}
|
|
51
|
+
})()
|
|
52
|
+
this.running = p
|
|
53
|
+
return p
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** a compute in the background: on a failure the last answer stays */
|
|
57
|
+
private kick(): void {
|
|
58
|
+
void this.run().catch(() => {})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
get stale(): boolean {
|
|
62
|
+
const v = this.value
|
|
63
|
+
if (v == null) return true
|
|
64
|
+
if (this.o.maxAgeMs != null && this.now() - v.at > this.o.maxAgeMs) return true
|
|
65
|
+
return this.o.stamp != null && this.o.stamp() !== v.stamp
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** the answer in memory, a stale one kicking a recompute behind it. With
|
|
69
|
+
* nothing in memory yet, or `fresh`, it waits on a compute */
|
|
70
|
+
async get(o: { fresh?: boolean } = {}): Promise<T> {
|
|
71
|
+
if (o.fresh === true) {
|
|
72
|
+
// a compute that started before this call may miss what the
|
|
73
|
+
// caller just changed: wait for it, then for one of our own
|
|
74
|
+
if (this.running != null) await this.running.catch(() => {})
|
|
75
|
+
return this.run()
|
|
76
|
+
}
|
|
77
|
+
const v = this.value
|
|
78
|
+
// nothing in memory, a failed compute included: wait on a new one
|
|
79
|
+
if (v == null) return this.run()
|
|
80
|
+
if (this.running == null && (this.stale || this.gen !== this.runGen)) this.kick()
|
|
81
|
+
return v.v
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** something this answer reads moved: recompute in the background */
|
|
85
|
+
invalidate(): void {
|
|
86
|
+
this.gen++
|
|
87
|
+
if (this.running == null) this.kick()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** compute now unless an answer is already in memory: the hub's load */
|
|
91
|
+
async warm(): Promise<void> {
|
|
92
|
+
if (this.value != null) return
|
|
93
|
+
await this.run().catch(() => {})
|
|
94
|
+
}
|
|
95
|
+
}
|