rvlib-shipkit 1.1.0 → 1.2.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 +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- 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/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -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 +208 -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 +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -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 +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -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/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- 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 +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- 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 +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- 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/{hub-st.ts → repo-st.ts} +155 -84
- 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/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- 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 +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -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/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
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. */
|
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,12 +232,38 @@ 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)
|
|
243
|
+
}
|
|
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
|
+
}
|
|
237
267
|
}
|
|
238
268
|
|
|
239
269
|
/** every invocation on record that ended at or after `since`, oldest
|
|
@@ -324,31 +354,36 @@ export function readLastStatusReadonly(path: string): { id: string; status: stri
|
|
|
324
354
|
}
|
|
325
355
|
}
|
|
326
356
|
|
|
327
|
-
/**
|
|
328
|
-
*
|
|
329
|
-
|
|
357
|
+
/** every check's last result and how many runs in a row ended on that same
|
|
358
|
+
* status: what the planner hands a check's `every` lambda. `{}` when the
|
|
359
|
+
* record cannot be read, so a plan look never crashes on it */
|
|
360
|
+
export function readLastResults(path: string): Record<string, { status: RunStatus; at: number; streak: number }> {
|
|
330
361
|
const opened = openDb(path, { create: false })
|
|
331
|
-
if (opened == null) return
|
|
362
|
+
if (opened == null) return {}
|
|
363
|
+
const out: Record<string, { status: RunStatus; at: number; streak: number }> = {}
|
|
332
364
|
try {
|
|
333
|
-
const rows = opened.db.query(
|
|
334
|
-
|
|
365
|
+
const rows = opened.db.query(`select l.id as id, l.status as status, l.at as at,
|
|
366
|
+
(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
|
|
367
|
+
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 }[]
|
|
368
|
+
for (const r of rows) out[r.id] = { status: r.status as RunStatus, at: Number(r.at), streak: Math.max(1, Number(r.streak)) }
|
|
335
369
|
} catch {
|
|
336
|
-
return
|
|
370
|
+
return {}
|
|
337
371
|
} finally {
|
|
338
372
|
try { opened.db.close() } catch { /* already gone */ }
|
|
339
373
|
}
|
|
374
|
+
return out
|
|
340
375
|
}
|
|
341
376
|
|
|
342
|
-
/**
|
|
343
|
-
*
|
|
344
|
-
export function
|
|
377
|
+
/** the rows that finished at or after `since`, oldest first: the recent
|
|
378
|
+
* window a live chart draws, without reading a fortnight of history */
|
|
379
|
+
export function readRunsSince(path: string, since: number): RunRow[] {
|
|
345
380
|
const opened = openDb(path, { create: false })
|
|
346
|
-
if (opened == null) return
|
|
381
|
+
if (opened == null) return []
|
|
347
382
|
try {
|
|
348
|
-
const
|
|
349
|
-
return
|
|
383
|
+
const rows = opened.db.query(`${RUN_ROWS} where r.at >= ? order by r.at asc, r.rowid asc`).all(since) as RawRow[]
|
|
384
|
+
return rows.map(runRow)
|
|
350
385
|
} catch {
|
|
351
|
-
return
|
|
386
|
+
return []
|
|
352
387
|
} finally {
|
|
353
388
|
try { opened.db.close() } catch { /* already gone */ }
|
|
354
389
|
}
|
|
@@ -417,13 +452,13 @@ export function runsStamp(path: string): string {
|
|
|
417
452
|
}
|
|
418
453
|
}
|
|
419
454
|
|
|
420
|
-
/**
|
|
421
|
-
* written while it runs is kept
|
|
422
|
-
export function sweepRuns(path: string, now: number, keepMs = KEEP_MS): void {
|
|
455
|
+
/** the record's cap, enforced: by age, then by count per table. A DELETE, so
|
|
456
|
+
* a row written while it runs is kept */
|
|
457
|
+
export function sweepRuns(path: string, now: number, keepMs = KEEP_MS, max = MAX_ROWS): void {
|
|
423
458
|
const opened = openDb(path, { create: false })
|
|
424
459
|
if (opened == null) return
|
|
425
460
|
try {
|
|
426
|
-
sweepAll(opened.db, now, now - keepMs)
|
|
461
|
+
sweepAll(opened.db, now, now - keepMs, max)
|
|
427
462
|
} catch {
|
|
428
463
|
// housekeeping never fails a run
|
|
429
464
|
} finally {
|