fix: preserve local host scheduler and fatal errors
This commit is contained in:
+16
-2
@@ -138,10 +138,24 @@ export function createLocalLoginHost(options: LocalLoginHostOptions): LocalLogin
|
||||
if (model.kind === 'denial') serverFailure = { data: model.outcome.data };
|
||||
if (model.kind === 'kicked') serverFailure = { data: model.data };
|
||||
try { options.view.showOverlay(model); }
|
||||
catch (error) { transportFault(error); throw error; }
|
||||
catch (error) {
|
||||
if (model.kind === 'fatal') fail(collected([model.error, error]));
|
||||
else transportFault(error);
|
||||
throw error;
|
||||
}
|
||||
if (model.kind === 'fatal') fail(model.error);
|
||||
},
|
||||
}, options.scheduler, fail);
|
||||
}, {
|
||||
request: callback => {
|
||||
try {
|
||||
const cancel = options.scheduler.request(callback);
|
||||
return () => {
|
||||
try { cancel(); }
|
||||
catch (error) { fail(error); throw error; }
|
||||
};
|
||||
} catch (error) { fail(error); throw error; }
|
||||
},
|
||||
}, fail);
|
||||
runtime = new PlatformRuntime({ gameEntry: entry,
|
||||
resolveRuntimeConfig: async () => context.config,
|
||||
createWireClient: () => createLocalLoginWire({ context, record: options.record, onFault: wireFault,
|
||||
|
||||
@@ -29,6 +29,7 @@ class Socket {
|
||||
push(message: unknown) { this.onmessage?.({ data: JSON.stringify(message) }); }
|
||||
}
|
||||
async function setup(overrides: { config?: Partial<ResolveRuntimeConfigOptions>; render?: (model: PageModel) => void;
|
||||
cancelFrame?: () => void; showOverlay?: (model: OverlayModel) => void;
|
||||
record?: (event: { kind: string; data: unknown }) => void; onBusy?: (busy: boolean) => void } = {}) {
|
||||
const api = await import('../../YouleNexus/assets/scripts/local-platform-login/local-login-host.ts')
|
||||
.catch(error => assert.fail(`Local login host implementation must exist: ${String(error)}`));
|
||||
@@ -43,8 +44,8 @@ async function setup(overrides: { config?: Partial<ResolveRuntimeConfigOptions>;
|
||||
storage: { getItem: key => values.get(key) ?? null, setItem: (key, value) => { values.set(key, value); } },
|
||||
random: () => .5, now: () => 123456, sampleHost: () => ({ location: null }),
|
||||
view: { render: model => { overrides.render?.(model); pages.push(model); },
|
||||
activate: page => { activations.push(page); }, showOverlay: model => { overlays.push(model); } },
|
||||
scheduler: { request: callback => { frames.add(callback); return () => { frames.delete(callback); }; } },
|
||||
activate: page => { activations.push(page); }, showOverlay: model => { overlays.push(model); overrides.showOverlay?.(model); } },
|
||||
scheduler: { request: callback => { frames.add(callback); return () => { frames.delete(callback); overrides.cancelFrame?.(); }; } },
|
||||
loadResources: () => resources.promise, waitForMinimumDisplay: () => minimum.promise,
|
||||
createSocket: () => { const socket = new Socket(); sockets.push(socket); return socket as unknown as WebSocket; },
|
||||
record: event => { evidence.push(event); overrides.record?.(event); },
|
||||
@@ -176,3 +177,36 @@ test('kick data remains primary when closing its socket also fails', async () =>
|
||||
const errors = (h.failures[0] as { errors: unknown[] }).errors;
|
||||
assert.deepEqual(errors[0], data); assert.ok(errors.includes(cleanup));
|
||||
});
|
||||
|
||||
test('pending frame cancellation retains its primary error when socket cleanup also fails', async () => {
|
||||
const primary = new Error('pending frame cancel failed'), cleanup = new Error('socket close failed');
|
||||
let armed = false;
|
||||
const h = await setup({ cancelFrame: () => { if (armed) throw primary; } });
|
||||
await h.ready(); await h.host.login();
|
||||
armed = true; h.sockets[0]!.closeError = cleanup;
|
||||
// The real login response schedules a Store frame, then showLobby flush cancels it.
|
||||
h.sockets[0]!.push(fixture());
|
||||
assert.equal(h.failures.length, 1);
|
||||
const fault = h.failures[0] as { errors?: unknown[] };
|
||||
assert.ok(fault.errors, 'primary and cleanup failures must both be retained');
|
||||
assert.strictEqual(fault.errors[0], primary);
|
||||
assert.ok(fault.errors.includes(cleanup));
|
||||
assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.frames.size, 0);
|
||||
assert.equal(h.sockets[0]!.closes, 1);
|
||||
});
|
||||
|
||||
test('fatal overlay observer failure retains the original Runtime error object', async () => {
|
||||
const observer = new Error('fatal overlay observer failed');
|
||||
let primary: Error | undefined;
|
||||
const h = await setup({ showOverlay: model => {
|
||||
if (model.kind === 'fatal') { primary = model.error; throw observer; }
|
||||
} });
|
||||
await h.ready();
|
||||
h.sockets[0]!.push({ route: 'agent', rpc: 'unsupported_test_rpc', data: {} });
|
||||
assert.ok(primary); assert.match(primary.message, /Unsupported platform rpc/);
|
||||
assert.equal(h.failures.length, 1);
|
||||
const fault = h.failures[0] as { errors?: unknown[] };
|
||||
assert.ok(fault.errors, 'Runtime error and overlay observer error must both be retained');
|
||||
assert.strictEqual(fault.errors[0], primary); assert.strictEqual(fault.errors[1], observer);
|
||||
assert.equal(h.host.getSnapshot().ready, false); assert.equal(h.sockets[0]!.closes, 1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user