fix: preserve reconnect after local socket errors
This commit is contained in:
+15
-1
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user