diff --git a/packages/errors/src/index.test.ts b/packages/errors/src/index.test.ts index e7d1e62e0..052a8f289 100644 --- a/packages/errors/src/index.test.ts +++ b/packages/errors/src/index.test.ts @@ -7,6 +7,8 @@ describe('index', () => { expect(Object.keys(indexModule).sort()).toStrictEqual([ 'ErrorCode', 'ErrorSentinel', + 'MarshaledErrorStruct', + 'MarshaledOcapErrorStruct', 'StreamReadError', 'VatAlreadyExistsError', 'VatCapTpConnectionExistsError', diff --git a/packages/errors/src/index.ts b/packages/errors/src/index.ts index c2aaf7186..a56fc15c4 100644 --- a/packages/errors/src/index.ts +++ b/packages/errors/src/index.ts @@ -5,7 +5,12 @@ export { VatAlreadyExistsError } from './errors/VatAlreadyExistsError.js'; export { VatDeletedError } from './errors/VatDeletedError.js'; export { VatNotFoundError } from './errors/VatNotFoundError.js'; export { StreamReadError } from './errors/StreamReadError.js'; -export { ErrorCode, ErrorSentinel } from './constants.js'; +export { + ErrorCode, + ErrorSentinel, + MarshaledErrorStruct, + MarshaledOcapErrorStruct, +} from './constants.js'; export { toError } from './utils/toError.js'; export { isOcapError } from './utils/isOcapError.js'; export { marshalError } from './marshal/marshalError.js'; diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index ac6b64133..b2e4ef1a9 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,6 +1,6 @@ import type { Json } from '@metamask/utils'; -import { ClusterCommandMethod, isClusterCommandReply } from '@ocap/kernel'; -import type { ClusterCommand } from '@ocap/kernel'; +import { KernelCommandMethod, isKernelCommandReply } from '@ocap/kernel'; +import type { KernelCommand } from '@ocap/kernel'; import { ChromeRuntimeTarget, ChromeRuntimeDuplexStream } from '@ocap/streams'; const OFFSCREEN_DOCUMENT_PATH = '/offscreen.html'; @@ -31,7 +31,7 @@ async function main(): Promise { * * @param command - The command to send. */ - const sendClusterCommand = async (command: ClusterCommand): Promise => { + const sendClusterCommand = async (command: KernelCommand): Promise => { await offscreenStream.write(command); }; @@ -40,21 +40,21 @@ async function main(): Promise { capTpCall: { value: async (method: string, params: Json[]) => sendClusterCommand({ - method: ClusterCommandMethod.CapTpCall, + method: KernelCommandMethod.capTpCall, params: { method, params }, }), }, evaluate: { value: async (source: string) => sendClusterCommand({ - method: ClusterCommandMethod.Evaluate, + method: KernelCommandMethod.evaluate, params: source, }), }, ping: { value: async () => sendClusterCommand({ - method: ClusterCommandMethod.Ping, + method: KernelCommandMethod.ping, params: null, }), }, @@ -64,14 +64,14 @@ async function main(): Promise { kvGet: { value: async (key: string) => sendClusterCommand({ - method: ClusterCommandMethod.KVGet, + method: KernelCommandMethod.kvGet, params: key, }), }, kvSet: { value: async (key: string, value: string) => sendClusterCommand({ - method: ClusterCommandMethod.KVSet, + method: KernelCommandMethod.kvSet, params: { key, value }, }), }, @@ -81,24 +81,24 @@ async function main(): Promise { // With this we can click the extension action button to wake up the service worker. chrome.action.onClicked.addListener(() => { sendClusterCommand({ - method: ClusterCommandMethod.Ping, + method: KernelCommandMethod.ping, params: null, }).catch(console.error); }); // Handle replies from the offscreen document for await (const message of offscreenStream) { - if (!isClusterCommandReply(message)) { + if (!isKernelCommandReply(message)) { console.error('Background received unexpected message', message); continue; } switch (message.method) { - case ClusterCommandMethod.Evaluate: - case ClusterCommandMethod.CapTpCall: - case ClusterCommandMethod.Ping: - case ClusterCommandMethod.KVGet: - case ClusterCommandMethod.KVSet: + case KernelCommandMethod.evaluate: + case KernelCommandMethod.capTpCall: + case KernelCommandMethod.ping: + case KernelCommandMethod.kvGet: + case KernelCommandMethod.kvSet: console.log(message.params); break; default: diff --git a/packages/extension/src/kernel/VatWorkerClient.test.ts b/packages/extension/src/kernel/VatWorkerClient.test.ts index 3b23a2db5..5ff1dd1c5 100644 --- a/packages/extension/src/kernel/VatWorkerClient.test.ts +++ b/packages/extension/src/kernel/VatWorkerClient.test.ts @@ -40,8 +40,8 @@ describe('ExtensionVatWorkerClient', () => { it.each` method - ${VatWorkerServiceCommandMethod.Launch} - ${VatWorkerServiceCommandMethod.Terminate} + ${VatWorkerServiceCommandMethod.launch} + ${VatWorkerServiceCommandMethod.terminate} `( "calls logger.error when receiving a $method reply it wasn't waiting for", async ({ method }) => { @@ -63,7 +63,7 @@ describe('ExtensionVatWorkerClient', () => { }, ); - it(`calls logger.error when receiving a ${VatWorkerServiceCommandMethod.Launch} reply without a port`, async () => { + it(`calls logger.error when receiving a ${VatWorkerServiceCommandMethod.launch} reply without a port`, async () => { const errorSpy = vi.spyOn(clientLogger, 'error'); const vatId: VatId = 'v0'; // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -71,7 +71,7 @@ describe('ExtensionVatWorkerClient', () => { const reply = { id: 'm1', payload: { - method: VatWorkerServiceCommandMethod.Launch, + method: VatWorkerServiceCommandMethod.launch, params: { vatId: 'v0' }, }, }; diff --git a/packages/extension/src/kernel/VatWorkerClient.ts b/packages/extension/src/kernel/VatWorkerClient.ts index cb6bbf574..84ae3c6f2 100644 --- a/packages/extension/src/kernel/VatWorkerClient.ts +++ b/packages/extension/src/kernel/VatWorkerClient.ts @@ -77,21 +77,21 @@ export class ExtensionVatWorkerClient implements VatWorkerService { vatId: VatId, ): Promise> { return this.#sendMessage({ - method: VatWorkerServiceCommandMethod.Launch, + method: VatWorkerServiceCommandMethod.launch, params: { vatId }, }); } async terminate(vatId: VatId): Promise { return this.#sendMessage({ - method: VatWorkerServiceCommandMethod.Terminate, + method: VatWorkerServiceCommandMethod.terminate, params: { vatId }, }); } async terminateAll(): Promise { return this.#sendMessage({ - method: VatWorkerServiceCommandMethod.TerminateAll, + method: VatWorkerServiceCommandMethod.terminateAll, params: null, }); } @@ -120,7 +120,7 @@ export class ExtensionVatWorkerClient implements VatWorkerService { } switch (method) { - case VatWorkerServiceCommandMethod.Launch: + case VatWorkerServiceCommandMethod.launch: if (!port) { this.#logger.error('Expected a port with message reply', event); return; @@ -132,8 +132,8 @@ export class ExtensionVatWorkerClient implements VatWorkerService { ), ); break; - case VatWorkerServiceCommandMethod.Terminate: - case VatWorkerServiceCommandMethod.TerminateAll: + case VatWorkerServiceCommandMethod.terminate: + case VatWorkerServiceCommandMethod.terminateAll: // If we were caching streams on the client this would be a good place // to remove them. promise.resolve(undefined); diff --git a/packages/extension/src/kernel/VatWorkerServer.test.ts b/packages/extension/src/kernel/VatWorkerServer.test.ts index cb6924a07..365e356a7 100644 --- a/packages/extension/src/kernel/VatWorkerServer.test.ts +++ b/packages/extension/src/kernel/VatWorkerServer.test.ts @@ -76,14 +76,14 @@ describe('ExtensionVatWorkerServer', () => { clientPort.postMessage({ id: 'm0', payload: { - method: VatWorkerServiceCommandMethod.Launch, + method: VatWorkerServiceCommandMethod.launch, params: { vatId }, }, }); clientPort.postMessage({ id: 'm1', payload: { - method: VatWorkerServiceCommandMethod.TerminateAll, + method: VatWorkerServiceCommandMethod.terminateAll, params: null, }, }); @@ -92,7 +92,7 @@ describe('ExtensionVatWorkerServer', () => { expect(errorSpy).toHaveBeenCalledOnce(); expect(errorSpy.mock.lastCall?.[0]).toBe( - `Error handling ${VatWorkerServiceCommandMethod.TerminateAll} for vatId ${vatId}`, + `Error handling ${VatWorkerServiceCommandMethod.terminateAll} for vatId ${vatId}`, ); expect(errorSpy.mock.lastCall?.[1]).toBe(vatNotFoundError); }); diff --git a/packages/extension/src/kernel/VatWorkerServer.ts b/packages/extension/src/kernel/VatWorkerServer.ts index 4abaa31dd..96c13c05b 100644 --- a/packages/extension/src/kernel/VatWorkerServer.ts +++ b/packages/extension/src/kernel/VatWorkerServer.ts @@ -86,17 +86,17 @@ export class ExtensionVatWorkerServer { }; switch (method) { - case VatWorkerServiceCommandMethod.Launch: + case VatWorkerServiceCommandMethod.launch: await this.#launch(params.vatId) .then((port) => this.#postMessage({ id, payload }, [port])) .catch(async (error) => handleError(error, params.vatId)); break; - case VatWorkerServiceCommandMethod.Terminate: + case VatWorkerServiceCommandMethod.terminate: await this.#terminate(params.vatId) .then(() => this.#postMessage({ id, payload })) .catch(async (error) => handleError(error, params.vatId)); break; - case VatWorkerServiceCommandMethod.TerminateAll: + case VatWorkerServiceCommandMethod.terminateAll: await Promise.all( Array.from(this.#vatWorkers.keys()).map(async (vatId) => this.#terminate(vatId).catch((error) => handleError(error, vatId)), diff --git a/packages/extension/src/kernel/kernel-worker.ts b/packages/extension/src/kernel/kernel-worker.ts index 1ee5fefce..f5c99ebc9 100644 --- a/packages/extension/src/kernel/kernel-worker.ts +++ b/packages/extension/src/kernel/kernel-worker.ts @@ -65,7 +65,7 @@ async function runVatLifecycle( const vatToPing = vats[Math.floor(Math.random() * vats.length)] as VatId; console.time(`Ping Vat "${vatToPing}"`); await kernel.sendMessage(vatToPing, { - method: VatCommandMethod.Ping, + method: VatCommandMethod.ping, params: null, }); console.timeEnd(`Ping Vat "${vatToPing}"`); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index d1d0c54bd..a1e34b14e 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -72,10 +72,10 @@ export class Kernel { let vat: Vat; switch (method) { - case KernelCommandMethod.Ping: + case KernelCommandMethod.ping: await this.#reply({ method, params: 'pong' }); break; - case KernelCommandMethod.Evaluate: + case KernelCommandMethod.evaluate: if (!this.#vats.size) { throw new Error('No vats available to call'); } @@ -85,7 +85,7 @@ export class Kernel { params: await this.evaluate(vat.id, params), }); break; - case KernelCommandMethod.CapTpCall: + case KernelCommandMethod.capTpCall: if (!this.#vats.size) { throw new Error('No vats available to call'); } @@ -95,14 +95,14 @@ export class Kernel { params: stringify(await vat.callCapTp(params)), }); break; - case KernelCommandMethod.KVSet: + case KernelCommandMethod.kvSet: this.kvSet(params.key, params.value); await this.#reply({ method, params: `~~~ set "${params.key}" to "${params.value}" ~~~`, }); break; - case KernelCommandMethod.KVGet: { + case KernelCommandMethod.kvGet: { try { const value = this.kvGet(params); const result = @@ -144,7 +144,7 @@ export class Kernel { async evaluate(vatId: VatId, source: string): Promise { try { const result = await this.sendMessage(vatId, { - method: VatCommandMethod.Evaluate, + method: VatCommandMethod.evaluate, params: source, }); return String(result); diff --git a/packages/kernel/src/Supervisor.test.ts b/packages/kernel/src/Supervisor.test.ts index eb56e6160..7111cc29d 100644 --- a/packages/kernel/src/Supervisor.test.ts +++ b/packages/kernel/src/Supervisor.test.ts @@ -78,11 +78,11 @@ describe('Supervisor', () => { await supervisor.handleMessage({ id: 'v0:0', - payload: { method: VatCommandMethod.Ping, params: null }, + payload: { method: VatCommandMethod.ping, params: null }, }); expect(replySpy).toHaveBeenCalledWith('v0:0', { - method: VatCommandMethod.Ping, + method: VatCommandMethod.ping, params: 'pong', }); }); @@ -93,11 +93,11 @@ describe('Supervisor', () => { await supervisor.handleMessage({ id: 'v0:0', - payload: { method: VatCommandMethod.CapTpInit, params: null }, + payload: { method: VatCommandMethod.capTpInit, params: null }, }); expect(replySpy).toHaveBeenCalledWith('v0:0', { - method: VatCommandMethod.CapTpInit, + method: VatCommandMethod.capTpInit, params: '~~~ CapTP Initialized ~~~', }); }); @@ -108,7 +108,7 @@ describe('Supervisor', () => { await supervisor.handleMessage({ id: 'v0:0', - payload: { method: VatCommandMethod.CapTpInit, params: null }, + payload: { method: VatCommandMethod.capTpInit, params: null }, }); const capTpQuestion = { @@ -141,11 +141,11 @@ describe('Supervisor', () => { await supervisor.handleMessage({ id: 'v0:0', - payload: { method: VatCommandMethod.Evaluate, params: '2 + 2' }, + payload: { method: VatCommandMethod.evaluate, params: '2 + 2' }, }); expect(replySpy).toHaveBeenCalledWith('v0:0', { - method: VatCommandMethod.Evaluate, + method: VatCommandMethod.evaluate, params: '4', }); }); @@ -158,7 +158,7 @@ describe('Supervisor', () => { await supervisor.handleMessage({ id: 'v0:0', // @ts-expect-error - invalid params type. - payload: { method: VatCommandMethod.Evaluate, params: null }, + payload: { method: VatCommandMethod.evaluate, params: null }, }); expect(replySpy).not.toHaveBeenCalled(); diff --git a/packages/kernel/src/Supervisor.ts b/packages/kernel/src/Supervisor.ts index 05725edfa..1d3418b9b 100644 --- a/packages/kernel/src/Supervisor.ts +++ b/packages/kernel/src/Supervisor.ts @@ -1,18 +1,11 @@ import { makeCapTP } from '@endo/captp'; +import type { Json } from '@metamask/utils'; import { StreamReadError } from '@ocap/errors'; import type { HandledDuplexStream, StreamMultiplexer } from '@ocap/streams'; import { stringify } from '@ocap/utils'; -import type { - CapTpMessage, - VatCommand, - VatCommandReply, -} from './messages/index.js'; -import { - isCapTpMessage, - isVatCommand, - VatCommandMethod, -} from './messages/index.js'; +import type { VatCommand, VatCommandReply } from './messages/index.js'; +import { isVatCommand, VatCommandMethod } from './messages/index.js'; type SupervisorConstructorProps = { id: string; @@ -27,7 +20,7 @@ export class Supervisor { readonly #commandStream: HandledDuplexStream; - readonly #capTpStream: HandledDuplexStream; + readonly #capTpStream: HandledDuplexStream; readonly #defaultCompartment = new Compartment({ URL }); @@ -41,12 +34,11 @@ export class Supervisor { this.#multiplexer = multiplexer; this.#commandStream = multiplexer.addChannel( 'command', - isVatCommand, this.handleMessage.bind(this), + isVatCommand, ); this.#capTpStream = multiplexer.addChannel( 'capTp', - isCapTpMessage, // eslint-disable-next-line no-void async (content): Promise => void this.capTp?.dispatch(content), ); @@ -76,7 +68,7 @@ export class Supervisor { */ async handleMessage({ id, payload }: VatCommand): Promise { switch (payload.method) { - case VatCommandMethod.Evaluate: { + case VatCommandMethod.evaluate: { if (typeof payload.params !== 'string') { console.error( 'Supervisor received command with unexpected params', @@ -87,27 +79,26 @@ export class Supervisor { } const result = this.evaluate(payload.params); await this.replyToMessage(id, { - method: VatCommandMethod.Evaluate, + method: VatCommandMethod.evaluate, params: stringify(result), }); break; } - case VatCommandMethod.CapTpInit: { + case VatCommandMethod.capTpInit: { this.capTp = makeCapTP( 'iframe', - async (content: unknown) => - this.#capTpStream.write(content as CapTpMessage), + async (content: Json) => this.#capTpStream.write(content), this.#bootstrap, ); await this.replyToMessage(id, { - method: VatCommandMethod.CapTpInit, + method: VatCommandMethod.capTpInit, params: '~~~ CapTP Initialized ~~~', }); break; } - case VatCommandMethod.Ping: + case VatCommandMethod.ping: await this.replyToMessage(id, { - method: VatCommandMethod.Ping, + method: VatCommandMethod.ping, params: 'pong', }); break; diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts index 4385c2f88..c41b54758 100644 --- a/packages/kernel/src/Vat.test.ts +++ b/packages/kernel/src/Vat.test.ts @@ -56,7 +56,7 @@ describe('Vat', () => { await vat.init(); expect(sendMessageMock).toHaveBeenCalledWith({ - method: VatCommandMethod.Ping, + method: VatCommandMethod.ping, params: null, }); expect(capTpMock).toHaveBeenCalled(); @@ -83,7 +83,7 @@ describe('Vat', () => { it('sends a message and resolves the promise', async () => { const { vat } = await makeVat(); const mockMessage = { - method: VatCommandMethod.Ping, + method: VatCommandMethod.ping, params: null, } as VatCommand['payload']; const sendMessagePromise = vat.sendMessage(mockMessage); @@ -98,7 +98,7 @@ describe('Vat', () => { const { vat } = await makeVat(); const mockMessageId = 'v0:1'; const mockPayload: VatCommandReply['payload'] = { - method: VatCommandMethod.Evaluate, + method: VatCommandMethod.evaluate, params: 'test-response', }; const mockPromiseKit = { resolve: vi.fn(), reject: vi.fn() }; @@ -114,7 +114,7 @@ describe('Vat', () => { const nonExistentMessageId = 'v0:9'; const mockPayload: VatCommandReply['payload'] = { - method: VatCommandMethod.Ping, + method: VatCommandMethod.ping, params: 'pong', }; @@ -165,7 +165,7 @@ describe('Vat', () => { .mockResolvedValueOnce(undefined); await vat.makeCapTp(); expect(sendMessageMock).toHaveBeenCalledWith({ - method: VatCommandMethod.CapTpInit, + method: VatCommandMethod.capTpInit, params: null, }); }); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index aa742e8a7..82da1ac68 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -1,6 +1,7 @@ import { makeCapTP } from '@endo/captp'; import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; +import type { Json } from '@metamask/utils'; import { VatCapTpConnectionExistsError, VatCapTpConnectionNotFoundError, @@ -11,13 +12,8 @@ import type { HandledDuplexStream, StreamMultiplexer } from '@ocap/streams'; import type { Logger } from '@ocap/utils'; import { makeLogger, makeCounter, stringify } from '@ocap/utils'; -import { - isCapTpMessage, - isVatCommandReply, - VatCommandMethod, -} from './messages/index.js'; +import { isVatCommandReply, VatCommandMethod } from './messages/index.js'; import type { - CapTpMessage, CapTpPayload, VatCommandReply, VatCommand, @@ -37,7 +33,7 @@ export class Vat { readonly #commandStream: HandledDuplexStream; - readonly #capTpStream: HandledDuplexStream; + readonly #capTpStream: HandledDuplexStream; readonly logger: Logger; @@ -55,12 +51,11 @@ export class Vat { this.#multiplexer = multiplexer; this.#commandStream = multiplexer.addChannel( 'command', - isVatCommandReply, this.handleMessage.bind(this), + isVatCommandReply, ); this.#capTpStream = multiplexer.addChannel( 'capTp', - isCapTpMessage, async (content): Promise => { this.logger.log('CapTP from vat', stringify(content)); this.capTp?.dispatch(content); @@ -97,7 +92,7 @@ export class Vat { throw new StreamReadError({ vatId: this.id }, error); }); - await this.sendMessage({ method: VatCommandMethod.Ping, params: null }); + await this.sendMessage({ method: VatCommandMethod.ping, params: null }); this.logger.debug('Created'); return await this.makeCapTp(); @@ -113,15 +108,15 @@ export class Vat { throw new VatCapTpConnectionExistsError(this.id); } - const ctp = makeCapTP(this.id, async (content: unknown) => { + const ctp = makeCapTP(this.id, async (content: Json) => { this.logger.log('CapTP to vat', stringify(content)); - await this.#capTpStream.write(content as CapTpMessage); + await this.#capTpStream.write(content); }); this.capTp = ctp; return this.sendMessage({ - method: VatCommandMethod.CapTpInit, + method: VatCommandMethod.capTpInit, params: null, }); } diff --git a/packages/kernel/src/index.test.ts b/packages/kernel/src/index.test.ts index c11c0bd83..543c6417f 100644 --- a/packages/kernel/src/index.test.ts +++ b/packages/kernel/src/index.test.ts @@ -5,16 +5,19 @@ import * as indexModule from './index.js'; describe('index', () => { it('has the expected exports', () => { - expect(Object.keys(indexModule)).toStrictEqual( - expect.arrayContaining( - ['Kernel', 'Vat'].concat( - ['Cluster', 'Kernel', 'Vat', 'VatWorkerService'].flatMap((value) => [ - `${value}CommandMethod`, - `is${value}Command`, - `is${value}CommandReply`, - ]), - ), - ), - ); + expect(Object.keys(indexModule).sort()).toStrictEqual([ + 'Kernel', + 'KernelCommandMethod', + 'Supervisor', + 'Vat', + 'VatCommandMethod', + 'VatWorkerServiceCommandMethod', + 'isKernelCommand', + 'isKernelCommandReply', + 'isVatCommand', + 'isVatCommandReply', + 'isVatWorkerServiceCommand', + 'isVatWorkerServiceCommandReply', + ]); }); }); diff --git a/packages/kernel/src/messages/captp.test.ts b/packages/kernel/src/messages/captp.test.ts deleted file mode 100644 index 3a49222ea..000000000 --- a/packages/kernel/src/messages/captp.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { isCapTpMessage, isCapTpPayload } from './captp.js'; - -describe('isCapTpMessage', () => { - it.each` - value | expectedResult | description - ${{ type: 'CTP_CALL', epoch: 0 }} | ${true} | ${'valid type with numerical epoch'} - ${{ type: 'CTP_CALL' }} | ${false} | ${'missing epoch'} - `('returns $expectedResult for $description', ({ value, expectedResult }) => { - expect(isCapTpMessage(value)).toBe(expectedResult); - }); -}); - -describe('isCapTpPayload', () => { - it.each` - value | expectedResult | description - ${{ method: 'foo', params: [0, 'bar', false] }} | ${true} | ${'valid command with string data'} - ${{ method: 'foo' }} | ${false} | ${'no params'} - ${{ method: 'foo', params: 'bar' }} | ${false} | ${'nonarray params'} - `('returns $expectedResult for $description', ({ value, expectedResult }) => { - expect(isCapTpPayload(value)).toBe(expectedResult); - }); -}); diff --git a/packages/kernel/src/messages/captp.ts b/packages/kernel/src/messages/captp.ts deleted file mode 100644 index 9f0d7d9fe..000000000 --- a/packages/kernel/src/messages/captp.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Json } from '@metamask/utils'; -import { isObject } from '@metamask/utils'; - -export type CapTpPayload = { - method: string; - params: Json[]; -}; - -export const isCapTpPayload = (value: unknown): value is CapTpPayload => - isObject(value) && - typeof value.method === 'string' && - Array.isArray(value.params); - -export type CapTpMessage = { - type: Type; - epoch: number; - [key: string]: Json; -}; - -export const isCapTpMessage = (value: unknown): value is CapTpMessage => - isObject(value) && - typeof value.type === 'string' && - value.type.startsWith('CTP_') && - typeof value.epoch === 'number'; diff --git a/packages/kernel/src/messages/cluster.ts b/packages/kernel/src/messages/cluster.ts deleted file mode 100644 index c98524af0..000000000 --- a/packages/kernel/src/messages/cluster.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { TypeGuard } from '@ocap/utils'; - -import { kernelCommand } from './kernel.js'; -import { makeMessageKit } from './message-kit.js'; - -const clusterCommand = { - ...kernelCommand, -}; - -const clusterCommandKit = makeMessageKit(clusterCommand); - -export const ClusterCommandMethod = clusterCommandKit.methods; - -export type ClusterCommand = typeof clusterCommandKit.send; -export const isClusterCommand: TypeGuard = - clusterCommandKit.sendGuard; - -export type ClusterCommandReply = typeof clusterCommandKit.reply; -export const isClusterCommandReply: TypeGuard = - clusterCommandKit.replyGuard; diff --git a/packages/kernel/src/messages/index.ts b/packages/kernel/src/messages/index.ts index 8d2e9d511..7cb1fb05b 100644 --- a/packages/kernel/src/messages/index.ts +++ b/packages/kernel/src/messages/index.ts @@ -1,17 +1,3 @@ -// CapTP. - -export { isCapTpPayload, isCapTpMessage } from './captp.js'; -export type { CapTpPayload, CapTpMessage } from './captp.js'; - -// Cluster commands. - -export { - ClusterCommandMethod, - isClusterCommand, - isClusterCommandReply, -} from './cluster.js'; -export type { ClusterCommand, ClusterCommandReply } from './cluster.js'; - // Kernel commands. export { @@ -19,7 +5,11 @@ export { isKernelCommand, isKernelCommandReply, } from './kernel.js'; -export type { KernelCommand, KernelCommandReply } from './kernel.js'; +export type { + CapTpPayload, + KernelCommand, + KernelCommandReply, +} from './kernel.js'; // Vat commands. diff --git a/packages/kernel/src/messages/kernel.test.ts b/packages/kernel/src/messages/kernel.test.ts index 6e8f2a3d6..ab95c0440 100644 --- a/packages/kernel/src/messages/kernel.test.ts +++ b/packages/kernel/src/messages/kernel.test.ts @@ -9,12 +9,12 @@ import { describe('isKernelCommand', () => { it.each` value | expectedResult | description - ${{ method: KernelCommandMethod.KVGet, params: 'data' }} | ${true} | ${'valid command with string data'} - ${{ method: KernelCommandMethod.KVSet, params: { key: 'foo', value: 'bar' } }} | ${true} | ${'valid command with object data'} + ${{ method: KernelCommandMethod.kvGet, params: 'data' }} | ${true} | ${'valid command with string data'} + ${{ method: KernelCommandMethod.kvSet, params: { key: 'foo', value: 'bar' } }} | ${true} | ${'valid command with object data'} ${123} | ${false} | ${'invalid command: primitive number'} ${{ method: true, params: 'data' }} | ${false} | ${'invalid command: invalid type'} - ${{ method: KernelCommandMethod.KVSet }} | ${false} | ${'invalid command: missing data'} - ${{ method: KernelCommandMethod.KVSet, params: 123 }} | ${false} | ${'invalid command: data is a primitive number'} + ${{ method: KernelCommandMethod.kvSet }} | ${false} | ${'invalid command: missing data'} + ${{ method: KernelCommandMethod.kvSet, params: 123 }} | ${false} | ${'invalid command: data is a primitive number'} ${{ method: 123, params: null }} | ${false} | ${'invalid command: invalid type and valid data'} ${{ method: 'some-type', params: true }} | ${false} | ${'invalid command: valid type and invalid data'} `('returns $expectedResult for $description', ({ value, expectedResult }) => { @@ -25,12 +25,12 @@ describe('isKernelCommand', () => { describe('isKernelCommandReply', () => { it.each` value | expectedResult | description - ${{ method: KernelCommandMethod.KVGet, params: 'foo' }} | ${true} | ${'valid command reply with string data'} - ${{ method: KernelCommandMethod.KVGet, params: null }} | ${false} | ${'invalid command reply: with null data'} + ${{ method: KernelCommandMethod.kvGet, params: 'foo' }} | ${true} | ${'valid command reply with string data'} + ${{ method: KernelCommandMethod.kvGet, params: null }} | ${false} | ${'invalid command reply: with null data'} ${123} | ${false} | ${'invalid command reply: primitive number'} ${{ method: true, params: 'data' }} | ${false} | ${'invalid command reply: invalid type'} - ${{ method: KernelCommandMethod.KVSet }} | ${false} | ${'invalid command reply: missing data'} - ${{ method: KernelCommandMethod.KVSet, params: 123 }} | ${false} | ${'invalid command reply: data is a primitive number'} + ${{ method: KernelCommandMethod.kvSet }} | ${false} | ${'invalid command reply: missing data'} + ${{ method: KernelCommandMethod.kvSet, params: 123 }} | ${false} | ${'invalid command reply: data is a primitive number'} ${{ method: 123, params: null }} | ${false} | ${'invalid command reply: invalid type and valid data'} ${{ method: 'some-type', params: true }} | ${false} | ${'invalid command reply: valid type and invalid data'} `('returns $expectedResult for $description', ({ value, expectedResult }) => { diff --git a/packages/kernel/src/messages/kernel.ts b/packages/kernel/src/messages/kernel.ts index 6ec8eb690..6edba84b5 100644 --- a/packages/kernel/src/messages/kernel.ts +++ b/packages/kernel/src/messages/kernel.ts @@ -1,41 +1,77 @@ -import { isObject } from '@metamask/utils'; +import { + object, + union, + literal, + string, + is, + array, +} from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { UnsafeJsonStruct } from '@metamask/utils'; import type { TypeGuard } from '@ocap/utils'; -import type { CapTpPayload } from './captp.js'; -import { isCapTpPayload } from './captp.js'; -import { makeMessageKit, messageType } from './message-kit.js'; -import { vatTestCommand } from './vat-test.js'; +import { + VatTestCommandMethod, + VatTestMethodStructs, + VatTestReplyStructs, +} from './vat.js'; -export const kernelCommand = { - CapTpCall: messageType( - (send) => isCapTpPayload(send), - (reply) => typeof reply === 'string', - ), +export const KernelCommandMethod = { + evaluate: VatTestCommandMethod.evaluate, + capTpCall: 'capTpCall', + kvSet: 'kvSet', + kvGet: 'kvGet', + ping: VatTestCommandMethod.ping, +} as const; - KVSet: messageType<{ key: string; value: string }, string>( - (send) => - isObject(send) && - typeof send.key === 'string' && - typeof send.value === 'string', - (reply) => typeof reply === 'string', - ), +const CapTpPayloadStruct = object({ + method: string(), + params: array(UnsafeJsonStruct), +}); - KVGet: messageType( - (send) => typeof send === 'string', - (reply) => typeof reply === 'string', - ), +export type CapTpPayload = Infer; - ...vatTestCommand, -}; +const KernelCommandStruct = union([ + object({ + method: literal(KernelCommandMethod.capTpCall), + params: CapTpPayloadStruct, + }), + object({ + method: literal(KernelCommandMethod.kvSet), + params: object({ key: string(), value: string() }), + }), + object({ + method: literal(KernelCommandMethod.kvGet), + params: string(), + }), + VatTestMethodStructs.evaluate, + VatTestMethodStructs.ping, +]); -const kernelCommandKit = makeMessageKit(kernelCommand); +const KernelCommandReplyStruct = union([ + object({ + method: literal(KernelCommandMethod.capTpCall), + params: string(), + }), + object({ + method: literal(KernelCommandMethod.kvSet), + params: string(), + }), + object({ + method: literal(KernelCommandMethod.kvGet), + params: string(), + }), + VatTestReplyStructs.evaluate, + VatTestReplyStructs.ping, +]); -export const KernelCommandMethod = kernelCommandKit.methods; +export type KernelCommand = Infer; +export type KernelCommandReply = Infer; -export type KernelCommand = typeof kernelCommandKit.send; -export const isKernelCommand: TypeGuard = - kernelCommandKit.sendGuard; +export const isKernelCommand: TypeGuard = ( + value: unknown, +): value is KernelCommand => is(value, KernelCommandStruct); -export type KernelCommandReply = typeof kernelCommandKit.reply; -export const isKernelCommandReply: TypeGuard = - kernelCommandKit.replyGuard; +export const isKernelCommandReply: TypeGuard = ( + value: unknown, +): value is KernelCommandReply => is(value, KernelCommandReplyStruct); diff --git a/packages/kernel/src/messages/message-kit.ts b/packages/kernel/src/messages/message-kit.ts deleted file mode 100644 index 841b843f3..000000000 --- a/packages/kernel/src/messages/message-kit.ts +++ /dev/null @@ -1,140 +0,0 @@ -import '@ocap/shims/endoify'; - -import { isObject } from '@metamask/utils'; -import type { Json } from '@metamask/utils'; -import type { ExtractGuardType, TypeGuard } from '@ocap/utils'; - -import { isMessageLike } from './message.js'; -import { uncapitalize } from './utils.js'; - -// Message kit. - -export type BoolExpr = (value: unknown) => boolean; - -export type SourceLike = Record; - -type MessageUnion = { - [Key in keyof Source]: Key extends string - ? { - method: Uncapitalize; - params: ExtractGuardType; - } - : never; -}[keyof Source]; - -export type Send = MessageUnion; - -export type Reply = MessageUnion; - -/** - * A typescript utility used to reduce boilerplate in message type declarations. - * - * @param sendGuard - A boolean expression that returns true for SendType values. - * @param replyGuard - A boolean expression that returns true for ReplyType values. - * @returns A pair of type guards. - */ -export const messageType = ( - sendGuard: BoolExpr, - replyGuard: BoolExpr, -): [TypeGuard, TypeGuard] => [ - (val): val is SendType => sendGuard(val), - (val): val is ReplyType => replyGuard(val), -]; - -type Methods = { - [Key in keyof Source]: Key extends string ? Uncapitalize : never; -}; - -const makeMethods = ( - source: Source, -): Methods => { - return Object.fromEntries( - Object.keys(source).map((key) => [key, uncapitalize(key)]), - ) as Methods; -}; - -const makeGuard = ( - source: Source, - methods: Methods, - index: Index, -): TypeGuard> => { - const guards = Object.fromEntries( - Object.entries(source).map(([key, value]) => [ - uncapitalize(key), - value[index], - ]), - ) as Record>; - - return (value: unknown): value is MessageUnion => - isMessageLike(value) && - Object.values(methods).includes(value.method) && - guards[value.method as keyof typeof guards](value.params); -}; - -/** - * An object type encapsulating all of the schematics that define a functional - * group of messages. - */ -export type MessageKit = { - source: Source; - methods: Methods; - send: Send; - sendGuard: TypeGuard>; - reply: Reply; - replyGuard: TypeGuard>; -}; - -export const makeMessageKit = ( - source: Source, -): MessageKit => { - const methods = makeMethods(source); - - return { - source, - methods, - sendGuard: makeGuard(source, methods, 0), - replyGuard: makeGuard(source, methods, 1), - } as MessageKit; -}; - -/** - * An object type encapsulating all of the schematics that define a functional - * group of messages as a payload wrapped with a message id. - */ -type IdentifiedMessageKit< - Source extends SourceLike, - MessageId extends string, -> = { - source: Source; - methods: Methods; - send: { id: MessageId; payload: Send }; - sendGuard: TypeGuard<{ id: MessageId; payload: Send }>; - reply: { id: MessageId; payload: Reply }; - replyGuard: TypeGuard<{ id: MessageId; payload: Reply }>; -}; - -export const makeIdentifiedMessageKit = < - Source extends SourceLike, - MessageId extends string, ->({ - source, - isMessageId, -}: { - source: Source; - isMessageId: TypeGuard; -}): IdentifiedMessageKit => { - const messageKit = makeMessageKit(source); - - return { - source: messageKit.source, - methods: messageKit.methods, - sendGuard: (value: unknown) => - isObject(value) && - isMessageId(value.id) && - messageKit.sendGuard(value.payload), - replyGuard: (value: unknown) => - isObject(value) && - isMessageId(value.id) && - messageKit.replyGuard(value.payload), - } as IdentifiedMessageKit; -}; diff --git a/packages/kernel/src/messages/message.ts b/packages/kernel/src/messages/message.ts deleted file mode 100644 index cd0c2ca10..000000000 --- a/packages/kernel/src/messages/message.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { is } from '@metamask/superstruct'; -import type { Json } from '@metamask/utils'; -import { isObject, UnsafeJsonStruct } from '@metamask/utils'; - -import { uncapitalize } from './utils.js'; - -const isJsonUnsafe = (value: unknown): value is Json => - is(value, UnsafeJsonStruct); - -export type MessageLike = { method: Uncapitalize; params: Json }; - -const isMethodLike = (value: unknown): value is Uncapitalize => - typeof value === 'string' && uncapitalize(value) === value; - -export const isMessageLike = (value: unknown): value is MessageLike => - isObject(value) && isMethodLike(value.method) && isJsonUnsafe(value.params); diff --git a/packages/kernel/src/messages/utils.ts b/packages/kernel/src/messages/utils.ts deleted file mode 100644 index ca9d7b510..000000000 --- a/packages/kernel/src/messages/utils.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Uncapitalize. - -export const uncapitalize = (value: string): Uncapitalize => - (value.at(0)?.toLowerCase() + value.slice(1)) as Uncapitalize; diff --git a/packages/kernel/src/messages/vat-test.ts b/packages/kernel/src/messages/vat-test.ts deleted file mode 100644 index 74f257090..000000000 --- a/packages/kernel/src/messages/vat-test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { messageType } from './message-kit.js'; - -export const vatTestCommand = { - Evaluate: messageType( - (send) => typeof send === 'string', - (reply) => typeof reply === 'string', - ), - - Ping: messageType( - (send) => send === null, - (reply) => reply === 'pong', - ), -}; diff --git a/packages/kernel/src/messages/vat-worker-service.test.ts b/packages/kernel/src/messages/vat-worker-service.test.ts index 0989dd5b2..7a2dc5403 100644 --- a/packages/kernel/src/messages/vat-worker-service.test.ts +++ b/packages/kernel/src/messages/vat-worker-service.test.ts @@ -15,15 +15,15 @@ import { import type { VatId } from '../types.js'; const launchPayload: VatWorkerServiceCommandReply['payload'] = harden({ - method: VatWorkerServiceCommandMethod.Launch, + method: VatWorkerServiceCommandMethod.launch, params: { vatId: 'v0' }, }); const terminatePayload: VatWorkerServiceCommandReply['payload'] = harden({ - method: VatWorkerServiceCommandMethod.Terminate, + method: VatWorkerServiceCommandMethod.terminate, params: { vatId: 'v0' }, }); const terminateAllPayload: VatWorkerServiceCommandReply['payload'] = harden({ - method: VatWorkerServiceCommandMethod.TerminateAll, + method: VatWorkerServiceCommandMethod.terminateAll, params: null, }); diff --git a/packages/kernel/src/messages/vat-worker-service.ts b/packages/kernel/src/messages/vat-worker-service.ts index 1d9f4355f..dabf75cd5 100644 --- a/packages/kernel/src/messages/vat-worker-service.ts +++ b/packages/kernel/src/messages/vat-worker-service.ts @@ -1,65 +1,72 @@ -import { hasProperty, isObject } from '@metamask/utils'; -import { isMarshaledError } from '@ocap/errors'; -import type { MarshaledError } from '@ocap/errors'; +import { object, union, optional, is, literal } from '@metamask/superstruct'; +import type { Infer } from '@metamask/superstruct'; +import { MarshaledErrorStruct } from '@ocap/errors'; import type { TypeGuard } from '@ocap/utils'; -import { makeIdentifiedMessageKit, messageType } from './message-kit.js'; -import type { VatId } from '../types.js'; -import { isVatId } from '../types.js'; +import { VatIdStruct, VatMessageIdStruct } from '../types.js'; -const hasOptionalMarshaledError = (value: object): boolean => - !hasProperty(value, 'error') || isMarshaledError(value.error); +export const VatWorkerServiceCommandMethod = { + launch: 'launch', + terminate: 'terminate', + terminateAll: 'terminateAll', +} as const; -export const vatWorkerServiceCommand = { - Launch: messageType< - { vatId: VatId }, - { vatId: VatId; error?: MarshaledError } - >( - (send) => isObject(send) && isVatId(send.vatId), - (reply) => - isObject(reply) && - isVatId(reply.vatId) && - hasOptionalMarshaledError(reply), - ), - - Terminate: messageType< - { vatId: VatId }, - { vatId: VatId; error?: MarshaledError } - >( - (send) => isObject(send) && isVatId(send.vatId), - (reply) => - isObject(reply) && - isVatId(reply.vatId) && - hasOptionalMarshaledError(reply), - ), - - TerminateAll: messageType< - null, - null | { vatId?: VatId; error: MarshaledError } - >( - (send) => send === null, - (reply) => - reply === null || - (isObject(reply) && - isMarshaledError(reply.error) && - (!hasProperty(reply, 'vatId') || isVatId(reply.vatId))), - ), -}; +const VatWorkerServiceCommandStruct = object({ + id: VatMessageIdStruct, + payload: union([ + object({ + method: literal(VatWorkerServiceCommandMethod.launch), + params: object({ vatId: VatIdStruct }), + }), + object({ + method: literal(VatWorkerServiceCommandMethod.terminate), + params: object({ vatId: VatIdStruct }), + }), + object({ + method: literal(VatWorkerServiceCommandMethod.terminateAll), + params: literal(null), + }), + ]), +}); -const messageKit = makeIdentifiedMessageKit({ - source: vatWorkerServiceCommand, - isMessageId: (value: unknown): value is `m${number}` => - typeof value === 'string' && - value.at(0) === 'm' && - value.slice(1) === String(Number(value.slice(1))), +const VatWorkerServiceCommandReplyStruct = object({ + id: VatMessageIdStruct, + payload: union([ + object({ + method: union([ + literal(VatWorkerServiceCommandMethod.launch), + literal(VatWorkerServiceCommandMethod.terminate), + ]), + params: object({ + vatId: VatIdStruct, + error: optional(MarshaledErrorStruct), + }), + }), + object({ + method: literal(VatWorkerServiceCommandMethod.terminateAll), + params: union([ + literal(null), + object({ + vatId: optional(VatIdStruct), + error: MarshaledErrorStruct, + }), + ]), + }), + ]), }); -export const VatWorkerServiceCommandMethod = messageKit.methods; +export type VatWorkerServiceCommand = Infer< + typeof VatWorkerServiceCommandStruct +>; +export type VatWorkerServiceCommandReply = Infer< + typeof VatWorkerServiceCommandReplyStruct +>; -export type VatWorkerServiceCommand = typeof messageKit.send; -export const isVatWorkerServiceCommand: TypeGuard = - messageKit.sendGuard; +export const isVatWorkerServiceCommand: TypeGuard = ( + value: unknown, +): value is VatWorkerServiceCommand => is(value, VatWorkerServiceCommandStruct); -export type VatWorkerServiceCommandReply = typeof messageKit.reply; -export const isVatWorkerServiceCommandReply: TypeGuard = - messageKit.replyGuard; +export const isVatWorkerServiceCommandReply: TypeGuard< + VatWorkerServiceCommandReply +> = (value: unknown): value is VatWorkerServiceCommandReply => + is(value, VatWorkerServiceCommandReplyStruct); diff --git a/packages/kernel/src/messages/vat.test.ts b/packages/kernel/src/messages/vat.test.ts index 8b28c63df..be829cecc 100644 --- a/packages/kernel/src/messages/vat.test.ts +++ b/packages/kernel/src/messages/vat.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import { isVatCommand, VatCommandMethod } from './vat.js'; describe('isVatCommand', () => { - const payload = { method: VatCommandMethod.Evaluate, params: '3 + 3' }; + const payload = { method: VatCommandMethod.evaluate, params: '3 + 3' }; it.each` value | expectedResult | description diff --git a/packages/kernel/src/messages/vat.ts b/packages/kernel/src/messages/vat.ts index 18c1ff58b..d6f704866 100644 --- a/packages/kernel/src/messages/vat.ts +++ b/packages/kernel/src/messages/vat.ts @@ -18,39 +18,78 @@ const isVatMessageId = (value: unknown): value is VatMessageId => /^\w+:\d+$/u.test(value) && isVatId(value.split(':')[0]); -export enum VatCommandMethod { - Evaluate = 'evaluate', - Ping = 'ping', - CapTpInit = 'capTpInit', -} +export const VatTestCommandMethod = { + evaluate: 'evaluate', + ping: 'ping', +} as const; + +export const VatCommandMethod = { + ...VatTestCommandMethod, + capTpInit: 'capTpInit', +} as const; const VatMessageIdStruct = refine(string(), 'VatMessageId', isVatMessageId); +export const VatTestMethodStructs = { + [VatCommandMethod.evaluate]: object({ + method: literal(VatCommandMethod.evaluate), + params: string(), + }), + [VatCommandMethod.ping]: object({ + method: literal(VatCommandMethod.ping), + params: literal(null), + }), +} as const; + +const VatMethodStructs = { + ...VatTestMethodStructs, + [VatCommandMethod.capTpInit]: object({ + method: literal(VatCommandMethod.capTpInit), + params: literal(null), + }), +} as const; + const VatCommandStruct = object({ id: VatMessageIdStruct, payload: union([ - object({ method: literal(VatCommandMethod.Evaluate), params: string() }), - object({ method: literal(VatCommandMethod.Ping), params: literal(null) }), - object({ - method: literal(VatCommandMethod.CapTpInit), - params: literal(null), - }), + VatMethodStructs.evaluate, + VatMethodStructs.ping, + VatMethodStructs.capTpInit, ]), }); export type VatCommand = Infer; -export type VatCommandReply = Infer; +export const VatTestReplyStructs = { + [VatCommandMethod.evaluate]: object({ + method: literal(VatCommandMethod.evaluate), + params: string(), + }), + [VatCommandMethod.ping]: object({ + method: literal(VatCommandMethod.ping), + params: string(), + }), +} as const; + +const VatReplyStructs = { + ...VatTestReplyStructs, + [VatCommandMethod.capTpInit]: object({ + method: literal(VatCommandMethod.capTpInit), + params: string(), + }), +} as const; const VatCommandReplyStruct = object({ id: VatMessageIdStruct, payload: union([ - object({ method: literal(VatCommandMethod.Evaluate), params: string() }), - object({ method: literal(VatCommandMethod.Ping), params: string() }), - object({ method: literal(VatCommandMethod.CapTpInit), params: string() }), + VatReplyStructs.evaluate, + VatReplyStructs.ping, + VatReplyStructs.capTpInit, ]), }); +export type VatCommandReply = Infer; + export const isVatCommand = (value: unknown): value is VatCommand => is(value, VatCommandStruct); diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index a5f5c51b2..e2d88eb25 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -1,4 +1,5 @@ import type { PromiseKit } from '@endo/promise-kit'; +import { define } from '@metamask/superstruct'; import type { DuplexStream, MultiplexEnvelope } from '@ocap/streams'; export type VatId = `v${number}`; @@ -8,6 +9,20 @@ export const isVatId = (value: unknown): value is VatId => value.at(0) === 'v' && value.slice(1) === String(Number(value.slice(1))); +export const VatIdStruct = define('VatId', isVatId); + +export type VatMessageId = `m${number}`; + +export const isVatMessageId = (value: unknown): value is VatMessageId => + typeof value === 'string' && + value.at(0) === 'm' && + value.slice(1) === String(Number(value.slice(1))); + +export const VatMessageIdStruct = define( + 'VatMessageId', + isVatMessageId, +); + export type PromiseCallbacks = Omit< PromiseKit, 'promise' diff --git a/packages/streams/src/ChromeRuntimeStream.test.ts b/packages/streams/src/ChromeRuntimeStream.test.ts index ea1572641..c813313b1 100644 --- a/packages/streams/src/ChromeRuntimeStream.test.ts +++ b/packages/streams/src/ChromeRuntimeStream.test.ts @@ -400,8 +400,8 @@ describe('ChromeRuntimeMultiplexer', () => { const handleRead = vi.fn(); multiplexer.addChannel( '1', - (value: unknown): value is number => typeof value === 'number', handleRead, + (value: unknown): value is number => typeof value === 'number', ); const drainP = multiplexer.drainAll(); diff --git a/packages/streams/src/MessagePortStream.test.ts b/packages/streams/src/MessagePortStream.test.ts index 034a248a3..89d036b4b 100644 --- a/packages/streams/src/MessagePortStream.test.ts +++ b/packages/streams/src/MessagePortStream.test.ts @@ -229,8 +229,8 @@ describe('MessagePortMultiplexer', () => { const handleRead = vi.fn(); multiplexer.addChannel( '1', - (value: unknown): value is number => typeof value === 'number', handleRead, + (value: unknown): value is number => typeof value === 'number', ); const drainP = multiplexer.drainAll(); diff --git a/packages/streams/src/PostMessageStream.test.ts b/packages/streams/src/PostMessageStream.test.ts index d3dce1a20..bcad53c33 100644 --- a/packages/streams/src/PostMessageStream.test.ts +++ b/packages/streams/src/PostMessageStream.test.ts @@ -255,8 +255,8 @@ describe('PostMessageMultiplexer', () => { const handleRead = vi.fn(); multiplexer.addChannel( '1', - (value: unknown): value is number => typeof value === 'number', handleRead, + (value: unknown): value is number => typeof value === 'number', ); const drainP = multiplexer.drainAll(); diff --git a/packages/streams/src/StreamMultiplexer.test.ts b/packages/streams/src/StreamMultiplexer.test.ts index 2e739d734..6bb97e18c 100644 --- a/packages/streams/src/StreamMultiplexer.test.ts +++ b/packages/streams/src/StreamMultiplexer.test.ts @@ -33,26 +33,26 @@ describe('StreamMultiplexer', () => { describe('addChannel', () => { it('makes and adds channels', async () => { const [multiplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); expect(ch1[Symbol.asyncIterator]()).toBe(ch1); expect(ch2[Symbol.asyncIterator]()).toBe(ch2); }); it('throws if adding a channel with the same name multiple times', async () => { const [multiplex] = await TestMultiplexer.make(); - multiplex.addChannel('1', isString, noop); - expect(() => multiplex.addChannel('1', isString, noop)).toThrow( + multiplex.addChannel('1', noop, isString); + expect(() => multiplex.addChannel('1', noop, isString)).toThrow( 'Channel "1" already exists', ); }); it('throws if adding channels after starting', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); - multiplex.addChannel('1', isString, noop); + multiplex.addChannel('1', noop, isString); const startP = multiplex.start(); - expect(() => multiplex.addChannel('2', isNumber, noop)).toThrow( + expect(() => multiplex.addChannel('2', noop, isNumber)).toThrow( 'Channels must be added before starting the multiplexer', ); @@ -62,11 +62,11 @@ describe('StreamMultiplexer', () => { it('throws if adding channels after ending', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); // Add one channel so we can start the multiplexer. - multiplex.addChannel('1', isString, noop); + multiplex.addChannel('1', noop, isString); await Promise.all([multiplex.drainAll(), duplex.return()]); - expect(() => multiplex.addChannel('2', isNumber, noop)).toThrow( + expect(() => multiplex.addChannel('2', noop, isNumber)).toThrow( 'Channels must be added before starting', ); }); @@ -76,7 +76,7 @@ describe('StreamMultiplexer', () => { it('is idempotent', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); // Add one channel so we can start the multiplexer. - multiplex.addChannel('1', isString, noop); + multiplex.addChannel('1', noop, isString); const startP = Promise.all([multiplex.start(), multiplex.start()]).then( () => undefined, ); @@ -88,8 +88,8 @@ describe('StreamMultiplexer', () => { const [multiplex, duplex] = await TestMultiplexer.make(); const ch1Handler = vi.fn(); const ch2Handler = vi.fn(); - const ch1 = multiplex.addChannel('1', isString, ch1Handler); - const ch2 = multiplex.addChannel('2', isNumber, ch2Handler); + const ch1 = multiplex.addChannel('1', ch1Handler, isString); + const ch2 = multiplex.addChannel('2', ch2Handler, isNumber); const startAndDrainP = Promise.all([ multiplex.start(), @@ -124,8 +124,8 @@ describe('StreamMultiplexer', () => { const [multiplex, duplex] = await TestMultiplexer.make(); const ch1Handler = vi.fn(); const ch2Handler = vi.fn(); - multiplex.addChannel('1', isString, ch1Handler); - multiplex.addChannel('2', isNumber, ch2Handler); + multiplex.addChannel('1', ch1Handler, isString); + multiplex.addChannel('2', ch2Handler, isNumber); const drainP = multiplex.drainAll(); await Promise.all([ @@ -144,8 +144,8 @@ describe('StreamMultiplexer', () => { it('ends all streams when the duplex stream returns', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); const drainP = multiplex.drainAll(); await duplex.return(); @@ -158,8 +158,8 @@ describe('StreamMultiplexer', () => { it('ends all streams when any channel returns', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); const drainP = multiplex.drainAll(); await ch1.return(); @@ -175,8 +175,8 @@ describe('StreamMultiplexer', () => { const [multiplex] = await TestMultiplexer.make( await TestDuplexStream.make(onDispatch), ); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); onDispatch.mockImplementationOnce(() => { throw new Error('foo'); }); @@ -196,8 +196,8 @@ describe('StreamMultiplexer', () => { it('ends all streams when a channel throws', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); const drainP = multiplex.drainAll(); @@ -212,8 +212,8 @@ describe('StreamMultiplexer', () => { it('ends all streams when receiving a message for a non-existent channel', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); const drainP = multiplex.drainAll(); @@ -236,8 +236,8 @@ describe('StreamMultiplexer', () => { MultiplexEnvelope >(dispatch); const [multiplex] = await TestMultiplexer.make(duplex); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); await ch1.write('foo'); await ch2.write(42); @@ -254,8 +254,8 @@ describe('StreamMultiplexer', () => { it('returns done results from channel writes after ending', async () => { const [multiplex, duplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); await Promise.all([multiplex.drainAll(), duplex.return()]); @@ -267,8 +267,8 @@ describe('StreamMultiplexer', () => { describe('return', () => { it('ends the multiplexer and its channels', async () => { const [multiplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); await multiplex.return(); @@ -278,8 +278,8 @@ describe('StreamMultiplexer', () => { it('is idempotent', async () => { const [multiplex] = await TestMultiplexer.make(); - const ch1 = multiplex.addChannel('1', isString, noop); - const ch2 = multiplex.addChannel('2', isNumber, noop); + const ch1 = multiplex.addChannel('1', noop, isString); + const ch2 = multiplex.addChannel('2', noop, isNumber); await multiplex.return(); diff --git a/packages/streams/src/StreamMultiplexer.ts b/packages/streams/src/StreamMultiplexer.ts index c643ada55..767b8cf29 100644 --- a/packages/streams/src/StreamMultiplexer.ts +++ b/packages/streams/src/StreamMultiplexer.ts @@ -199,14 +199,14 @@ export class StreamMultiplexer { * multiplexer. * * @param channelName - The channel name. Must be unique. - * @param validateInput - The channel's input validator. * @param handleRead - The channel's drain handler. + * @param validateInput - The channel's input validator. * @returns The channel stream. */ addChannel( channelName: ChannelName, - validateInput: ValidateInput, handleRead: HandleRead, + validateInput?: ValidateInput, ): HandledDuplexStream { if (this.#status !== MultiplexerStatus.Idle) { throw new Error('Channels must be added before starting the multiplexer'); @@ -217,8 +217,8 @@ export class StreamMultiplexer { const { stream, receiveInput } = this.#makeChannel( channelName, - validateInput, handleRead, + validateInput, ); // We downcast some properties in order to store all records in one place. @@ -237,14 +237,14 @@ export class StreamMultiplexer { * write method that forwards messages to the underlying duplex stream. * * @param channelName - The channel name. Must be unique. - * @param validateInput - The channel's input validator. * @param handleRead - The channel's drain handler. + * @param validateInput - The channel's input validator. * @returns The channel stream and its `receiveInput` method. */ #makeChannel( channelName: ChannelName, - validateInput: ValidateInput, handleRead: HandleRead, + validateInput?: ValidateInput, ): { stream: HandledDuplexStream; receiveInput: ReceiveInput;