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) => {
|
socket.onerror = (event) => {
|
||||||
if (this.socket !== socket) return;
|
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?.();
|
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.onerror?.(error);
|
||||||
socket.onclose?.({} as CloseEvent);
|
socket.onclose?.({} as CloseEvent);
|
||||||
assert.equal(closed, 1);
|
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 () => {
|
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);
|
assert.throws(() => h.transport.send('late'), /open/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('explicit stop from fault observer suppresses the pending error close callback', async () => {
|
test('explicit stop from error evidence observer suppresses the pending error close callback', async () => {
|
||||||
const h = await setup({ onFault: () => h.transport.close() });
|
const h = await setup({ record: () => h.transport.close() });
|
||||||
let closed = 0;
|
let closed = 0;
|
||||||
h.transport.onClose(() => closed++);
|
h.transport.onClose(() => closed++);
|
||||||
h.transport.connect(h.url);
|
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);
|
assert.equal(closed, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a replacement connection from fault observer cannot receive the old close callback', async () => {
|
test('a replacement connection from error evidence observer cannot receive the old close callback', async () => {
|
||||||
const h = await setup({ onFault: () => h.transport.connect(h.url) });
|
const h = await setup({ record: () => h.transport.connect(h.url) });
|
||||||
let closed = 0;
|
let closed = 0;
|
||||||
h.transport.onClose(() => closed++);
|
h.transport.onClose(() => closed++);
|
||||||
h.transport.connect(h.url);
|
h.transport.connect(h.url);
|
||||||
@@ -228,3 +229,45 @@ test('natural close lets real WireClient reconnect while explicit stop cancels i
|
|||||||
wire.stop();
|
wire.stop();
|
||||||
assert.equal(timers.size, 0);
|
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