diff --git a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts index c30d760..03571b8 100644 --- a/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts +++ b/cocoscreator_projects/YouleNexus/assets/scripts/local-platform-login/local-login-transport.ts @@ -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?.(); }; } diff --git a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts index 8ffea5f..ab53751 100644 --- a/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts +++ b/cocoscreator_projects/framework-tests/local-platform-login/transport.test.ts @@ -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 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); + }); +}