126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
/**
|
|
* Host exit and ordered teardown of swarm/Hyperdrive/Corestore (Pear-safe ordering).
|
|
* @module bare-os-lifecycle-manager
|
|
*/
|
|
|
|
/**
|
|
* On Pear/Bare, `process.exit` is often missing or ineffective; use `Bare.exit`
|
|
* (see holepunch `pear-prerelease`, `pear-terminal`, `bare-process`).
|
|
* @param {number} code
|
|
*/
|
|
export function exitHostProcess(code) {
|
|
const Bare = globalThis.Bare
|
|
if (Bare && typeof Bare.exit === 'function') {
|
|
/* Defer so Hyperdrive/swarm/native stdio teardown is not on the same stack as exit
|
|
* (avoids malloc "pointer being freed was not allocated" under Pear on macOS). */
|
|
const run = () => Bare.exit(code)
|
|
if (typeof setImmediate === 'function') setImmediate(run)
|
|
else Promise.resolve().then(run)
|
|
return
|
|
}
|
|
const p = globalThis.process
|
|
if (p && typeof p.exit === 'function') p.exit(code)
|
|
}
|
|
|
|
/**
|
|
* Stop replication before closing drives — closing Hyperdrive while Protomux streams
|
|
* are still live can corrupt native heaps under Pear.
|
|
*
|
|
* Boot budget / bare-stdlib telemetry is recorded by the booter before initd (**`BARE_OS_BOOT_BARE_STDLIB_RESOLUTION_MS`**, **`bootReadyStateRef.subsystems.bareStdlib`**) and by the guest in **`/run/bare-os/boot-perf.json`** (**schema 5**); **`metrics_live.json`** **`bootBudgetTelemetry`** (**schema 2**) mirrors the same env keys for dashboards.
|
|
*
|
|
* @param {{
|
|
* hdmsController?: { deactivate(): Promise<unknown> },
|
|
* personalDrive?: { close(): Promise<unknown> },
|
|
* drive?: { close(): Promise<unknown> }
|
|
* }} disk
|
|
* @param {{ destroy(): Promise<unknown> }} swarm
|
|
* @param {{ close(): Promise<unknown> }} store
|
|
*/
|
|
export async function teardownBareOsBootResources(disk, swarm, store) {
|
|
try {
|
|
if (disk.hdmsController) await disk.hdmsController.deactivate()
|
|
} catch (_) {}
|
|
try {
|
|
await swarm.destroy()
|
|
} catch (_) {}
|
|
try {
|
|
if (disk.personalDrive) await disk.personalDrive.close()
|
|
} catch (_) {}
|
|
try {
|
|
if (disk.drive) await disk.drive.close()
|
|
} catch (_) {}
|
|
try {
|
|
await store.close()
|
|
} catch (_) {}
|
|
}
|
|
|
|
/**
|
|
* @returns {{ state: string, canTransitionTo: (next: string) => boolean, transition: (next: string) => boolean, snapshot: () => { state: string, atMs: number, transitions: Array<{ from: string, to: string, atMs: number }> } }}
|
|
*/
|
|
export function createBareOsLifecycleStateMachine() {
|
|
/** @type {'opening'|'ready'|'suspending'|'resuming'|'closing'|'closed'} */
|
|
let state = 'opening'
|
|
/** @type {Array<{ from: string, to: string, atMs: number }>} */
|
|
const transitions = []
|
|
const ok = (from, to) =>
|
|
(from === 'opening' && (to === 'ready' || to === 'closing')) ||
|
|
(from === 'ready' && (to === 'suspending' || to === 'closing')) ||
|
|
(from === 'suspending' && (to === 'resuming' || to === 'closing')) ||
|
|
(from === 'resuming' && (to === 'ready' || to === 'closing')) ||
|
|
(from === 'closing' && to === 'closed')
|
|
return {
|
|
get state() {
|
|
return state
|
|
},
|
|
canTransitionTo(next) {
|
|
return ok(state, String(next || ''))
|
|
},
|
|
transition(next) {
|
|
const n = String(next || '')
|
|
if (!ok(state, n)) return false
|
|
transitions.push({ from: state, to: n, atMs: Date.now() })
|
|
state = /** @type {typeof state} */ (n)
|
|
return true
|
|
},
|
|
snapshot() {
|
|
return { state, atMs: Date.now(), transitions: [...transitions] }
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Array<{ name: string, run: () => Promise<unknown> }>} hooks
|
|
* @returns {Promise<{ schema: 1, ok: boolean, rows: Array<{ name: string, ok: boolean, error?: string, atMs: number }> }>}
|
|
*/
|
|
export async function runTransactionalLifecycleHooks(hooks) {
|
|
/** @type {Array<{ name: string, ok: boolean, error?: string, atMs: number }>} */
|
|
const rows = []
|
|
let ok = true
|
|
for (const h of Array.isArray(hooks) ? hooks : []) {
|
|
const name = String(h && h.name ? h.name : 'hook')
|
|
try {
|
|
await Promise.resolve(h.run())
|
|
rows.push({ name, ok: true, atMs: Date.now() })
|
|
} catch (e) {
|
|
ok = false
|
|
rows.push({
|
|
name,
|
|
ok: false,
|
|
error: e && e.message ? e.message : String(e),
|
|
atMs: Date.now()
|
|
})
|
|
}
|
|
}
|
|
return { schema: 1, ok, rows }
|
|
}
|
|
|
|
/**
|
|
* Best-effort settle delay before teardown.
|
|
* @param {{ timeoutMs?: number }} [opts]
|
|
*/
|
|
export async function quiesceReplicationBeforeTeardown(opts = {}) {
|
|
const t = Math.max(0, Math.min(10000, Number(opts.timeoutMs) || 200))
|
|
if (t <= 0) return
|
|
await new Promise((resolve) => setTimeout(resolve, t))
|
|
}
|