// Durable patch (runs as `postinstall`): guard autobase's _drain warmup
// pre-download against a null user-view core. A view may not be materialized
// yet during warmup, so `this.views[k].core` can be null; the warmup download
// is a PERF optimization, not correctness, so a null must be SKIPPED, never
// fatal. Without this, pgv3 SIGABRT-crash-loops on boot when an org/tracker
// autobase drains a not-yet-materialized view (pear-git #3, ashatron 2026-07-18).
//
// Idempotent: a no-op if already patched or the anchor is gone (autobase bump).
// Vendored here rather than patch-package to avoid a network/registry dependency.
const fs = require('fs')
const path = require('path')
const file = path.join(__dirname, '..', 'node_modules', 'autobase', 'lib', 'apply-state.js')
// NOTE: patch 1 must NOT process.exit() when it is already applied — patch 2
// below still has to run. (It did exit, which is why the appendBatch bound
// silently never landed on an already-patched tree.)
let patch1Done = false
try {
let s = fs.readFileSync(file, 'utf8')
if (s.includes('GUARD (pgv3 patch)')) {
patch1Done = true // already patched — fall through to patch 2, do NOT exit
} else {
const before = ' const view = this.views[k]\n view.core.download({ blocks })'
const after = ' const view = this.views[k]\n' +
' // GUARD (pgv3 patch): a user view may not be materialized yet during\n' +
' // _drain warmup — its core can be null. This warmup pre-download is a\n' +
' // perf optimization, NOT correctness, so a null view.core must be SKIPPED,\n' +
' // never fatal. Before this guard the whole daemon SIGABRT-crash-looped on\n' +
' // boot (pear-git #3). Log the autobase key so the offending base is named.\n' +
' if (!view || !view.core) {\n' +
' try { console.error("[autobase][warmup] null view.core at view index " + k + " on base " + (this.base && this.base.key ? this.base.key.toString("hex").slice(0,16) : "?") + " — skipping (was fatal)") } catch {}\n' +
' continue\n' +
' }\n' +
' view.core.download({ blocks })'
if (!s.includes(before)) {
console.error('[patch-autobase-warmup] anchor not found (autobase version changed?) — SKIPPING; re-verify the warmup guard manually')
} else {
s = s.replace(before, after)
fs.writeFileSync(file, s)
patch1Done = true
console.error('[patch-autobase-warmup] applied null-view.core warmup guard to autobase')
}
}
} catch (err) {
console.error('[patch-autobase-warmup] failed:', err && err.message)
// Never fail the install over the patch — the guard is defensive.
}
// ── Patch 2: bound the _appendBatch flush loop ──────────────────────────────
//
// autobase/index.js _appendBatch spins until the appended nodes flush:
//
// while (this._appended < target && !this._interrupting) {
// await this._bump()
// if (runs++ >= 16 && this.localWriter && this.localWriter.idle()) break
// }
//
// The escape hatch requires BOTH runs >= 16 AND localWriter.idle(). A writer
// that can never flush never goes idle, so the break never fires and this spins
// forever — `await this._bump()` is a MICROtask chain into _advance, so the
// event loop is never handed back: no accept(), no timers, no SIGCHLD.
//
// Reached on a MIRROR whose writer key the origin has not enrolled yet. The
// 'Not writable' guard above only fires when localWriter is null or removed;
// stortron's writer EXISTS but cannot linearize, so it passes that check and
// then hangs here.
//
// Measured on stortron (mirror, 145 repos), caller stack captured from inside
// _advance at call #200,000 AND #800,000, identical both times:
// _doAnnounce -> registerMirrorWriters -> _appendBatch -> _bump -> _advance
// 800,000 _advance calls == 800,000 iterations of this one while loop. The
// daemon never printed its banner and never served a request.
//
// The fix makes the bound unconditional: 16 bumps, then stop. A short/failed
// append is recoverable; an unkillable daemon is not.
const idx = path.join(__dirname, '..', 'node_modules', 'autobase', 'index.js')
try {
let s2 = fs.readFileSync(idx, 'utf8')
if (s2.includes('BOUND (pgv3 patch)')) {
console.error('[patch-autobase-warmup] _appendBatch flush bound already present')
return
}
const b2 = ' // safety\n' +
' if (runs++ >= 16 && this.localWriter && this.localWriter.idle()) break'
const a2 = ' // BOUND (pgv3 patch): the original also required\n' +
' // `this.localWriter.idle()`, so a writer that can never flush (a mirror\n' +
' // whose key the origin has not enrolled) never satisfied the break and\n' +
' // this loop spun forever. `await this._bump()` is a microtask chain into\n' +
' // _advance, so the event loop was never handed back — no accept(), no\n' +
' // timers, no SIGCHLD reaping. Bound it unconditionally: a short append is\n' +
' // recoverable, a wedged daemon is not.\n' +
' if (runs++ >= 16) break'
if (!s2.includes(b2)) {
console.error('[patch-autobase-warmup] appendBatch anchor not found (autobase version changed?) — SKIPPING; re-verify the flush bound manually')
process.exit(0)
}
s2 = s2.replace(b2, a2)
fs.writeFileSync(idx, s2)
console.error('[patch-autobase-warmup] applied _appendBatch flush bound to autobase')
} catch (err) {
console.error('[patch-autobase-warmup] appendBatch patch failed:', err && err.message)
}