fix: preserve reconnect after local socket errors

This commit is contained in:
2026-09-05 18:09:56 +08:00
parent a3cd74d316
commit 9ad477f474
2 changed files with 63 additions and 6 deletions
@@ -65,7 +65,21 @@ export class LocalLoginTransport implements Transport {
};
socket.onerror = (event) => {
if (this.socket !== socket) return;
this.stopWithFault(event);
try {
this.options.record({ kind: 'transport-error', data: event });
} catch (error) {
this.stopWithFault(error);
throw error;
}
if (this.socket !== socket || this.generation !== generation) return;
// Invalidate before close: browsers may dispatch close after error.
this.socket = null;
try {
socket.close();
} catch (error) {
this.options.onFault(error);
throw error;
}
if (this.generation === generation) this.closeCallback?.();
};
}
@@ -157,7 +157,8 @@ test('error followed by close notifies once and retains original error event', a
socket.onerror?.(error);
socket.onclose?.({} as CloseEvent);
assert.equal(closed, 1);
assert.equal(h.faults[0], error);
assert.deepEqual(h.faults, []);
assert.deepEqual(h.evidence, [{ kind: 'transport-error', data: error }]);
});
test('explicit close and superseded sockets invalidate every old callback', async () => {
@@ -191,8 +192,8 @@ test('close failure is reported and thrown after invalidation', async () => {
assert.throws(() => h.transport.send('late'), /open/i);
});
test('explicit stop from fault observer suppresses the pending error close callback', async () => {
const h = await setup({ onFault: () => h.transport.close() });
test('explicit stop from error evidence observer suppresses the pending error close callback', async () => {
const h = await setup({ record: () => h.transport.close() });
let closed = 0;
h.transport.onClose(() => closed++);
h.transport.connect(h.url);
@@ -200,8 +201,8 @@ test('explicit stop from fault observer suppresses the pending error close callb
assert.equal(closed, 0);
});
test('a replacement connection from fault observer cannot receive the old close callback', async () => {
const h = await setup({ onFault: () => h.transport.connect(h.url) });
test('a replacement connection from error evidence observer cannot receive the old close callback', async () => {
const h = await setup({ record: () => h.transport.connect(h.url) });
let closed = 0;
h.transport.onClose(() => closed++);
h.transport.connect(h.url);
@@ -228,3 +229,45 @@ test('natural close lets real WireClient reconnect while explicit stop cancels i
wire.stop();
assert.equal(timers.size, 0);
});
test('natural error reconnects real WireClient even when terminal onFault stops the host', async () => {
const h = await setup({ onFault: () => wire.stop() });
const timers = new Map<unknown, () => void>();
const clock: Clock = {
setTimeout: (fn) => { const key = {}; timers.set(key, fn); return key; },
clearTimeout: (key) => { timers.delete(key); },
};
const wire = new WireClient({ servers: h.context.config.servers, transportFactory: () => h.transport, clock });
wire.start();
h.sockets[0]!.open();
const error = {} as Event;
h.sockets[0]!.onerror?.(error);
h.sockets[0]!.onclose?.({} as CloseEvent);
assert.deepEqual(h.faults, []);
assert.deepEqual(h.evidence, [{ kind: 'transport-error', data: error }]);
assert.equal(timers.size, 1);
const retry = [...timers.values()][0]!;
timers.clear(); retry();
assert.equal(h.sockets.length, 2);
wire.stop();
assert.equal(timers.size, 0);
});
for (const failure of ['record', 'close']) {
test(`socket error ${failure} failure reports terminal original and suppresses reconnect`, async () => {
const error = new Error(`${failure} failed`);
const h = await setup(failure === 'record' ? { record: () => { throw error; } } : {});
let closed = 0;
h.transport.onClose(() => closed++);
h.transport.connect(h.url);
const socket = h.sockets[0]!;
if (failure === 'close') socket.closeError = error;
const event = {} as Event;
assert.throws(() => socket.onerror?.(event), (caught) => caught === error);
assert.deepEqual(h.faults, [error]);
assert.equal(closed, 0);
socket.onclose?.({} as CloseEvent);
assert.equal(closed, 0);
assert.throws(() => h.transport.send('late'), /open/i);
});
}