From a5fe04d2e4e0e2e39f5868a8e9b27b287fd80863 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:34:17 -0800 Subject: [PATCH 1/6] refactor(kernel-browser-runtime): Simplify kernel worker initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kernel worker initialization was using a convoluted pattern where every incoming message had to traverse a promise chain, even after the server was initialized. This refactoring updates receiveInternalConnections to accept either a direct handler function or a promise that resolves to a handler, implements a lazy cache pattern where subsequent messages use the cached handler directly with zero promise overhead, and simplifies kernel-worker.ts to pass a handler promise instead of wrapping every message in a promise chain. The synchronous listener setup is preserved to avoid missing early connections, while eliminating the performance overhead of promise chaining on every message after initialization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../internal-comms/internal-connections.ts | 26 ++++++++++++++++--- .../src/kernel-worker/kernel-worker.ts | 8 +++--- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts index 960b3c8e0..276c4685b 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts @@ -103,7 +103,7 @@ const connectToInternalProcess = async ( }; type ReceiveConnectionsOptions = Omit & { - handleInternalMessage: HandleInternalMessage; + handleInternalMessage: HandleInternalMessage | Promise; }; /** @@ -112,7 +112,9 @@ type ReceiveConnectionsOptions = Omit & { * processes have attempted to connect. * * @param options - The options for the connection. - * @param options.handleInternalMessage - The function to handle the internal message. + * @param options.handleInternalMessage - The function to handle the internal message, + * or a promise that resolves to such a function. If a promise is provided, messages will + * be buffered until the handler is ready, then subsequent messages are handled directly. * @param options.logger - The logger instance. * @param options.controlChannelName - The name of the control channel. Must match * the name used by {@link connectToKernel} on the other end. @@ -122,6 +124,22 @@ export const receiveInternalConnections = ({ logger, controlChannelName = COMMS_CONTROL_CHANNEL_NAME, }: ReceiveConnectionsOptions): void => { + // Support both direct handler and promise-based handler + let handler: HandleInternalMessage | null = null; + let handlerReady: Promise; + + if (typeof handleInternalMessage === 'function') { + // Direct handler - use immediately + handler = handleInternalMessage; + handlerReady = Promise.resolve(handleInternalMessage); + } else { + // Promise-based handler - cache once resolved + handlerReady = handleInternalMessage.then((resolvedHandler) => { + handler = resolvedHandler; + return resolvedHandler; + }); + } + const seenChannels = new Set(); new BroadcastChannel(controlChannelName).onmessage = (event) => { if (!isCommsControlMessage(event.data)) { @@ -148,7 +166,9 @@ export const receiveInternalConnections = ({ `Received message from internal process "${channelName}": ${JSON.stringify(message)}`, ); - const reply = await handleInternalMessage(message); + // Use cached handler if available, otherwise await once + const messageHandler = handler ?? (await handlerReady); + const reply = await messageHandler(message); if (reply !== undefined) { await kernelRpcStream.write(reply); } diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index 4fe244572..d7cdfd578 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -54,18 +54,18 @@ async function main(): Promise { resetStorage, }, ); - const serverP = kernelP.then((kernel) => { - return new JsonRpcServer({ + const handlerP = kernelP.then((kernel) => { + const server = new JsonRpcServer({ middleware: [ makeLoggingMiddleware(logger.subLogger('kernel-command')), makePanelMessageMiddleware(kernel, kernelDatabase), ], }); + return async (request: JsonRpcCall) => server.handle(request); }); receiveInternalConnections({ - handleInternalMessage: async (request) => - serverP.then(async (rpcServer) => rpcServer.handle(request)), + handleInternalMessage: handlerP, logger, }); From 4e9df00a9b9be2dbf17c31a872ada34f06d81789 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:15:37 +0000 Subject: [PATCH 2/6] refactor: Replace handleInternalMessage with mutually exclusive options Replace the union type parameter with discriminated union options: - `handler`: Direct handler function (for tests) - `handlerPromise`: Promise-based handler (for async initialization) This addresses the anti-pattern of accepting either a value or a promise of that value, making the API more explicit and easier to understand. Co-authored-by: Erik Marks --- .../internal-connections.test.ts | 16 ++++----- .../internal-comms/internal-connections.ts | 33 ++++++++++++------- .../src/kernel-worker/kernel-worker.ts | 2 +- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts index 3dd09c888..3b319cd8a 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts @@ -183,7 +183,7 @@ describe('internal-connections', () => { it('should handle new internal process connections', async () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); @@ -208,7 +208,7 @@ describe('internal-connections', () => { it('should handle valid message', async () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); @@ -257,7 +257,7 @@ describe('internal-connections', () => { it('should handle JSON-RPC notifications', async () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); @@ -300,7 +300,7 @@ describe('internal-connections', () => { it('should handle multiple simultaneous connections', async () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); @@ -335,7 +335,7 @@ describe('internal-connections', () => { it('should forget ids of closed channels', async () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); const controlChannel = MockBroadcastChannel.channels.get( @@ -376,7 +376,7 @@ describe('internal-connections', () => { it('should reject duplicate connections', () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); const controlChannel = MockBroadcastChannel.channels.get( @@ -401,7 +401,7 @@ describe('internal-connections', () => { it('should reject invalid control commands', () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); const controlChannel = MockBroadcastChannel.channels.get( @@ -425,7 +425,7 @@ describe('internal-connections', () => { it('should handle comms channel message errors', async () => { receiveInternalConnections({ - handleInternalMessage: mockHandleMessage, + handler: mockHandleMessage, logger, }); diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts index 276c4685b..dcbb1d3ea 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts @@ -102,9 +102,17 @@ const connectToInternalProcess = async ( return stream; }; -type ReceiveConnectionsOptions = Omit & { - handleInternalMessage: HandleInternalMessage | Promise; -}; +type ReceiveConnectionsOptions = Omit & + ( + | { + handler: HandleInternalMessage; + handlerPromise?: never; + } + | { + handler?: never; + handlerPromise: Promise; + } + ); /** * Listens for connections between the kernel and an internal process, e.g. a UI instance. @@ -112,15 +120,18 @@ type ReceiveConnectionsOptions = Omit & { * processes have attempted to connect. * * @param options - The options for the connection. - * @param options.handleInternalMessage - The function to handle the internal message, - * or a promise that resolves to such a function. If a promise is provided, messages will - * be buffered until the handler is ready, then subsequent messages are handled directly. + * @param options.handler - The function to handle internal messages. Mutually exclusive + * with `handlerPromise`. + * @param options.handlerPromise - A promise that resolves to the handler function. + * Messages will be buffered until the handler is ready, then subsequent messages are + * handled directly. Mutually exclusive with `handler`. * @param options.logger - The logger instance. * @param options.controlChannelName - The name of the control channel. Must match * the name used by {@link connectToKernel} on the other end. */ export const receiveInternalConnections = ({ - handleInternalMessage, + handler: directHandler, + handlerPromise, logger, controlChannelName = COMMS_CONTROL_CHANNEL_NAME, }: ReceiveConnectionsOptions): void => { @@ -128,13 +139,13 @@ export const receiveInternalConnections = ({ let handler: HandleInternalMessage | null = null; let handlerReady: Promise; - if (typeof handleInternalMessage === 'function') { + if (directHandler !== undefined) { // Direct handler - use immediately - handler = handleInternalMessage; - handlerReady = Promise.resolve(handleInternalMessage); + handler = directHandler; + handlerReady = Promise.resolve(directHandler); } else { // Promise-based handler - cache once resolved - handlerReady = handleInternalMessage.then((resolvedHandler) => { + handlerReady = handlerPromise.then((resolvedHandler) => { handler = resolvedHandler; return resolvedHandler; }); diff --git a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts index d7cdfd578..894711634 100644 --- a/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts +++ b/packages/kernel-browser-runtime/src/kernel-worker/kernel-worker.ts @@ -65,7 +65,7 @@ async function main(): Promise { }); receiveInternalConnections({ - handleInternalMessage: handlerP, + handlerPromise: handlerP, logger, }); From 3a40d6a3042b84bc0668f0cbbdbcaab06fe81b85 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 22:48:33 +0000 Subject: [PATCH 3/6] refactor(kernel-browser-runtime): Address PR review concerns - Add test coverage for handlerPromise with delayed resolution - Fix documentation to clarify caching behavior (not buffering) - Add explicit error handling for handlerPromise rejection - Rename handlerReady to handlerResolution for clarity Co-authored-by: Erik Marks --- .../internal-connections.test.ts | 178 ++++++++++++++++++ .../internal-comms/internal-connections.ts | 60 ++++-- 2 files changed, 221 insertions(+), 17 deletions(-) diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts index 3b319cd8a..95cf79e82 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts @@ -455,5 +455,183 @@ describe('internal-connections', () => { expect.any(Error), ); }); + + it('should handle messages with handlerPromise after resolution', async () => { + const handlerPromise = Promise.resolve(mockHandleMessage); + + receiveInternalConnections({ + handlerPromise, + logger, + }); + + const controlChannel = MockBroadcastChannel.channels.get( + COMMS_CONTROL_CHANNEL_NAME, + ); + controlChannel?.onmessage?.( + new MessageEvent('message', { + data: { + method: 'init', + params: { channelName: 'internal-process-channel' }, + }, + }), + ); + + await delay(); + const commsStream = streamInstances[0]!; + expect(commsStream).toBeDefined(); + const commsStreamWriteSpy = vi.spyOn(commsStream, 'write'); + + const commsChannel = MockBroadcastChannel.channels.get( + 'internal-process-channel', + )!; + + // Send first message + commsChannel.onmessage?.( + new MessageEvent('message', { + data: { + method: 'getStatus', + params: null, + id: 1, + }, + }), + ); + await delay(); + + expect(mockHandleMessage).toHaveBeenCalledWith({ + method: 'getStatus', + params: null, + id: 1, + }); + expect(commsStreamWriteSpy).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: 1, + result: { vats: [], clusterConfig: makeClusterConfig() }, + }); + + // Send second message to verify caching (handler should be used directly) + commsChannel.onmessage?.( + new MessageEvent('message', { + data: { + method: 'getStatus', + params: null, + id: 2, + }, + }), + ); + await delay(); + + expect(mockHandleMessage).toHaveBeenCalledTimes(2); + expect(commsStreamWriteSpy).toHaveBeenCalledTimes(2); + }); + + it('should queue messages until handlerPromise resolves', async () => { + let resolveHandler: (handler: typeof mockHandleMessage) => void; + const handlerPromise = new Promise((resolve) => { + resolveHandler = resolve; + }); + + receiveInternalConnections({ + handlerPromise, + logger, + }); + + const controlChannel = MockBroadcastChannel.channels.get( + COMMS_CONTROL_CHANNEL_NAME, + ); + controlChannel?.onmessage?.( + new MessageEvent('message', { + data: { + method: 'init', + params: { channelName: 'internal-process-channel' }, + }, + }), + ); + + await delay(); + const commsStream = streamInstances[0]!; + expect(commsStream).toBeDefined(); + const commsStreamWriteSpy = vi.spyOn(commsStream, 'write'); + + const commsChannel = MockBroadcastChannel.channels.get( + 'internal-process-channel', + )!; + + // Send message before handler is ready + commsChannel.onmessage?.( + new MessageEvent('message', { + data: { + method: 'getStatus', + params: null, + id: 1, + }, + }), + ); + + // Handler should not be called yet + await delay(); + expect(mockHandleMessage).not.toHaveBeenCalled(); + expect(commsStreamWriteSpy).not.toHaveBeenCalled(); + + // Now resolve the handler + resolveHandler!(mockHandleMessage); + await delay(); + + // Now the message should be handled + expect(mockHandleMessage).toHaveBeenCalledWith({ + method: 'getStatus', + params: null, + id: 1, + }); + expect(commsStreamWriteSpy).toHaveBeenCalledWith({ + jsonrpc: '2.0', + id: 1, + result: { vats: [], clusterConfig: makeClusterConfig() }, + }); + }); + + it('should handle handlerPromise rejection', async () => { + const handlerError = new Error('Handler initialization failed'); + const handlerPromise = Promise.reject(handlerError); + + receiveInternalConnections({ + handlerPromise, + logger, + }); + + const controlChannel = MockBroadcastChannel.channels.get( + COMMS_CONTROL_CHANNEL_NAME, + ); + controlChannel?.onmessage?.( + new MessageEvent('message', { + data: { + method: 'init', + params: { channelName: 'internal-process-channel' }, + }, + }), + ); + + await delay(); + const commsChannel = MockBroadcastChannel.channels.get( + 'internal-process-channel', + )!; + + // Send message - should trigger error handling + commsChannel.onmessage?.( + new MessageEvent('message', { + data: { + method: 'getStatus', + params: null, + id: 1, + }, + }), + ); + + await delay(); + + expect(logger.error).toHaveBeenCalledWith( + 'Error initializing message handler for internal process "internal-process-channel":', + handlerError, + ); + }); }); }); diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts index dcbb1d3ea..4a0b3955e 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts @@ -123,8 +123,8 @@ type ReceiveConnectionsOptions = Omit & * @param options.handler - The function to handle internal messages. Mutually exclusive * with `handlerPromise`. * @param options.handlerPromise - A promise that resolves to the handler function. - * Messages will be buffered until the handler is ready, then subsequent messages are - * handled directly. Mutually exclusive with `handler`. + * Incoming messages await handler initialization once, then the resolved handler is + * cached and used directly for all subsequent messages. Mutually exclusive with `handler`. * @param options.logger - The logger instance. * @param options.controlChannelName - The name of the control channel. Must match * the name used by {@link connectToKernel} on the other end. @@ -137,18 +137,24 @@ export const receiveInternalConnections = ({ }: ReceiveConnectionsOptions): void => { // Support both direct handler and promise-based handler let handler: HandleInternalMessage | null = null; - let handlerReady: Promise; + let handlerResolution: Promise; if (directHandler !== undefined) { // Direct handler - use immediately handler = directHandler; - handlerReady = Promise.resolve(directHandler); + handlerResolution = Promise.resolve(directHandler); } else { // Promise-based handler - cache once resolved - handlerReady = handlerPromise.then((resolvedHandler) => { - handler = resolvedHandler; - return resolvedHandler; - }); + handlerResolution = handlerPromise.then( + (resolvedHandler) => { + handler = resolvedHandler; + return resolvedHandler; + }, + (error) => { + // Re-throw to propagate initialization errors to message handlers + throw error; + }, + ); } const seenChannels = new Set(); @@ -177,19 +183,39 @@ export const receiveInternalConnections = ({ `Received message from internal process "${channelName}": ${JSON.stringify(message)}`, ); - // Use cached handler if available, otherwise await once - const messageHandler = handler ?? (await handlerReady); - const reply = await messageHandler(message); - if (reply !== undefined) { - await kernelRpcStream.write(reply); + try { + // Use cached handler if available, otherwise await once + const messageHandler = handler ?? (await handlerResolution); + const reply = await messageHandler(message); + if (reply !== undefined) { + await kernelRpcStream.write(reply); + } + } catch (error) { + // Check if this is a handler initialization error + if (handler === null) { + logger.error( + `Error initializing message handler for internal process "${channelName}":`, + error, + ); + } else { + logger.error( + `Error handling message from internal process "${channelName}":`, + error, + ); + } + throw error; } }); }) .catch((error) => { - logger.error( - `Error handling message from internal process "${channelName}":`, - error, - ); + // This catch handles connection errors and re-thrown handler errors + if (handler !== null) { + logger.error( + `Error handling message from internal process "${channelName}":`, + error, + ); + } + // Initialization errors are already logged in the try-catch above }) .finally(() => { logger.debug(`Closed connection to internal process "${channelName}"`); From 9736e31cdcc0cf112e4640b0b22a5e53cfff079f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:33:13 -0800 Subject: [PATCH 4/6] refactor(kernel-browser-runtime): Simplify error handling in internal-connections Remove overly complex error handling logic that differentiated between handler initialization errors and message handling errors. Replace with simple one-liner handler resolution and single catch block. Co-Authored-By: Claude Opus 4.5 --- .../internal-connections.test.ts | 12 ++-- .../internal-comms/internal-connections.ts | 64 ++++--------------- 2 files changed, 19 insertions(+), 57 deletions(-) diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts index 95cf79e82..63d17e380 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts @@ -526,9 +526,11 @@ describe('internal-connections', () => { it('should queue messages until handlerPromise resolves', async () => { let resolveHandler: (handler: typeof mockHandleMessage) => void; - const handlerPromise = new Promise((resolve) => { - resolveHandler = resolve; - }); + const handlerPromise = new Promise( + (resolve) => { + resolveHandler = resolve; + }, + ); receiveInternalConnections({ handlerPromise, @@ -592,6 +594,8 @@ describe('internal-connections', () => { it('should handle handlerPromise rejection', async () => { const handlerError = new Error('Handler initialization failed'); const handlerPromise = Promise.reject(handlerError); + // Prevent unhandled rejection warning - error is handled by receiveInternalConnections + handlerPromise.catch(() => undefined); receiveInternalConnections({ handlerPromise, @@ -629,7 +633,7 @@ describe('internal-connections', () => { await delay(); expect(logger.error).toHaveBeenCalledWith( - 'Error initializing message handler for internal process "internal-process-channel":', + 'Error handling message from internal process "internal-process-channel":', handlerError, ); }); diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts index 4a0b3955e..d62b27d01 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts @@ -123,39 +123,18 @@ type ReceiveConnectionsOptions = Omit & * @param options.handler - The function to handle internal messages. Mutually exclusive * with `handlerPromise`. * @param options.handlerPromise - A promise that resolves to the handler function. - * Incoming messages await handler initialization once, then the resolved handler is - * cached and used directly for all subsequent messages. Mutually exclusive with `handler`. + * Mutually exclusive with `handler`. * @param options.logger - The logger instance. * @param options.controlChannelName - The name of the control channel. Must match * the name used by {@link connectToKernel} on the other end. */ export const receiveInternalConnections = ({ - handler: directHandler, + handler, handlerPromise, logger, controlChannelName = COMMS_CONTROL_CHANNEL_NAME, }: ReceiveConnectionsOptions): void => { - // Support both direct handler and promise-based handler - let handler: HandleInternalMessage | null = null; - let handlerResolution: Promise; - - if (directHandler !== undefined) { - // Direct handler - use immediately - handler = directHandler; - handlerResolution = Promise.resolve(directHandler); - } else { - // Promise-based handler - cache once resolved - handlerResolution = handlerPromise.then( - (resolvedHandler) => { - handler = resolvedHandler; - return resolvedHandler; - }, - (error) => { - // Re-throw to propagate initialization errors to message handlers - throw error; - }, - ); - } + const handlerResolution = handler ? Promise.resolve(handler) : handlerPromise; const seenChannels = new Set(); new BroadcastChannel(controlChannelName).onmessage = (event) => { @@ -183,39 +162,18 @@ export const receiveInternalConnections = ({ `Received message from internal process "${channelName}": ${JSON.stringify(message)}`, ); - try { - // Use cached handler if available, otherwise await once - const messageHandler = handler ?? (await handlerResolution); - const reply = await messageHandler(message); - if (reply !== undefined) { - await kernelRpcStream.write(reply); - } - } catch (error) { - // Check if this is a handler initialization error - if (handler === null) { - logger.error( - `Error initializing message handler for internal process "${channelName}":`, - error, - ); - } else { - logger.error( - `Error handling message from internal process "${channelName}":`, - error, - ); - } - throw error; + const messageHandler = await handlerResolution; + const reply = await messageHandler(message); + if (reply !== undefined) { + await kernelRpcStream.write(reply); } }); }) .catch((error) => { - // This catch handles connection errors and re-thrown handler errors - if (handler !== null) { - logger.error( - `Error handling message from internal process "${channelName}":`, - error, - ); - } - // Initialization errors are already logged in the try-catch above + logger.error( + `Error handling message from internal process "${channelName}":`, + error, + ); }) .finally(() => { logger.debug(`Closed connection to internal process "${channelName}"`); From 0827983c062b4e842c265d7cccb3a3e98323da3c Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:47:52 -0800 Subject: [PATCH 5/6] refactor: Fix handler caching --- .../src/PlatformServicesClient.test.ts | 6 ++--- .../src/PlatformServicesServer.test.ts | 23 ++++++++----------- .../internal-comms/internal-connections.ts | 17 +++++++++++--- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/packages/kernel-browser-runtime/src/PlatformServicesClient.test.ts b/packages/kernel-browser-runtime/src/PlatformServicesClient.test.ts index f18ece846..5673a8ccd 100644 --- a/packages/kernel-browser-runtime/src/PlatformServicesClient.test.ts +++ b/packages/kernel-browser-runtime/src/PlatformServicesClient.test.ts @@ -419,8 +419,7 @@ describe('PlatformServicesClient', () => { ); await delay(50); - expect(remoteHandler).toHaveBeenCalledOnce(); - expect(remoteHandler).toHaveBeenCalledWith( + expect(remoteHandler).toHaveBeenCalledExactlyOnceWith( 'peer-123', 'test-message', ); @@ -471,8 +470,7 @@ describe('PlatformServicesClient', () => { ); await delay(50); - expect(giveUpHandler).toHaveBeenCalledOnce(); - expect(giveUpHandler).toHaveBeenCalledWith('peer-456'); + expect(giveUpHandler).toHaveBeenCalledExactlyOnceWith('peer-456'); const successResponse = outputs.find( (message) => diff --git a/packages/kernel-browser-runtime/src/PlatformServicesServer.test.ts b/packages/kernel-browser-runtime/src/PlatformServicesServer.test.ts index fd1b1b53c..504221936 100644 --- a/packages/kernel-browser-runtime/src/PlatformServicesServer.test.ts +++ b/packages/kernel-browser-runtime/src/PlatformServicesServer.test.ts @@ -231,8 +231,7 @@ describe('PlatformServicesServer', () => { await stream.receiveInput(makeMessageEvent('m0', { method: 'foo' })); await delay(10); - expect(errorSpy).toHaveBeenCalledOnce(); - expect(errorSpy).toHaveBeenCalledWith( + expect(errorSpy).toHaveBeenCalledExactlyOnceWith( 'Error handling "foo" request:', rpcErrors.methodNotFound(), ); @@ -275,8 +274,9 @@ describe('PlatformServicesServer', () => { await delay(10); expect(workers).toHaveLength(1); - expect(workers[0]?.launch).toHaveBeenCalledOnce(); - expect(workers[0]?.launch).toHaveBeenCalledWith(makeVatConfig()); + expect(workers[0]?.launch).toHaveBeenCalledExactlyOnceWith( + makeVatConfig(), + ); }); it('logs error if a vat with the same id already exists', async () => { @@ -285,8 +285,7 @@ describe('PlatformServicesServer', () => { await stream.receiveInput(makeLaunchMessageEvent('m1', 'v0')); await delay(10); - expect(errorSpy).toHaveBeenCalledOnce(); - expect(errorSpy).toHaveBeenCalledWith( + expect(errorSpy).toHaveBeenCalledExactlyOnceWith( 'Error handling "launch" request:', new VatAlreadyExistsError('v0'), ); @@ -306,8 +305,7 @@ describe('PlatformServicesServer', () => { await delay(10); expect(workers).toHaveLength(1); - expect(workers[0]?.terminate).toHaveBeenCalledOnce(); - expect(workers[0]?.terminate).toHaveBeenCalledWith(); + expect(workers[0]?.terminate).toHaveBeenCalledExactlyOnceWith(); }); it('logs error if a vat with the specified id does not exist', async () => { @@ -315,8 +313,7 @@ describe('PlatformServicesServer', () => { await stream.receiveInput(makeTerminateMessageEvent('m0', 'v0')); await delay(10); - expect(errorSpy).toHaveBeenCalledOnce(); - expect(errorSpy).toHaveBeenCalledWith( + expect(errorSpy).toHaveBeenCalledExactlyOnceWith( 'Error handling "terminate" request:', new VatNotFoundError('v0'), ); @@ -336,8 +333,7 @@ describe('PlatformServicesServer', () => { await stream.receiveInput(makeTerminateMessageEvent('m1', vatId)); await delay(10); - expect(errorSpy).toHaveBeenCalledOnce(); - expect(errorSpy).toHaveBeenCalledWith( + expect(errorSpy).toHaveBeenCalledExactlyOnceWith( 'Error handling "terminate" request:', vatNotFoundError, ); @@ -380,8 +376,7 @@ describe('PlatformServicesServer', () => { await stream.receiveInput(makeTerminateAllMessageEvent('m1')); await delay(10); - expect(errorSpy).toHaveBeenCalledOnce(); - expect(errorSpy).toHaveBeenCalledWith( + expect(errorSpy).toHaveBeenCalledExactlyOnceWith( 'Error handling "terminateAll" request:', vatNotFoundError, ); diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts index d62b27d01..4e461dbfc 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.ts @@ -129,12 +129,23 @@ type ReceiveConnectionsOptions = Omit & * the name used by {@link connectToKernel} on the other end. */ export const receiveInternalConnections = ({ - handler, + handler: directHandler, handlerPromise, logger, controlChannelName = COMMS_CONTROL_CHANNEL_NAME, }: ReceiveConnectionsOptions): void => { - const handlerResolution = handler ? Promise.resolve(handler) : handlerPromise; + let handler: HandleInternalMessage | null = null; + let handlerReady: Promise; + + if (handlerPromise === undefined) { + handler = directHandler; + handlerReady = Promise.resolve(directHandler); + } else { + handlerReady = handlerPromise.then((resolvedHandler) => { + handler = resolvedHandler; + return resolvedHandler; + }); + } const seenChannels = new Set(); new BroadcastChannel(controlChannelName).onmessage = (event) => { @@ -162,7 +173,7 @@ export const receiveInternalConnections = ({ `Received message from internal process "${channelName}": ${JSON.stringify(message)}`, ); - const messageHandler = await handlerResolution; + const messageHandler = handler ?? (await handlerReady); const reply = await messageHandler(message); if (reply !== undefined) { await kernelRpcStream.write(reply); From 5820a935476a0d16d8f0a50fc491863827b590cf Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Wed, 7 Jan 2026 16:01:47 -0800 Subject: [PATCH 6/6] test(kernel-browser-runtime): Remove handlerPromise rejection test This edge case is not expected to occur in practice. Co-Authored-By: Claude Opus 4.5 --- .../internal-connections.test.ts | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts index 63d17e380..004065841 100644 --- a/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts +++ b/packages/kernel-browser-runtime/src/internal-comms/internal-connections.test.ts @@ -590,52 +590,5 @@ describe('internal-connections', () => { result: { vats: [], clusterConfig: makeClusterConfig() }, }); }); - - it('should handle handlerPromise rejection', async () => { - const handlerError = new Error('Handler initialization failed'); - const handlerPromise = Promise.reject(handlerError); - // Prevent unhandled rejection warning - error is handled by receiveInternalConnections - handlerPromise.catch(() => undefined); - - receiveInternalConnections({ - handlerPromise, - logger, - }); - - const controlChannel = MockBroadcastChannel.channels.get( - COMMS_CONTROL_CHANNEL_NAME, - ); - controlChannel?.onmessage?.( - new MessageEvent('message', { - data: { - method: 'init', - params: { channelName: 'internal-process-channel' }, - }, - }), - ); - - await delay(); - const commsChannel = MockBroadcastChannel.channels.get( - 'internal-process-channel', - )!; - - // Send message - should trigger error handling - commsChannel.onmessage?.( - new MessageEvent('message', { - data: { - method: 'getStatus', - params: null, - id: 1, - }, - }), - ); - - await delay(); - - expect(logger.error).toHaveBeenCalledWith( - 'Error handling message from internal process "internal-process-channel":', - handlerError, - ); - }); }); });