@preventive/triage 1.0.0-alpha.18 → 1.0.0-alpha.19

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.
@@ -18,9 +18,9 @@ export type ObjstoreInitDeps = {
18
18
  // the workspace_revision handle in server-e2e/db.ts.
19
19
  handle: Handle
20
20
  reapIntervalMs: number
21
- // Hard off-switch (OBJSTORE_REAP_DISABLED). When true, NEITHER the boot
21
+ // Automatic-sweep switch (OBJSTORE_REAP_DISABLED). When true, NEITHER the boot
22
22
  // sweep nor the periodic timer runs: `startupReap` resolves immediately
23
- // and `stopReaper` is a no-op. Orphaned/superseded blobs and stale
23
+ // and no timer is installed. Orphaned/superseded blobs and stale
24
24
  // staging rows then accumulate unbounded — only safe if an external job
25
25
  // handles GC. Omitted/false → reaper runs (the default). See config.ts.
26
26
  reapDisabled?: boolean
@@ -59,6 +59,7 @@ export type ObjstoreInitDeps = {
59
59
  export type ObjstoreInit = {
60
60
  handlers: ObjstoreHandlers
61
61
  restDeps: ObjstoreRestDeps
62
+ reap: () => Promise<void>
62
63
  startupReap: Promise<void>
63
64
  // Cancels the periodic timer AND resolves only after any
64
65
  // currently-in-flight sweep has finished, so shutdown can safely
@@ -102,31 +103,22 @@ export function initObjstore(deps: ObjstoreInitDeps): ObjstoreInit {
102
103
  let inFlight: Promise<void> | null = null
103
104
  function enqueueSweep(): Promise<void> {
104
105
  if (inFlight) return inFlight
105
- // Log reaper failures unconditionally (not gated on `debug`) — a
106
- // failed sweep means stranded files / staging rows that never get
107
- // cleaned, and operators need to see it. Inner catch makes the
108
- // promise resolve so callers (startupReap awaiter + setInterval)
109
- // don't have to handle rejection.
110
- const p = reapOrphans(handle).catch((err) => {
111
- console.warn('objstore reaper error:', errStack(err))
112
- }).finally(() => { if (inFlight === p) inFlight = null })
106
+ // Keep failures visible to explicit callers; automatic sweeps log them.
107
+ const p = reapOrphans(handle).finally(() => { if (inFlight === p) inFlight = null })
113
108
  inFlight = p
114
109
  return p
115
110
  }
116
- // Hard off-switch (OBJSTORE_REAP_DISABLED). Skip the boot sweep AND the
117
- // periodic timer entirely: `startupReap` resolves immediately so the
118
- // index.ts `await startupReap` gate is a no-op, and `stopReaper` has
119
- // nothing to clear or drain. Loud, unconditional warning — with GC off,
120
- // orphaned/superseded blobs and stale staging rows are never reclaimed
121
- // (they accumulate until an external job, if any, collects them).
111
+ const drain = async () => { if (inFlight) await inFlight.catch(() => {}) }
112
+ const automaticSweep = () => enqueueSweep().catch(err => console.warn('objstore reaper error:', errStack(err)))
113
+ // Explicit cleanup remains available when automatic sweeps are disabled.
122
114
  if (deps.reapDisabled) {
123
- console.warn('objstore: reaper DISABLED (OBJSTORE_REAP_DISABLED) — orphaned/superseded blobs and stale staging will NOT be reclaimed')
124
- return { handlers, restDeps, startupReap: Promise.resolve(), stopReaper: async () => {} }
115
+ console.warn('objstore: automatic reaper DISABLED (OBJSTORE_REAP_DISABLED) — use /api/reap or an external cleanup job')
116
+ return { handlers, restDeps, reap: enqueueSweep, startupReap: Promise.resolve(), stopReaper: drain }
125
117
  }
126
118
  // Caller awaits this before accepting traffic so a fresh boot
127
119
  // can't hand out list / fetch / put-begin against a tag whose
128
120
  // on-disk state still has stranded files from a prior crash.
129
- const startupReap = enqueueSweep()
121
+ const startupReap = automaticSweep()
130
122
  // Jittered start of the periodic timer. Multi-replica deploys
131
123
  // (Neon + Vercel Blob) commonly boot N replicas in tight lock-step
132
124
  // (deploy rollout, cluster restart), which would otherwise sync every
@@ -139,20 +131,20 @@ export function initObjstore(deps: ObjstoreInitDeps): ObjstoreInit {
139
131
  let reapTimer: ReturnType<typeof setInterval> | null = null
140
132
  const jitterMs = Math.floor(Math.random() * deps.reapIntervalMs)
141
133
  const firstTimer = setTimeout(() => {
142
- enqueueSweep()
143
- reapTimer = setInterval(enqueueSweep, deps.reapIntervalMs)
134
+ void automaticSweep()
135
+ reapTimer = setInterval(automaticSweep, deps.reapIntervalMs)
144
136
  reapTimer.unref?.()
145
137
  }, deps.reapIntervalMs + jitterMs)
146
138
  firstTimer.unref?.()
147
139
  return {
148
- handlers, restDeps, startupReap,
140
+ handlers, restDeps, reap: enqueueSweep, startupReap,
149
141
  stopReaper: async () => {
150
142
  clearTimeout(firstTimer)
151
143
  if (reapTimer) clearInterval(reapTimer)
152
144
  // Drain whichever sweep is currently running (startup or
153
145
  // periodic) — either could be mid-readdir/unlink at SIGTERM
154
146
  // time and would otherwise outlive `handle.close()`.
155
- if (inFlight) await inFlight.catch(() => {})
147
+ await drain()
156
148
  },
157
149
  }
158
150
  }
@@ -4,7 +4,7 @@
4
4
  // dialect on the wire.
5
5
  //
6
6
  // `@neondatabase/serverless` is an OPTIONAL peer dep — selected by
7
- // the `DATABASE_URL` branch in `server-e2e/index.ts`. The peer dep
7
+ // the configured database URL branch in `server-e2e/index.ts`. The peer dep
8
8
  // itself is loaded lazily inside `openNeonObjstore` below. A single
9
9
  // Neon project / database holds both `workspace_revision` and the
10
10
  // two objstore tables; each plane opens its own stateless
@@ -13,14 +13,10 @@
13
13
  // release.
14
14
  //
15
15
  // Byte plane: passed in as a `BlobBackend` (./blob.ts). The
16
- // supported pairings selected by server-e2e/index.ts are:
17
- // - Neon + Vercel Blob Private Storage (multi-replica, the only
18
- // pairing that survives a replica restart without local-disk
19
- // coordination). Activated by setting BLOB_READ_WRITE_TOKEN
20
- // alongside DATABASE_URL.
21
- // - Neon + local FS (development / operator escape hatch — bytes
22
- // still need to live on a shared filesystem if you run more
23
- // than one replica).
16
+ // supported Neon pairing selected by server-e2e/index.ts is Vercel
17
+ // Blob Private Storage. DATABASE_URL or E2E_DATABASE_URL plus BLOB_READ_WRITE_TOKEN
18
+ // is required; the launcher never selects Neon + local FS. Tests
19
+ // can inject a local-FS backend directly through this adapter.
24
20
 
25
21
  import type { BlobBackend } from './blob.ts'
26
22
  import type { AllStmt, GetStmt, RunStmt } from '../db-stmt.ts'