fix(anime): restart the bridge on the next request after it dies

The reported 'APK crash' turned out not to exist: repeated full scans of
all installed extensions load cleanly. The real incident was the app's
quit sequence stopping the bridge (its /stop endpoint) while requests
were in flight - and it exposed that a bridge dying for any reason left
the anime browser failing every call with a raw 'fetch failed' until
the whole app was restarted.

SidecarHandle now reports the child's exit (including to late
subscribers), and the runtime detects a death it did not order: it
drops the dead handle, closes the strip proxy, tells the UI, and the
next request transparently starts a fresh bridge. requireBridge became
an async bridge() that starts on demand, which also covers first use.

Verified live: search -> SIGKILL the JVM -> search recovers.
This commit is contained in:
2026-08-01 01:05:40 -07:00
parent a34c3e95c5
commit b528440d48
4 changed files with 94 additions and 11 deletions
+39
View File
@@ -1,5 +1,6 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { EventEmitter } from 'node:events';
import type { spawn as spawnType, ChildProcess } from 'node:child_process';
import { allocatePort, startSidecar } from './sidecar-process';
@@ -64,3 +65,41 @@ test('an early exit is reported with its code rather than waiting out the deadli
/exited before becoming ready \(code 1/,
);
});
test('onExit reports a death after readiness, including to late subscribers', async () => {
const port = await allocatePort();
// Fake the bridge's capabilities endpoint so startSidecar reports ready.
const server = http.createServer((_req, res) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(
JSON.stringify({ mangatanMihonBridge: 1, sourceFactory: true, preferenceCallbacks: true }),
);
});
await new Promise<void>((resolve) => server.listen(port, '127.0.0.1', resolve));
const child = fakeChild();
const spawnImpl = (() => child) as unknown as typeof spawnType;
try {
const handle = await startSidecar({ binaries, port, readyTimeoutMs: 5000, spawnImpl });
const exits: Array<{ code: number | null; signal: NodeJS.Signals | null }> = [];
handle.onExit((info) => exits.push(info));
assert.equal(exits.length, 0);
child.emit('exit', 137, null);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual([...exits], [{ code: 137, signal: null }]);
// A listener attached after the death still hears about it.
handle.onExit((info) => exits.push(info));
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(
[...exits],
[
{ code: 137, signal: null },
{ code: 137, signal: null },
],
);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});