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
|
@@ -19,9 +19,9 @@ import { discoverLegacyChecks } from '../core/discover-legacy-checks.ts'
|
|
|
19
19
|
import { checkFileRunBy } from '../core/legacy-shapes.ts'
|
|
20
20
|
import { ruleCacheExtra } from '../rules/rule.ts'
|
|
21
21
|
import { discoverChecks } from '../core/discover-checks.ts'
|
|
22
|
-
import { checkCacheExtra, describeFirst, type TaskDef, type CheckFrom } from '../check.ts'
|
|
22
|
+
import { checkCacheExtra, describeFirst, type TaskDef, type CheckFrom, type CheckEnv } from '../check.ts'
|
|
23
23
|
import { CHECK_TIMEOUT_MS, type RunnableCheck, type CheckBody } from '../core/check-runner.ts'
|
|
24
|
-
import {
|
|
24
|
+
import { ttlMsOf } from '../core/wire.ts'
|
|
25
25
|
import { budgetOf } from '../core/budget.ts'
|
|
26
26
|
import { whenOfLegacyCheck, whenOfDisabledSlot, whenOfRule, whenOfTask, whenOfCheck, type WhenDecl } from '../core/when.ts'
|
|
27
27
|
import type { CheckDecl } from './git-key.ts'
|
|
@@ -31,6 +31,7 @@ import type { RvtConfig } from '../ts/config.ts'
|
|
|
31
31
|
import { LIB_ROOT } from '../core/lib-root.ts'
|
|
32
32
|
import type { DiscoveredCheck } from '../core/discover-checks.ts'
|
|
33
33
|
import { declaredTestUnits, inputsOutsideUnits, isTestFile } from '../suite/test-units.ts'
|
|
34
|
+
import type { PlanDecl } from '../plan/cadence.ts'
|
|
34
35
|
|
|
35
36
|
/** why an undeclared `run` check cannot be keyed on its reads, or null when it
|
|
36
37
|
* can. A built-in is walked inside the toolkit, a repo check inside the repo
|
|
@@ -73,6 +74,18 @@ export async function allTasks(c: RepoConfig, root: string): Promise<FloorResolu
|
|
|
73
74
|
/** one gate check with everything a reader asks about it, not only what the
|
|
74
75
|
* key layer asks. The cache half stays in `decl` so `CheckDecl` never grows
|
|
75
76
|
* fields the key layer must then decide to ignore. */
|
|
77
|
+
/** a check's `planOff`, read now; one that throws stays planned, and says so */
|
|
78
|
+
function planOffOf(id: string, fn: ((env: CheckEnv) => string | null) | undefined, env: CheckEnv): { planOff?: string } {
|
|
79
|
+
if (fn == null) return {}
|
|
80
|
+
try {
|
|
81
|
+
const why = fn(env)
|
|
82
|
+
return why == null ? {} : { planOff: why }
|
|
83
|
+
} catch (e) {
|
|
84
|
+
console.error(`WARN ${id} planOff threw, so it stays planned: ${(e as Error).message}`)
|
|
85
|
+
return {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
export type CheckRow = {
|
|
77
90
|
decl: CheckDecl
|
|
78
91
|
/** the display name an operator reads (its title, else the id) */
|
|
@@ -118,6 +131,11 @@ export type CheckRow = {
|
|
|
118
131
|
when: WhenDecl
|
|
119
132
|
/** what it waits on, null when it waits on nothing */
|
|
120
133
|
deps: string[] | null
|
|
134
|
+
/** when the planner runs it on its own (check.ts `every`, `cron`,
|
|
135
|
+
* `whenInactive`). Absent: nothing declared, the planner's defaults */
|
|
136
|
+
plan?: PlanDecl
|
|
137
|
+
/** why the plan leaves this check out in this repo (check.ts `planOff`) */
|
|
138
|
+
planOff?: string
|
|
121
139
|
/** whose check it is: `shipkit` = it ships with the toolkit (a built-in
|
|
122
140
|
* SK check, the contracts pass, a floor slot nothing in the repo filled),
|
|
123
141
|
* `repo` = this repo declared it (a package.json script behind a floor
|
|
@@ -150,7 +168,6 @@ function taskBody(t: TaskDef): CheckBody {
|
|
|
150
168
|
}
|
|
151
169
|
}
|
|
152
170
|
|
|
153
|
-
const ttlMsOf = (ttl: string | undefined): number => (ttl == null || ttl === '' ? 0 : parseDuration(ttl))
|
|
154
171
|
|
|
155
172
|
/** the runnable half of a row: everything the runner needs, and nothing it
|
|
156
173
|
* has to look up again */
|
|
@@ -340,6 +357,8 @@ export async function composeChecks(root: string, c: RepoConfig, o: ComposeOpts
|
|
|
340
357
|
when: w,
|
|
341
358
|
deps: u.def.deps ?? null,
|
|
342
359
|
from: u.from,
|
|
360
|
+
...(u.def.every !== undefined || u.def.cron != null || u.def.whenInactive != null ? { plan: { every: u.def.every, cron: u.def.cron, whenInactive: u.def.whenInactive } } : {}),
|
|
361
|
+
...planOffOf(u.id, u.def.planOff, env),
|
|
343
362
|
}
|
|
344
363
|
const body: CheckBody = {
|
|
345
364
|
...(u.def.cmd == null ? {} : { cmd: u.def.cmd }),
|
package/src/ledger/git-scan.ts
CHANGED
|
@@ -145,13 +145,8 @@ export function indexPaths(root: string): { scratch: string; ignored: string; re
|
|
|
145
145
|
// sharing one file across concurrent runners buys no warmth and only adds
|
|
146
146
|
// lock contention
|
|
147
147
|
const scratch = join(dir, `scan-${process.pid}.idx`)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const cutoff = Date.now() - 3_600_000
|
|
152
|
-
for (const f of readdirSync(dir))
|
|
153
|
-
if (/^scan-\d+\.idx(\.tmp)?$/.test(f) && statSync(join(dir, f)).mtimeMs < cutoff) rmSync(join(dir, f), { force: true })
|
|
154
|
-
} catch {}
|
|
148
|
+
sweepScanIndexes(dir, Date.now())
|
|
149
|
+
dropAtExit(scratch)
|
|
155
150
|
// tmp + rename: a half-copied index is a corrupt index, and git's error
|
|
156
151
|
// for that is not the one the retry loop is meant to absorb.
|
|
157
152
|
//
|
|
@@ -180,6 +175,35 @@ export function indexPaths(root: string): { scratch: string; ignored: string; re
|
|
|
180
175
|
return { scratch, ignored, real }
|
|
181
176
|
}
|
|
182
177
|
|
|
178
|
+
/** a scratch index is this process's own: it goes when the process exits */
|
|
179
|
+
const dropping = new Set<string>()
|
|
180
|
+
function dropAtExit(p: string): void {
|
|
181
|
+
if (dropping.size === 0) process.on('exit', () => { for (const f of dropping) { try { rmSync(f, { force: true }) } catch { /* gone either way */ } } })
|
|
182
|
+
dropping.add(p)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** a scratch index older than this is swept whatever its pid: pids are reused */
|
|
186
|
+
export const SCAN_INDEX_KEEP_MS = 60 * 60 * 1000
|
|
187
|
+
|
|
188
|
+
/** the scratch indexes of processes that are gone (killed ones leave theirs),
|
|
189
|
+
* and any past the keep window */
|
|
190
|
+
export function sweepScanIndexes(dir: string, now: number): void {
|
|
191
|
+
try {
|
|
192
|
+
for (const f of readdirSync(dir)) {
|
|
193
|
+
const m = /^scan-(\d+)\.idx(\.tmp)?$/.exec(f)
|
|
194
|
+
if (m == null || Number(m[1]) === process.pid) continue
|
|
195
|
+
const p = join(dir, f)
|
|
196
|
+
try {
|
|
197
|
+
if (statSync(p).mtimeMs < now - SCAN_INDEX_KEEP_MS || !pidAlive(Number(m[1]))) rmSync(p, { force: true })
|
|
198
|
+
} catch { /* raced: gone either way */ }
|
|
199
|
+
}
|
|
200
|
+
} catch { /* no dir: nothing to sweep */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const pidAlive = (pid: number): boolean => {
|
|
204
|
+
try { process.kill(pid, 0); return true } catch (e) { return (e as { code?: string }).code === 'EPERM' }
|
|
205
|
+
}
|
|
206
|
+
|
|
183
207
|
/** the whole work tree as one input. A rule that walks the repo (SK001's test
|
|
184
208
|
* file probe, SK010's `git ls-files`) genuinely reads all of it, and saying so
|
|
185
209
|
* is more honest than naming three of the files it happens to open. Its object
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// THE HOUSE'S RUNS: what the dock at the bottom of the page draws, every
|
|
2
|
+
// house repo's recent runs side by side on one clock. Each repo is shaped the
|
|
3
|
+
// way its own live view is (live-timeline.ts), so a run looks the same
|
|
4
|
+
// wherever it is drawn, and a repo that ran nothing in the window has no group.
|
|
5
|
+
//
|
|
6
|
+
// Pure, on a clock the caller passes. The reads are the caller's
|
|
7
|
+
// (serve/house-runs.ts), which opens a record only when its files moved.
|
|
8
|
+
import type { LiveRow } from './live-runs.ts'
|
|
9
|
+
import type { InvocationRow, RunRow } from './run-log.ts'
|
|
10
|
+
import type { TimelineInvocation } from './run-timeline.ts'
|
|
11
|
+
import { LIVE_HISTORY_MS, shapeLive, type RunSummary } from './live-timeline.ts'
|
|
12
|
+
|
|
13
|
+
/** one repo's record, as read for the window */
|
|
14
|
+
export type HouseRecord = { repo: string; live: LiveRow[]; runs: RunRow[]; invocations: InvocationRow[] }
|
|
15
|
+
|
|
16
|
+
/** one repo that ran something in the window: its runs, newest first, and
|
|
17
|
+
* what it has in flight */
|
|
18
|
+
export type HouseGroup = { repo: string; invocations: TimelineInvocation[]; busy: number; runs: RunSummary[] }
|
|
19
|
+
|
|
20
|
+
export type HouseView = {
|
|
21
|
+
now: number
|
|
22
|
+
historyMs: number
|
|
23
|
+
/** the runner's cap, drawn over the concurrency band */
|
|
24
|
+
cap: number
|
|
25
|
+
/** checks in flight across every repo */
|
|
26
|
+
busy: number
|
|
27
|
+
/** by repo name, so a group keeps its place from one answer to the next */
|
|
28
|
+
groups: HouseGroup[]
|
|
29
|
+
/** what holds the machine's plan back, when the host has one (Planner.held) */
|
|
30
|
+
held?: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function shapeHouse(o: { now: number; cap: number; repos: HouseRecord[]; alive?: (pid: number) => boolean; historyMs?: number }): HouseView {
|
|
34
|
+
const historyMs = o.historyMs ?? LIVE_HISTORY_MS
|
|
35
|
+
const groups = o.repos
|
|
36
|
+
.map((r): HouseGroup => {
|
|
37
|
+
const v = shapeLive({ now: o.now, cap: o.cap, live: r.live, runs: r.runs, invocations: r.invocations, alive: o.alive, historyMs })
|
|
38
|
+
return { repo: r.repo, invocations: v.invocations, busy: v.busy, runs: v.runs }
|
|
39
|
+
})
|
|
40
|
+
.filter((g) => g.invocations.length > 0)
|
|
41
|
+
.sort((a, b) => a.repo.localeCompare(b.repo))
|
|
42
|
+
return { now: o.now, historyMs, cap: o.cap, busy: groups.reduce((a, g) => a + g.busy, 0), groups }
|
|
43
|
+
}
|
package/src/ledger/ledger.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// stored under the identity of the question it answered, replayed only when
|
|
3
3
|
// that identity is bit-for-bit the same question again.
|
|
4
4
|
//
|
|
5
|
-
// ledger/git-key.ts owns the identity, ledger/store.ts owns the
|
|
5
|
+
// ledger/git-key.ts owns the identity, ledger/store.ts owns the table. This
|
|
6
6
|
// module owns the POLICY, and the policy is where a cache turns into a false
|
|
7
7
|
// green, so every rule below exists because a specific failure was reproduced
|
|
8
8
|
// during the design pass:
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
// on disk. The same set is re-checked at commit, so an edit landing
|
|
39
39
|
// between the read and the store throws the answer away (rule 2). A body
|
|
40
40
|
// that reached the world (exec, fetch, tool) stores nothing.
|
|
41
|
-
import {
|
|
41
|
+
import { join } from 'path'
|
|
42
42
|
import type { RvtConfig } from '../ts/config.ts'
|
|
43
43
|
import type { Line } from '../core/wire.ts'
|
|
44
44
|
import { KeyLayer, type CheckDecl, type CheckKey } from './git-key.ts'
|
|
45
|
-
import {
|
|
45
|
+
import { shipkitRoot } from './db.ts'
|
|
46
|
+
import { Answers, answersDb, FRESH_ENV, type AnswerRef, type AnswerWrite } from './store.ts'
|
|
46
47
|
import type { Problem } from '../problems/problem-line.ts'
|
|
47
48
|
import { cachedStamp, outcomeCounts } from '../core/outcome-words.ts'
|
|
48
49
|
import { readSetDrift, type ReadSet } from '../core/read-set.ts'
|
|
@@ -119,6 +120,8 @@ export function ledgerReadEnabled(): boolean {
|
|
|
119
120
|
|
|
120
121
|
export class Ledger {
|
|
121
122
|
private readonly buffered = new Map<string, Stored>()
|
|
123
|
+
/** answers replayed this run, stamped as used in the commit's one write */
|
|
124
|
+
private readonly replayed: AnswerRef[] = []
|
|
122
125
|
private viewMemo: RepoView | null = null
|
|
123
126
|
private readonly keys = new Map<string, CheckKey>()
|
|
124
127
|
private hits = 0
|
|
@@ -128,7 +131,10 @@ export class Ledger {
|
|
|
128
131
|
|
|
129
132
|
private constructor(
|
|
130
133
|
readonly layer: KeyLayer,
|
|
131
|
-
|
|
134
|
+
/** the repo's answers, one handle for the run, closed by commit() */
|
|
135
|
+
private readonly answers: Answers,
|
|
136
|
+
/** the attach slots dir (core/attach.ts) */
|
|
137
|
+
private readonly inflight: string,
|
|
132
138
|
/** replays are allowed */
|
|
133
139
|
readonly reading: boolean,
|
|
134
140
|
private readonly now: () => number,
|
|
@@ -138,7 +144,7 @@ export class Ledger {
|
|
|
138
144
|
const layer = KeyLayer.open(root, decls, { cliEntry: o.cliEntry, cfg: o.cfg, scope: o.scope })
|
|
139
145
|
const fresh = o.fresh === true || process.env[FRESH_ENV] != null
|
|
140
146
|
const reading = (o.read ?? ledgerReadEnabled()) && !fresh
|
|
141
|
-
return new Ledger(layer,
|
|
147
|
+
return new Ledger(layer, new Answers(answersDb(root)), join(shipkitRoot(root), 'inflight'), reading, o.now ?? Date.now)
|
|
142
148
|
}
|
|
143
149
|
|
|
144
150
|
/** the key for a check, computed once per run and remembered: `keyFor`
|
|
@@ -150,8 +156,8 @@ export class Ledger {
|
|
|
150
156
|
return k
|
|
151
157
|
}
|
|
152
158
|
|
|
153
|
-
private
|
|
154
|
-
return
|
|
159
|
+
private read(k: CheckKey): { payload: Stored } | null {
|
|
160
|
+
return this.answers.read<Stored>(NS, k.id, k.key!)
|
|
155
161
|
}
|
|
156
162
|
|
|
157
163
|
/** the file list this run lists and validates against, taken once */
|
|
@@ -166,10 +172,11 @@ export class Ledger {
|
|
|
166
172
|
const k = this.key(id)
|
|
167
173
|
if (k.key == null) { this.unkeyed++; return null }
|
|
168
174
|
if (!this.reading) { this.misses++; return null }
|
|
169
|
-
const e =
|
|
175
|
+
const e = this.read(k)
|
|
170
176
|
if (e == null || !Array.isArray(e.payload.lines)) { this.misses++; return null }
|
|
171
177
|
if (k.population === 'reads' && (e.payload.reads == null || this.driftOf(e.payload.reads, () => this.view()) != null)) { this.misses++; return null }
|
|
172
178
|
this.hits++
|
|
179
|
+
this.replayed.push({ ns: NS, name: k.id, key: k.key })
|
|
173
180
|
return e.payload
|
|
174
181
|
}
|
|
175
182
|
|
|
@@ -177,7 +184,7 @@ export class Ledger {
|
|
|
177
184
|
storedReads(id: string): ReadSet | null {
|
|
178
185
|
const k = this.key(id)
|
|
179
186
|
if (k.key == null || k.population !== 'reads') return null
|
|
180
|
-
return
|
|
187
|
+
return this.read(k)?.payload.reads ?? null
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
/** the view is asked for only when a glob needs it: a read set with no
|
|
@@ -200,7 +207,7 @@ export class Ledger {
|
|
|
200
207
|
// a READS key names the question and not the tree, so two runs over
|
|
201
208
|
// different trees share it: only one over this tree may answer
|
|
202
209
|
if (k.population === 'reads') return null
|
|
203
|
-
return k.key == null ? null :
|
|
210
|
+
return k.key == null ? null : inflightSlot(this.inflight, NS, k.id, k.key)
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
/** a check that missed the store and then adopted another run's in
|
|
@@ -227,13 +234,21 @@ export class Ledger {
|
|
|
227
234
|
/** re-hash, then write what still describes the tree on disk. */
|
|
228
235
|
commit(): CommitReport {
|
|
229
236
|
const empty: CommitReport = { stored: [], discarded: [], drifted: [], error: null, ms: 0 }
|
|
230
|
-
if (this.buffered.size === 0)
|
|
237
|
+
if (this.buffered.size === 0) {
|
|
238
|
+
this.answers.write([], { touched: this.replayed.splice(0), now: this.now() })
|
|
239
|
+
this.answers.close()
|
|
240
|
+
return empty
|
|
241
|
+
}
|
|
231
242
|
const started = performance.now()
|
|
232
243
|
const d = this.layer.rescan()
|
|
233
|
-
if (d.error != null)
|
|
244
|
+
if (d.error != null) {
|
|
245
|
+
this.answers.close()
|
|
246
|
+
return { ...empty, discarded: [...this.buffered.keys()].sort(), error: d.error, ms: performance.now() - started }
|
|
247
|
+
}
|
|
234
248
|
const touched = this.layer.unitsTouchedBy(d.drifted)
|
|
235
249
|
const stored: string[] = []
|
|
236
250
|
const discarded: string[] = []
|
|
251
|
+
const rows: AnswerWrite[] = []
|
|
237
252
|
const drifted = [...d.drifted]
|
|
238
253
|
// a fresh list, never this run's: the question is whether the tree
|
|
239
254
|
// still is what the body read
|
|
@@ -246,10 +261,14 @@ export class Ledger {
|
|
|
246
261
|
if (moved != null) { discarded.push(id); drifted.push(moved); continue }
|
|
247
262
|
}
|
|
248
263
|
const k = this.key(id)
|
|
249
|
-
|
|
264
|
+
rows.push({ ns: NS, name: k.id, key: k.key!, payload: outcome, at: this.now() })
|
|
250
265
|
stored.push(id)
|
|
251
266
|
}
|
|
252
|
-
|
|
267
|
+
// one transaction for every answer and every replay stamp. A write
|
|
268
|
+
// that fails stores nothing, which costs the next run a measurement
|
|
269
|
+
if (!this.answers.write(rows, { touched: this.replayed.splice(0), now: this.now() })) stored.length = 0
|
|
270
|
+
this.answers.sweep(this.now())
|
|
271
|
+
this.answers.close()
|
|
253
272
|
return { stored: stored.sort(), discarded: discarded.sort(), drifted, error: null, ms: performance.now() - started }
|
|
254
273
|
}
|
|
255
274
|
|
|
@@ -276,6 +295,13 @@ export class Ledger {
|
|
|
276
295
|
}
|
|
277
296
|
}
|
|
278
297
|
|
|
298
|
+
/** where askers of one question meet while one of them measures it: a file
|
|
299
|
+
* name per (ns, id, key), hashed so any id is a safe name */
|
|
300
|
+
export function inflightSlot(dir: string, ns: string, id: string, key: string): string {
|
|
301
|
+
const h = new Bun.CryptoHasher('sha256').update(`${ns}\0${id}\0${key}`).digest('hex').slice(0, 12)
|
|
302
|
+
return join(dir, ns, `${id.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 60)}.${h}`)
|
|
303
|
+
}
|
|
304
|
+
|
|
279
305
|
/** stamp a cached check's lines. The FIRST line carries the word, because
|
|
280
306
|
* that is the line an agent reads, and a cached answer that renders
|
|
281
307
|
* identically to one that ran is the one thing this design refuses to ship. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// THE LIVE TIMELINE:
|
|
1
|
+
// THE LIVE TIMELINE: one repo's recent runs and its runs in flight. The same
|
|
2
2
|
// shape the runs page reads (ledger/run-timeline.ts), cut to a rolling
|
|
3
|
-
// window that ends now, with the runs still going in it
|
|
4
|
-
//
|
|
3
|
+
// window that ends now, with the runs still going in it. Every house repo's
|
|
4
|
+
// view side by side is the dock at the bottom of the page (house-runs.ts).
|
|
5
5
|
//
|
|
6
6
|
// Pure, on a clock the caller passes, so every rule is pinned on a fake one.
|
|
7
7
|
// A run still in the live table is drawn from there, its running checks
|
|
@@ -12,12 +12,9 @@ import { liveRowsOf, type LiveRow, type LiveWait } from './live-runs.ts'
|
|
|
12
12
|
import type { InvocationRow, RunRow } from './run-log.ts'
|
|
13
13
|
import { invocationsOf, type TimelineBar, type TimelineInvocation } from './run-timeline.ts'
|
|
14
14
|
|
|
15
|
-
/** how far back the server looks: the
|
|
15
|
+
/** how far back the server looks: the widest window a live chart draws */
|
|
16
16
|
export const LIVE_HISTORY_MS = 600_000
|
|
17
17
|
|
|
18
|
-
/** another repo with something in flight, collapsed to one lane */
|
|
19
|
-
export type RepoLane = { repo: string; label: string; by: string; start: number; running: number; checks: string[] }
|
|
20
|
-
|
|
21
18
|
/** a run in flight here, for the header's one line */
|
|
22
19
|
export type RunSummary = { runId: string; label: string; by: string; done: number; total: number; reds: number }
|
|
23
20
|
|
|
@@ -31,7 +28,6 @@ export type LiveView = {
|
|
|
31
28
|
/** every invocation touching the window, newest first, the ones in flight
|
|
32
29
|
* marked running */
|
|
33
30
|
invocations: TimelineInvocation[]
|
|
34
|
-
repos: RepoLane[]
|
|
35
31
|
runs: RunSummary[]
|
|
36
32
|
/** of the newest run in flight: checks ready for a slot, and checks
|
|
37
33
|
* waiting on another one */
|
|
@@ -53,15 +49,6 @@ export function invocationOfLive(r: LiveRow, now: number): TimelineInvocation {
|
|
|
53
49
|
}
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
/** another repo, collapsed: one lane while it has anything running, from
|
|
57
|
-
* its earliest running check to now. null when nothing runs there */
|
|
58
|
-
export function repoLane(repo: string, rows: LiveRow[], now: number, alive?: (pid: number) => boolean): RepoLane | null {
|
|
59
|
-
const going = liveRowsOf(rows, now, alive).filter((r) => r.ended == null && r.running.length > 0)
|
|
60
|
-
if (going.length === 0) return null
|
|
61
|
-
const running = going.flatMap((r) => r.running)
|
|
62
|
-
return { repo, label: going[0]!.label, by: going[0]!.by, start: Math.min(...running.map((c) => c.started)), running: running.length, checks: running.map((c) => c.id) }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
52
|
export function shapeLive(o: {
|
|
66
53
|
now: number
|
|
67
54
|
cap: number
|
|
@@ -69,7 +56,6 @@ export function shapeLive(o: {
|
|
|
69
56
|
/** the run record's rows that finished inside the window */
|
|
70
57
|
runs: RunRow[]
|
|
71
58
|
invocations: InvocationRow[]
|
|
72
|
-
others?: { repo: string; rows: LiveRow[] }[]
|
|
73
59
|
alive?: (pid: number) => boolean
|
|
74
60
|
historyMs?: number
|
|
75
61
|
}): LiveView {
|
|
@@ -89,7 +75,6 @@ export function shapeLive(o: {
|
|
|
89
75
|
cap: o.cap,
|
|
90
76
|
busy: inFlight.reduce((a, r) => a + r.running.length, 0),
|
|
91
77
|
invocations,
|
|
92
|
-
repos: (o.others ?? []).map((x) => repoLane(x.repo, x.rows, o.now, o.alive)).filter((x) => x != null).sort((a, b) => a.repo.localeCompare(b.repo)),
|
|
93
78
|
runs: inFlight.map((r) => ({ runId: r.runId, label: r.label, by: r.by, done: r.done, total: r.total, reds: r.reds })),
|
|
94
79
|
queued: newest?.waiting.filter((w) => w.deps.length === 0).length ?? 0,
|
|
95
80
|
blocked: newest?.waiting.filter((w) => w.deps.length > 0) ?? [],
|
package/src/ledger/run-log.ts
CHANGED
|
@@ -23,10 +23,16 @@ import { existsSync } from 'fs'
|
|
|
23
23
|
import type { Line } from '../core/wire.ts'
|
|
24
24
|
import { BUSY_MS, dbPath, INSERT_RUN, openDb, UPSERT_INVOCATION, UPSERT_LAST, UPSERT_LIVE } from './db.ts'
|
|
25
25
|
import { LIVE_BEAT_MS, liveRowDead, type LiveRow, type LiveState } from './live-runs.ts'
|
|
26
|
-
import { KEEP_MS } from './store.ts'
|
|
27
26
|
|
|
28
27
|
export type RunStatus = 'pass' | 'warn' | 'fail' | 'skip'
|
|
29
28
|
|
|
29
|
+
/** how long a row of the record survives */
|
|
30
|
+
export const KEEP_MS = 30 * 24 * 60 * 60 * 1000
|
|
31
|
+
|
|
32
|
+
/** the most rows each record table keeps, newest first. A busy repo writes
|
|
33
|
+
* tens of thousands of run rows a day, so age alone is no cap */
|
|
34
|
+
export const MAX_ROWS = 200_000
|
|
35
|
+
|
|
30
36
|
/** one check, one run. `ms` is what it MEASURED: a replay measured nothing and
|
|
31
37
|
* says 0, so the two can never be averaged together by accident. */
|
|
32
38
|
export type RunRow = {
|
|
@@ -48,9 +54,7 @@ export const runsPath = dbPath
|
|
|
48
54
|
|
|
49
55
|
/** what wrote a run: the command (`ci` answers the gate's question, `check`
|
|
50
56
|
* the health pass's), who started it (a terminal, the hub's buttons, the
|
|
51
|
-
*
|
|
52
|
-
* narrowed health pass is not a health pass: the scheduler reads the last
|
|
53
|
-
* WHOLE one. */
|
|
57
|
+
* plan), and whether it was narrowed to named checks. */
|
|
54
58
|
export type InvocationKind = 'ci' | 'check'
|
|
55
59
|
export type InvokedBy = 'cli' | 'hub' | 'schedule'
|
|
56
60
|
export type Invocation = { kind: InvocationKind; label: string; by: InvokedBy; narrowed: boolean }
|
|
@@ -228,28 +232,57 @@ const unwritable = (path: string): Line[] => [
|
|
|
228
232
|
|
|
229
233
|
/** the age sweep, over every table that grows per run. A live row nobody
|
|
230
234
|
* writes anymore is a run lost mid way, and it goes here too */
|
|
231
|
-
const sweepAll = (db: Database, now: number, cutoff: number): void => {
|
|
235
|
+
const sweepAll = (db: Database, now: number, cutoff: number, max?: number): void => {
|
|
232
236
|
sweepDeadLive(db, now)
|
|
233
237
|
db.query('delete from runs where at < ?').run(cutoff)
|
|
234
238
|
db.query('delete from invocations where ended < ?').run(cutoff)
|
|
235
239
|
db.query('delete from suites where at < ?').run(cutoff)
|
|
236
240
|
db.query('delete from suite_runs where at < ?').run(cutoff)
|
|
241
|
+
if (max == null) return
|
|
242
|
+
for (const [table, col] of RECORD_TABLES) db.query(`delete from ${table} where rowid in (select rowid from ${table} order by ${col} desc limit -1 offset ?)`).run(max)
|
|
237
243
|
}
|
|
238
244
|
|
|
245
|
+
/** every record table and the column its age is read from */
|
|
246
|
+
const RECORD_TABLES = [['runs', 'at'], ['invocations', 'ended'], ['suites', 'at'], ['suite_runs', 'at']] as const
|
|
247
|
+
|
|
248
|
+
/** the biggest record table's row count and the oldest row, null when the db
|
|
249
|
+
* cannot be read */
|
|
250
|
+
export function recordUse(path: string): { count: number; oldest: number | null } | null {
|
|
251
|
+
const opened = openDb(path, { create: false })
|
|
252
|
+
if (opened == null) return null
|
|
253
|
+
try {
|
|
254
|
+
let count = 0
|
|
255
|
+
let oldest: number | null = null
|
|
256
|
+
for (const [table, col] of RECORD_TABLES) {
|
|
257
|
+
const r = opened.db.query(`select count(*) as n, min(${col}) as o from ${table}`).get() as { n: number; o: number | null }
|
|
258
|
+
count = Math.max(count, Number(r.n))
|
|
259
|
+
if (r.o != null) oldest = oldest == null ? r.o : Math.min(oldest, r.o)
|
|
260
|
+
}
|
|
261
|
+
return { count, oldest }
|
|
262
|
+
} catch {
|
|
263
|
+
return null
|
|
264
|
+
} finally {
|
|
265
|
+
try { opened.db.close() } catch { /* already gone */ }
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
type RawInvocation = { runId: string; kind: string; label: string; by: string; narrowed: number; started: number; ended: number }
|
|
270
|
+
|
|
271
|
+
const INVOCATIONS_SINCE = 'select run_id as runId, kind, label, by, narrowed, started, ended from invocations where ended >= ? order by started asc'
|
|
272
|
+
|
|
273
|
+
const invocationRow = (r: RawInvocation): InvocationRow => ({
|
|
274
|
+
runId: String(r.runId), kind: r.kind === 'ci' ? 'ci' : 'check', label: String(r.label),
|
|
275
|
+
by: r.by === 'hub' || r.by === 'schedule' ? r.by : 'cli', narrowed: Number(r.narrowed) === 1,
|
|
276
|
+
started: Number(r.started), ended: Number(r.ended),
|
|
277
|
+
})
|
|
278
|
+
|
|
239
279
|
/** every invocation on record that ended at or after `since`, oldest
|
|
240
280
|
* first. An unreadable db is none. */
|
|
241
281
|
export function readInvocations(path: string, since = 0): InvocationRow[] {
|
|
242
282
|
const opened = openDb(path, { create: false })
|
|
243
283
|
if (opened == null) return []
|
|
244
284
|
try {
|
|
245
|
-
|
|
246
|
-
runId: string; kind: string; label: string; by: string; narrowed: number; started: number; ended: number
|
|
247
|
-
}[]
|
|
248
|
-
return rows.map((r): InvocationRow => ({
|
|
249
|
-
runId: String(r.runId), kind: r.kind === 'ci' ? 'ci' : 'check', label: String(r.label),
|
|
250
|
-
by: r.by === 'hub' || r.by === 'schedule' ? r.by : 'cli', narrowed: Number(r.narrowed) === 1,
|
|
251
|
-
started: Number(r.started), ended: Number(r.ended),
|
|
252
|
-
}))
|
|
285
|
+
return (opened.db.query(INVOCATIONS_SINCE).all(since) as RawInvocation[]).map(invocationRow)
|
|
253
286
|
} catch {
|
|
254
287
|
return []
|
|
255
288
|
} finally {
|
|
@@ -309,14 +342,17 @@ export function readLiveReadonly(path: string): LiveRow[] {
|
|
|
309
342
|
|
|
310
343
|
/** every check's last outcome in a record, read only, for a hub that shows
|
|
311
344
|
* ANOTHER repo's gate: `[]` when the record or the table is missing */
|
|
312
|
-
export function readLastStatusReadonly(path: string): { id: string; status: string }[]
|
|
345
|
+
export function readLastStatusReadonly(path: string): { id: string; status: string; at: number }[]
|
|
346
|
+
/** with `lines`: what each check last printed too, as its raw json */
|
|
347
|
+
export function readLastStatusReadonly(path: string, o: { lines: true }): { id: string; status: string; at: number; lines: string }[]
|
|
348
|
+
export function readLastStatusReadonly(path: string, o: { lines?: boolean } = {}): { id: string; status: string; at: number; lines?: string }[] {
|
|
313
349
|
if (!existsSync(path)) return []
|
|
314
350
|
let db: Database | null = null
|
|
315
351
|
try {
|
|
316
352
|
db = new Database(path, { readonly: true })
|
|
317
353
|
db.exec('pragma busy_timeout = 50')
|
|
318
354
|
if (db.query("select 1 from sqlite_master where type = 'table' and name = 'check_last'").get() == null) return []
|
|
319
|
-
return db.query(
|
|
355
|
+
return db.query(`select id, status, at${o.lines === true ? ', lines' : ''} from check_last`).all() as { id: string; status: string; at: number; lines?: string }[]
|
|
320
356
|
} catch {
|
|
321
357
|
return []
|
|
322
358
|
} finally {
|
|
@@ -324,6 +360,26 @@ export function readLastStatusReadonly(path: string): { id: string; status: stri
|
|
|
324
360
|
}
|
|
325
361
|
}
|
|
326
362
|
|
|
363
|
+
/** every check's last result and how many runs in a row ended on that same
|
|
364
|
+
* status: what the planner hands a check's `every` lambda. `{}` when the
|
|
365
|
+
* record cannot be read, so a plan look never crashes on it */
|
|
366
|
+
export function readLastResults(path: string): Record<string, { status: RunStatus; at: number; streak: number }> {
|
|
367
|
+
const opened = openDb(path, { create: false })
|
|
368
|
+
if (opened == null) return {}
|
|
369
|
+
const out: Record<string, { status: RunStatus; at: number; streak: number }> = {}
|
|
370
|
+
try {
|
|
371
|
+
const rows = opened.db.query(`select l.id as id, l.status as status, l.at as at,
|
|
372
|
+
(select count(*) from runs r where r.id = l.id and r.at > coalesce((select max(r2.at) from runs r2 where r2.id = l.id and r2.status != l.status), 0)) as streak
|
|
373
|
+
from runs l where l.rowid = (select r3.rowid from runs r3 where r3.id = l.id order by r3.at desc, r3.rowid desc limit 1)`).all() as { id: string; status: string; at: number; streak: number }[]
|
|
374
|
+
for (const r of rows) out[r.id] = { status: r.status as RunStatus, at: Number(r.at), streak: Math.max(1, Number(r.streak)) }
|
|
375
|
+
} catch {
|
|
376
|
+
return {}
|
|
377
|
+
} finally {
|
|
378
|
+
try { opened.db.close() } catch { /* already gone */ }
|
|
379
|
+
}
|
|
380
|
+
return out
|
|
381
|
+
}
|
|
382
|
+
|
|
327
383
|
/** the rows that finished at or after `since`, oldest first: the recent
|
|
328
384
|
* window a live chart draws, without reading a fortnight of history */
|
|
329
385
|
export function readRunsSince(path: string, since: number): RunRow[] {
|
|
@@ -339,18 +395,26 @@ export function readRunsSince(path: string, since: number): RunRow[] {
|
|
|
339
395
|
}
|
|
340
396
|
}
|
|
341
397
|
|
|
342
|
-
/**
|
|
343
|
-
*
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
398
|
+
/** what a live chart of ANOTHER repo draws from its record, strictly read
|
|
399
|
+
* only and in one open: its live rows, and the runs and invocations that
|
|
400
|
+
* finished at or after `since`. A table the record does not have yet is
|
|
401
|
+
* none, and so is a record that cannot be opened */
|
|
402
|
+
export function readWindowReadonly(path: string, since: number): { live: LiveRow[]; runs: RunRow[]; invocations: InvocationRow[] } {
|
|
403
|
+
const out = { live: [] as LiveRow[], runs: [] as RunRow[], invocations: [] as InvocationRow[] }
|
|
404
|
+
if (!existsSync(path)) return out
|
|
405
|
+
let db: Database | null = null
|
|
347
406
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
407
|
+
db = new Database(path, { readonly: true })
|
|
408
|
+
db.exec('pragma busy_timeout = 50')
|
|
409
|
+
const has = (t: string): boolean => db!.query("select 1 from sqlite_master where type = 'table' and name = ?").get(t) != null
|
|
410
|
+
if (has('live')) out.live = (db.query(LIVE_SELECT).all() as RawLive[]).map(liveRowOf).filter((r) => r != null)
|
|
411
|
+
if (has('invocations')) out.invocations = (db.query(INVOCATIONS_SINCE).all(since) as RawInvocation[]).map(invocationRow)
|
|
412
|
+
if (has('runs') && has('invocations')) out.runs = (db.query(`${RUN_ROWS} where r.at >= ? order by r.at asc, r.rowid asc`).all(since) as RawRow[]).map(runRow)
|
|
413
|
+
return out
|
|
350
414
|
} catch {
|
|
351
|
-
return
|
|
415
|
+
return out
|
|
352
416
|
} finally {
|
|
353
|
-
try {
|
|
417
|
+
try { db?.close() } catch { /* already gone */ }
|
|
354
418
|
}
|
|
355
419
|
}
|
|
356
420
|
|
|
@@ -417,13 +481,13 @@ export function runsStamp(path: string): string {
|
|
|
417
481
|
}
|
|
418
482
|
}
|
|
419
483
|
|
|
420
|
-
/**
|
|
421
|
-
* written while it runs is kept
|
|
422
|
-
export function sweepRuns(path: string, now: number, keepMs = KEEP_MS): void {
|
|
484
|
+
/** the record's cap, enforced: by age, then by count per table. A DELETE, so
|
|
485
|
+
* a row written while it runs is kept */
|
|
486
|
+
export function sweepRuns(path: string, now: number, keepMs = KEEP_MS, max = MAX_ROWS): void {
|
|
423
487
|
const opened = openDb(path, { create: false })
|
|
424
488
|
if (opened == null) return
|
|
425
489
|
try {
|
|
426
|
-
sweepAll(opened.db, now, now - keepMs)
|
|
490
|
+
sweepAll(opened.db, now, now - keepMs, max)
|
|
427
491
|
} catch {
|
|
428
492
|
// housekeeping never fails a run
|
|
429
493
|
} finally {
|