OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-rooms commits / 62b0e09

0.1.3: isolate tracker autobase errors instead of crashing the daemon
Re-applied on top of main's 0.1.2 (per-org-autobase fast-forwarded to main, keeping the bare-fs override) so the pgh multiversion-collapse picks THIS version — the prior 0.1.1 branch lost to the higher 0.1.2. autobase routes drain/apply failures through _onError -> emit('error'). With no 'error' listener, EventEmitter re-throws -> one broken tracker SIGABRTs the whole daemon. A standalone peer hits this when its stale local state can't reconcile a group manifest migration; full-replica mirrors never do. Attach 'error'/'warning' handlers that log, record `errored`, and re-emit safe 'base-error'/'base-warning' for consumers (Increment 2: fresh-namespace re-sync). test/resilience.test.js 6/6. pear-git #3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
62b0e095b659dd527dea2fcb8a57c6e4589f2ab1
by plankatron <fleeky@gmail.com> · 2026-07-19T02:06:57.000Z

Changed files (3) +79 -1

lib/room.js +35 -0
……@@ -86,6 +86,33 @@ export class Room extends ReadyResource {
8686 ackInterval: 1000,
8787 ackThreshold: 4
8888 })
89
90 // Resilience (pear-git #3): autobase routes any drain/apply failure through
91 // _onError → emit('error'). With NO 'error' listener, EventEmitter re-throws,
92 // so ONE broken tracker SIGABRTs the whole daemon — crash-looping and taking
93 // every other repo down. A standalone peer hits this when its stale local
94 // state can't reconcile a manifest migration that happened in the group,
95 // whereas full-replica mirrors never do (they take the finished picture).
96 // Attach handlers so a broken tracker is ISOLATED + observable instead of
97 // fatal — matching keet's room-autobase 'error'/'warning' handlers. Consumers
98 // react to the re-emitted 'base-error' (Increment 2: fresh-namespace re-sync).
99 // Custom event names never throw when unlistened, so re-emitting is safe.
100 this._baseError = null
101 this._base.on('error', (err) => {
102 this._baseError = err
103 try {
104 // eslint-disable-next-line no-console
105 console.error('[hyper-rooms] tracker autobase error (ns=' + this._namespace + '): ' + (err && err.message ? err.message : err))
106 } catch {}
107 this.emit('base-error', err)
108 })
109 this._base.on('warning', (err) => {
110 try {
111 // eslint-disable-next-line no-console
112 console.error('[hyper-rooms] tracker autobase warning (ns=' + this._namespace + '): ' + (err && err.message ? err.message : err))
113 } catch {}
114 this.emit('base-warning', err)
115 })
89116 }
90117
91118 // ── Lifecycle ────────────────────────────────────────────────
……@@ -112,6 +139,14 @@ export class Room extends ReadyResource {
112139 /** True if this peer can append ops. */
113140 get writable () { return this._base.writable }
114141
142 /**
143 * The autobase error that isolated this room, if any (resilience, #3).
144 * Non-null once the tracker's autobase has emitted 'error' (e.g. a stale
145 * local state that couldn't reconcile a group manifest migration). A consumer
146 * treats a non-null value as "this tracker is broken — re-sync it fresh".
147 */
148 get errored () { return this._baseError }
149
115150 /** Read-only access to the Hyperbee view. */
116151 get view () { return this._base.view }
117152
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "hyper-rooms",
3 "version": "0.1.2",
3 "version": "0.1.3",
44 "description": "Multiwriter rooms on Autobase with pluggable schemas — the substrate for hyper-issues, hyper-orgs, etc.",
55 "type": "module",
66 "main": "index.js",
test/resilience.test.js +43 -0
……@@ -0,0 +1,43 @@
1import test from 'brittle'
2import Corestore from 'corestore'
3import { mkdtempSync } from 'fs'
4import { join } from 'path'
5import { tmpdir } from 'os'
6import { Room, jsonSchema } from '../index.js'
7
8function tmp () { return mkdtempSync(join(tmpdir(), 'rooms-resil-')) }
9
10const schema = jsonSchema({
11 validate () {},
12 async apply (batch, op) { if (op.type === 'put') await batch.put('k:' + op.id, { v: op.v }) }
13})
14
15// pear-git #3 Increment 1: a tracker autobase 'error' must be ISOLATED (logged +
16// re-emitted as 'base-error'), never fatal. autobase routes drain/apply failures
17// through _onError -> emit('error'); with no listener EventEmitter re-throws and
18// SIGABRTs the whole daemon. This proves the Room's handler catches it.
19test('tracker autobase error is isolated, not fatal (#3 resilience)', async (t) => {
20 const store = new Corestore(tmp())
21 const room = new Room(store, { schema, namespace: 'resil-test', bootstrapKey: null })
22 await room.ready()
23
24 t.is(room.errored, null, 'not errored initially')
25
26 let reEmitted = null
27 room.on('base-error', (err) => { reEmitted = err })
28
29 const boom = new Error('simulated drain failure (null view)')
30 t.execution(() => room._base.emit('error', boom), 'emitting base error does NOT throw')
31
32 t.is(room.errored, boom, 'room.errored records the error')
33 t.is(reEmitted, boom, "'base-error' re-emitted to consumers")
34
35 let warned = null
36 room.on('base-warning', (err) => { warned = err })
37 const warn = new Error('simulated warning')
38 t.execution(() => room._base.emit('warning', warn), 'emitting base warning does NOT throw')
39 t.is(warned, warn, "'base-warning' re-emitted to consumers")
40
41 await room.close()
42 await store.close()
43})