From 6139482a56539c58aa43849a0d86f0eab8cb6906 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 19 Sep 2024 17:16:01 +0100 Subject: [PATCH 01/19] add kernel package --- packages/extension/package.json | 1 + packages/extension/src/offscreen.ts | 26 ++-- packages/kernel/.eslintrc.cjs | 23 ++++ packages/kernel/CHANGELOG.md | 10 ++ packages/kernel/README.md | 7 ++ packages/kernel/package.json | 76 ++++++++++++ packages/kernel/src/VatBase.ts | 149 +++++++++++++++++++++++ packages/kernel/src/VatIframe.ts | 30 +++++ packages/kernel/src/VatManager.ts | 93 ++++++++++++++ packages/kernel/src/index.ts | 2 + packages/kernel/src/type-guards.ts | 18 +++ packages/kernel/src/types.ts | 67 ++++++++++ packages/kernel/src/utils/getHtmlId.ts | 9 ++ packages/kernel/src/utils/makeCounter.ts | 13 ++ packages/kernel/tsconfig.build.json | 11 ++ packages/kernel/tsconfig.json | 12 ++ packages/kernel/typedoc.json | 7 ++ packages/kernel/vitest.config.ts | 20 +++ yarn.lock | 39 ++++++ 19 files changed, 603 insertions(+), 10 deletions(-) create mode 100644 packages/kernel/.eslintrc.cjs create mode 100644 packages/kernel/CHANGELOG.md create mode 100644 packages/kernel/README.md create mode 100644 packages/kernel/package.json create mode 100644 packages/kernel/src/VatBase.ts create mode 100644 packages/kernel/src/VatIframe.ts create mode 100644 packages/kernel/src/VatManager.ts create mode 100644 packages/kernel/src/index.ts create mode 100644 packages/kernel/src/type-guards.ts create mode 100644 packages/kernel/src/types.ts create mode 100644 packages/kernel/src/utils/getHtmlId.ts create mode 100644 packages/kernel/src/utils/makeCounter.ts create mode 100644 packages/kernel/tsconfig.build.json create mode 100644 packages/kernel/tsconfig.json create mode 100644 packages/kernel/typedoc.json create mode 100644 packages/kernel/vitest.config.ts diff --git a/packages/extension/package.json b/packages/extension/package.json index 5df46d844..bb730eb3b 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -39,6 +39,7 @@ "@endo/promise-kit": "^1.1.4", "@metamask/snaps-utils": "^7.8.0", "@metamask/utils": "^9.1.0", + "@ocap/kernel": "workspace:^", "@ocap/shims": "workspace:^", "@ocap/streams": "workspace:^", "ses": "^1.7.0" diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index c379f3c85..cfbed112f 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,4 +1,7 @@ -import { IframeManager } from './iframe-manager.js'; +import { createWindow } from '@metamask/snaps-utils'; +import { initializeMessageChannel } from '@ocap/streams'; +import { VatManager, VatIframe } from '@ocap/kernel'; + import type { ExtensionMessage } from './message.js'; import { Command, ExtensionMessageTarget } from './message.js'; import { makeHandledCallback } from './shared.js'; @@ -9,12 +12,15 @@ main().catch(console.error); * The main function for the offscreen script. */ async function main(): Promise { - // Hard-code a single iframe for now. - const IFRAME_ID = 'default'; - const iframeManager = new IframeManager(); - const iframeReadyP = iframeManager - .create({ id: IFRAME_ID }) - .then(async () => iframeManager.makeCapTp(IFRAME_ID)); + const kernel = new VatManager(); + + const vatIframe = new VatIframe({ id: 'default' }); + const newWindow = await createWindow('iframe.html', vatIframe.iframeId); + const port = await initializeMessageChannel(newWindow); + await vatIframe.init(port); + const iframeReadyP = await vatIframe.makeCapTp(); + + kernel.addVat(vatIframe); // Handle messages from the background service worker chrome.runtime.onMessage.addListener( @@ -33,12 +39,12 @@ async function main(): Promise { await reply(Command.Evaluate, await evaluate(message.data)); break; case Command.CapTpCall: { - const result = await iframeManager.callCapTp(IFRAME_ID, message.data); + const result = await vatIframe.callCapTp(message.data); await reply(Command.CapTpCall, JSON.stringify(result, null, 2)); break; } case Command.CapTpInit: - await iframeManager.makeCapTp(IFRAME_ID); + await vatIframe.makeCapTp(); await reply(Command.CapTpInit, '~~~ CapTP Initialized ~~~'); break; case Command.Ping: @@ -76,7 +82,7 @@ async function main(): Promise { */ async function evaluate(source: string): Promise { try { - const result = await iframeManager.sendMessage(IFRAME_ID, { + const result = await kernel.sendMessage(vatIframe.id, { type: Command.Evaluate, data: source, }); diff --git a/packages/kernel/.eslintrc.cjs b/packages/kernel/.eslintrc.cjs new file mode 100644 index 000000000..c747e0b88 --- /dev/null +++ b/packages/kernel/.eslintrc.cjs @@ -0,0 +1,23 @@ +module.exports = { + extends: ['../../.eslintrc.cjs'], + + overrides: [ + { + files: ['src/**/*.ts'], + globals: { + chrome: 'readonly', + clients: 'readonly', + Compartment: 'readonly', + }, + }, + + { + files: ['vite.config.ts'], + parserOptions: { + sourceType: 'module', + tsconfigRootDir: __dirname, + project: ['./tsconfig.scripts.json'], + }, + }, + ], +}; diff --git a/packages/kernel/CHANGELOG.md b/packages/kernel/CHANGELOG.md new file mode 100644 index 000000000..0c82cb1ed --- /dev/null +++ b/packages/kernel/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +[Unreleased]: https://github.com/MetaMask/ocap-kernel/ diff --git a/packages/kernel/README.md b/packages/kernel/README.md new file mode 100644 index 000000000..fd1a5cbfc --- /dev/null +++ b/packages/kernel/README.md @@ -0,0 +1,7 @@ +# `@ocap/kernel` + +OCap kernel core components + +## Contributing + +This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/ocap-kernel#readme). diff --git a/packages/kernel/package.json b/packages/kernel/package.json new file mode 100644 index 000000000..8d3b009fb --- /dev/null +++ b/packages/kernel/package.json @@ -0,0 +1,76 @@ +{ + "name": "@ocap/kernel", + "version": "0.0.0", + "private": true, + "description": "OCap kernel core components", + "repository": { + "type": "git", + "url": "https://github.com/MetaMask/ocap-kernel.git" + }, + "sideEffects": false, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "files": [ + "dist/" + ], + "scripts": { + "build": "node scripts/bundle.js", + "build:docs": "typedoc", + "changelog:validate": "../../scripts/validate-changelog.sh @ocap/kernel", + "clean": "rimraf --glob ./dist './*.tsbuildinfo'", + "lint": "yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies", + "lint:dependencies": "depcheck", + "lint:eslint": "eslint . --cache --ext js,mjs,cjs,ts,mts,cts", + "lint:fix": "yarn constraints --fix && yarn lint:eslint --fix && yarn lint:misc --write", + "lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore", + "publish:preview": "yarn npm publish --tag preview", + "test": "vitest run --config vitest.config.ts", + "test:clean": "yarn test --no-cache --coverage.clean", + "test:dev": "yarn test --coverage false", + "test:verbose": "yarn test --reporter verbose", + "test:watch": "vitest --config vitest.config.ts" + }, + "dependencies": { + "@endo/captp": "^4.2.2", + "@endo/eventual-send": "^1.2.4", + "@endo/exo": "^1.5.2", + "@endo/patterns": "^1.4.2", + "@endo/promise-kit": "^1.1.5", + "@metamask/snaps-utils": "^7.8.0", + "@metamask/utils": "^9.1.0", + "@ocap/shims": "workspace:^", + "@ocap/streams": "workspace:^", + "ses": "^1.7.0" + }, + "devDependencies": { + "@endo/bundle-source": "^3.3.0", + "@metamask/auto-changelog": "^3.4.4", + "@metamask/eslint-config": "^13.0.0", + "@metamask/eslint-config-nodejs": "^13.0.0", + "@metamask/eslint-config-typescript": "^13.0.0", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", + "depcheck": "^1.4.7", + "eslint": "^8.57.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import-x": "^0.5.1", + "eslint-plugin-jsdoc": "^47.0.2", + "eslint-plugin-n": "^16.6.2", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-vitest": "^0.4.1", + "prettier": "^2.7.1", + "rimraf": "^6.0.1", + "typescript": "~5.5.4", + "vite": "^5.3.5", + "vitest": "^2.0.5" + }, + "engines": { + "node": "^18.18 || >=20" + } +} diff --git a/packages/kernel/src/VatBase.ts b/packages/kernel/src/VatBase.ts new file mode 100644 index 000000000..18a0c8c5b --- /dev/null +++ b/packages/kernel/src/VatBase.ts @@ -0,0 +1,149 @@ +import { makeCapTP } from '@endo/captp'; +import { E } from '@endo/eventual-send'; +import { makePromiseKit } from '@endo/promise-kit'; +import type { StreamPair } from '@ocap/streams'; +import { makeMessagePortStreamPair } from '@ocap/streams'; + +import type { + CapTpMessage, + CapTpPayload, + MessageId, + UnresolvedMessages, + VatMessage, +} from './types.ts'; +import { Command } from './types.ts'; +import { makeCounter } from './utils/makeCounter.ts'; + +export type VatBaseProps = { + id: string; +}; + +export abstract class VatBase { + readonly id: string; + + readonly #messageCounter: () => number; + + readonly unresolvedMessages: UnresolvedMessages = new Map(); + + streams: StreamPair; + + streamEnvelopeHandler: StreamEnvelopeHandler; + + capTp?: ReturnType; + + constructor({ id }: VatBaseProps) { + this.id = id; + this.#messageCounter = makeCounter(); + } + + /** + * Initializes the vat. + * + * @param port - The message port to use for communication. + * @returns A promise that resolves when the vat is initialized. + */ + async init(port: MessagePort): Promise { + this.streams = makeMessagePortStreamPair(port); + this.streamEnvelopeHandler = makeStreamEnvelopeHandler( + { + command: async ({ id, message }) => { + const promiseCallbacks = this.unresolvedMessages.get(id); + if (promiseCallbacks === undefined) { + console.error(`No unresolved message with id "${id}".`); + } else { + this.unresolvedMessages.delete(id); + promiseCallbacks.resolve(message.data); + } + }, + }, + console.warn, + ); + + await this.sendMessage({ type: Command.Ping, data: null }); + console.debug(`Created vat with id "${this.id}"`); + } + + /** + * Make a CapTP connection. + * + * @returns A promise that resolves when the CapTP connection is made. + */ + async makeCapTp(): Promise { + if (!this.capTp) { + throw new Error( + `Vat with id "${this.id}" already has a CapTP connection.`, + ); + } + + // Handle writes here. #receiveMessages() handles reads. + const { writer } = this.streams; + // https://github.com/endojs/endo/issues/2412 + // eslint-disable-next-line @typescript-eslint/no-misused-promises + const ctp = makeCapTP(this.id, async (content: unknown) => { + console.log('CapTP to vat', JSON.stringify(content, null, 2)); + await writer.next(wrapCapTp(content as CapTpMessage)); + }); + + this.capTp = ctp; + this.streamEnvelopeHandler.contentHandlers.capTp = async ( + content: string, + ) => { + console.log('CapTP from vat', JSON.stringify(content, null, 2)); + ctp.dispatch(content); + }; + + return this.sendMessage({ type: Command.CapTpInit, data: null }); + } + + /** + * Call a CapTP method. + * + * @param payload - The CapTP payload. + * @returns A promise that resolves the result of the CapTP call. + */ + async callCapTp(payload: CapTpPayload): Promise { + if (!this.capTp) { + throw new Error( + `Vat with id "${this.id}" does not have a CapTP connection.`, + ); + } + return E(this.capTp.getBootstrap())[payload.method](...payload.params); + } + + /** + * Terminates the vat. + */ + terminate(): void { + this.streams.return(); + + // Handle orphaned messages + for (const [messageId, promiseCallback] of this.unresolvedMessages) { + promiseCallback?.reject(new Error('Vat was deleted')); + this.unresolvedMessages.delete(messageId); + } + } + + /** + * Send a message to a vat. + * + * @param message - The message to send. + * @returns A promise that resolves the response to the message. + */ + public async sendMessage(message: VatMessage): Promise { + const { promise, reject, resolve } = makePromiseKit(); + const messageId = this.#nextMessageId(); + this.unresolvedMessages.set(messageId, { reject, resolve }); + await this.streams.writer.next(wrapCommand({ id: messageId, message })); + return promise; + } + + /** + * Gets the next message ID. + * + * @param id - The vat ID. + * @returns The message ID. + */ + readonly #nextMessageId = (): MessageId => { + return `${this.id}-${this.#messageCounter()}`; + }; +} diff --git a/packages/kernel/src/VatIframe.ts b/packages/kernel/src/VatIframe.ts new file mode 100644 index 000000000..12b339e27 --- /dev/null +++ b/packages/kernel/src/VatIframe.ts @@ -0,0 +1,30 @@ +import { getHtmlId } from './utils/getHtmlId.ts'; +import type { VatBaseProps } from './VatBase.ts'; +import { VatBase } from './VatBase.ts'; + +export class VatIframe extends VatBase { + readonly iframeId: string; + + constructor({ id }: VatBaseProps) { + super({ id }); + + this.iframeId = getHtmlId(id); + } + + /** + * Terminates the vat. + */ + terminate(): void { + super.terminate(); + + const iframe = document.getElementById(this.iframeId); + /* v8 ignore next 6: Not known to be possible. */ + if (iframe === null) { + console.error( + `iframe of vat with id "${this.id}" already removed from DOM`, + ); + return; + } + iframe.remove(); + } +} diff --git a/packages/kernel/src/VatManager.ts b/packages/kernel/src/VatManager.ts new file mode 100644 index 000000000..ac67535e5 --- /dev/null +++ b/packages/kernel/src/VatManager.ts @@ -0,0 +1,93 @@ +import '@ocap/shims/endoify'; +import type { VatId, VatMessage } from './types.ts'; +import type { VatIframe } from './VatIframe.ts'; + +export class VatManager { + readonly #vats: Map; + + constructor() { + this.#vats = new Map(); + } + + /** + * Gets the vat IDs in the kernel. + * + * @returns An array of vat IDs. + */ + public getVatIDs(): VatId[] { + return Array.from(this.#vats.keys()); + } + + /** + * Adds a vat to the kernel. + * + * @param vat - The vat record. + */ + public addVat(vat: VatIframe): void { + if (this.#vats.has(vat.id)) { + throw new Error(`Vat with ID ${vat.id} already exists.`); + } + this.#vats.set(vat.id, vat); + + /* v8 ignore next 4: Not known to be possible. */ + this.#receiveMessages(vat.id, vat.streams.reader).catch((error) => { + console.error(`Unexpected read error from vat "${vat.id}"`, error); + this.deleteVat(vat.id); + }); + } + + /** + * Deletes a vat from the kernel. + * + * @param id - The ID of the vat. + */ + public deleteVat(id: string): void { + const vat = this.#vats.get(id); + vat?.terminate(); + this.#vats.delete(id); + } + + /** + * Send a message to a vat. + * + * @param id - The id of the vat to send the message to. + * @param message - The message to send. + * @returns A promise that resolves the response to the message. + */ + public async sendMessage(id: VatId, message: VatMessage): Promise { + const vat = this.#getVat(id); + return vat.sendMessage(message); + } + + /** + * Receives messages from a vat. + * + * @param vatId - The ID of the vat. + * @param reader - The reader for the messages. + */ + async #receiveMessages( + vatId: VatId, + reader: Reader, + ): Promise { + const vat = this.#getVat(vatId); + + for await (const rawMessage of reader) { + console.debug('Offscreen received message', rawMessage); + await vat.streamEnvelopeHandler.handle(rawMessage); + } + } + + /** + * Gets a vat from the kernel. + * + * @param id - The ID of the vat. + * @returns The vat record. + */ + #getVat(id: string): VatIframe { + const vat = this.#vats.get(id); + if (vat === undefined) { + throw new Error(`Vat with ID ${id} does not exist.`); + } + return vat; + } +} diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts new file mode 100644 index 000000000..ca1b5f323 --- /dev/null +++ b/packages/kernel/src/index.ts @@ -0,0 +1,2 @@ +export { VatManager } from './VatManager.ts'; +export { VatIframe } from './VatIframe.ts'; diff --git a/packages/kernel/src/type-guards.ts b/packages/kernel/src/type-guards.ts new file mode 100644 index 000000000..10b5a538b --- /dev/null +++ b/packages/kernel/src/type-guards.ts @@ -0,0 +1,18 @@ +import { isObject } from '@metamask/utils'; + +import type { CapTpMessage, WrappedVatMessage } from './types.ts'; + +export const isWrappedVatMessage = ( + value: unknown, +): value is WrappedVatMessage => + isObject(value) && + typeof value.id === 'string' && + isObject(value.message) && + typeof value.message.type === 'string' && + (typeof value.message.data === 'string' || value.message.data === null); + +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/types.ts b/packages/kernel/src/types.ts new file mode 100644 index 000000000..e10d9a232 --- /dev/null +++ b/packages/kernel/src/types.ts @@ -0,0 +1,67 @@ +import type { Primitive } from '@endo/captp'; +import type { PromiseKit } from '@endo/promise-kit'; + +export type VatId = string; + +export type MessageId = string; + +export enum KernelMessageTarget { + Background = 'background', + Offscreen = 'offscreen', + WebWorker = 'webWorker', + Node = 'node', +} + +export type PromiseCallbacks = Omit, 'promise'>; + +export type UnresolvedMessages = Map; + +export type GetPort = (targetWindow: Window) => Promise; + +export type DataObject = + | Primitive + | Promise + | DataObject[] + | { [key: string]: DataObject }; + +type CommandLike< + CommandType extends Command, + Data extends DataObject, + TargetType extends KernelMessageTarget, +> = { + type: CommandType; + target?: TargetType; + data: Data; +}; + +export enum Command { + CapTpCall = 'callCapTp', + CapTpInit = 'makeCapTp', + Evaluate = 'evaluate', + Ping = 'ping', +} + +export type CapTpPayload = { + method: string; + params: DataObject[]; +}; + +type CommandMessage = + | CommandLike + | CommandLike + | CommandLike + | CommandLike; + +export type KernelMessage = CommandMessage; +export type VatMessage = CommandMessage; + +export type WrappedVatMessage = { + id: MessageId; + message: VatMessage; +}; + +export type CapTpMessage = { + type: Type; + epoch: number; + [key: string]: unknown; +}; diff --git a/packages/kernel/src/utils/getHtmlId.ts b/packages/kernel/src/utils/getHtmlId.ts new file mode 100644 index 000000000..7a20c8249 --- /dev/null +++ b/packages/kernel/src/utils/getHtmlId.ts @@ -0,0 +1,9 @@ +import type { VatId } from 'src/types.ts'; + +/** + * Get a DOM id for our iframes, for greater collision resistance. + * + * @param id - The vat id to base the DOM id on. + * @returns The DOM id. + */ +export const getHtmlId = (id: VatId): string => `ocap-iframe-${id}`; diff --git a/packages/kernel/src/utils/makeCounter.ts b/packages/kernel/src/utils/makeCounter.ts new file mode 100644 index 000000000..0a74c3737 --- /dev/null +++ b/packages/kernel/src/utils/makeCounter.ts @@ -0,0 +1,13 @@ +/** + * A simple counter which increments and returns when called. + * + * @param start - One less than the first returned number. + * @returns A counter. + */ +export const makeCounter = (start: number = 0) => { + let counter: number = start; + return () => { + counter += 1; + return counter; + }; +}; diff --git a/packages/kernel/tsconfig.build.json b/packages/kernel/tsconfig.build.json new file mode 100644 index 000000000..2a475d693 --- /dev/null +++ b/packages/kernel/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.packages.build.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src", + "types": ["chrome", "ses"] + }, + "references": [], + "include": ["./src"] +} diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json new file mode 100644 index 000000000..3c8627035 --- /dev/null +++ b/packages/kernel/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.packages.json", + "compilerOptions": { + "baseUrl": "./", + "allowImportingTsExtensions": true, + "emitDeclarationOnly": true, + "allowJs": true, + "checkJs": true + }, + "references": [], + "include": ["./src"] +} diff --git a/packages/kernel/typedoc.json b/packages/kernel/typedoc.json new file mode 100644 index 000000000..c9da015db --- /dev/null +++ b/packages/kernel/typedoc.json @@ -0,0 +1,7 @@ +{ + "entryPoints": ["./src/index.ts"], + "excludePrivate": true, + "hideGenerator": true, + "out": "docs", + "tsconfig": "./tsconfig.build.json" +} diff --git a/packages/kernel/vitest.config.ts b/packages/kernel/vitest.config.ts new file mode 100644 index 000000000..6b2328fbd --- /dev/null +++ b/packages/kernel/vitest.config.ts @@ -0,0 +1,20 @@ +// eslint-disable-next-line spaced-comment +/// + +import { defineConfig, mergeConfig } from 'vite'; + +import { getDefaultConfig } from '../../vitest.config.packages.js'; + +const defaultConfig = getDefaultConfig(); + +const config = mergeConfig( + defaultConfig, + defineConfig({ + test: { + pool: 'vmThreads', + }, + }), +); + +delete config.test.coverage.thresholds; +export default config; diff --git a/yarn.lock b/yarn.lock index 15519ea65..0ffecc01c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1379,6 +1379,7 @@ __metadata: "@metamask/eslint-config-typescript": "npm:^13.0.0" "@metamask/snaps-utils": "npm:^7.8.0" "@metamask/utils": "npm:^9.1.0" + "@ocap/kernel": "workspace:^" "@ocap/shims": "workspace:^" "@ocap/streams": "workspace:^" "@ocap/test-utils": "workspace:^" @@ -1408,6 +1409,44 @@ __metadata: languageName: unknown linkType: soft +"@ocap/kernel@workspace:^, @ocap/kernel@workspace:packages/kernel": + version: 0.0.0-use.local + resolution: "@ocap/kernel@workspace:packages/kernel" + dependencies: + "@endo/bundle-source": "npm:^3.3.0" + "@endo/captp": "npm:^4.2.2" + "@endo/eventual-send": "npm:^1.2.4" + "@endo/exo": "npm:^1.5.2" + "@endo/patterns": "npm:^1.4.2" + "@endo/promise-kit": "npm:^1.1.5" + "@metamask/auto-changelog": "npm:^3.4.4" + "@metamask/eslint-config": "npm:^13.0.0" + "@metamask/eslint-config-nodejs": "npm:^13.0.0" + "@metamask/eslint-config-typescript": "npm:^13.0.0" + "@metamask/snaps-utils": "npm:^7.8.0" + "@metamask/utils": "npm:^9.1.0" + "@ocap/shims": "workspace:^" + "@ocap/streams": "workspace:^" + "@typescript-eslint/eslint-plugin": "npm:^8.1.0" + "@typescript-eslint/parser": "npm:^8.1.0" + depcheck: "npm:^1.4.7" + eslint: "npm:^8.57.0" + eslint-config-prettier: "npm:^8.8.0" + eslint-plugin-import-x: "npm:^0.5.1" + eslint-plugin-jsdoc: "npm:^47.0.2" + eslint-plugin-n: "npm:^16.6.2" + eslint-plugin-prettier: "npm:^4.2.1" + eslint-plugin-promise: "npm:^6.1.1" + eslint-plugin-vitest: "npm:^0.4.1" + prettier: "npm:^2.7.1" + rimraf: "npm:^6.0.1" + ses: "npm:^1.7.0" + typescript: "npm:~5.5.4" + vite: "npm:^5.3.5" + vitest: "npm:^2.0.5" + languageName: unknown + linkType: soft + "@ocap/monorepo@workspace:.": version: 0.0.0-use.local resolution: "@ocap/monorepo@workspace:." From aedaab6d2196e0021cc091e871b49b79ddb4629a Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 19 Sep 2024 20:56:36 +0100 Subject: [PATCH 02/19] extract all code from extension and pass realm to vat --- packages/extension/package.json | 1 - packages/extension/src/background.ts | 12 +- packages/extension/src/iframe-manager.test.ts | 543 ------------------ packages/extension/src/iframe-manager.ts | 232 -------- packages/extension/src/iframe.ts | 26 +- packages/extension/src/makeIframeVatRealm.ts | 39 ++ packages/extension/src/message.test.ts | 26 - packages/extension/src/offscreen.ts | 41 +- packages/extension/src/shared.test.ts | 23 +- packages/extension/src/shared.ts | 14 - packages/kernel/package.json | 14 +- .../kernel/src/{VatManager.ts => Kernel.ts} | 42 +- packages/kernel/src/{VatBase.ts => Vat.ts} | 92 ++- packages/kernel/src/VatIframe.ts | 30 - packages/kernel/src/index.ts | 5 +- packages/kernel/src/types.ts | 64 +-- packages/kernel/src/utils/getHtmlId.ts | 9 - packages/kernel/src/utils/makeCounter.test.ts | 24 + packages/kernel/tsconfig.build.json | 3 +- packages/kernel/tsconfig.json | 2 +- packages/streams/package.json | 4 +- packages/streams/src/index.ts | 29 +- .../src/stream-envelope.test.ts | 16 +- .../src/stream-envelope.ts | 15 +- .../{kernel => streams}/src/type-guards.ts | 2 +- .../src/message.ts => streams/src/types.ts} | 42 +- packages/streams/tsconfig.json | 6 +- yarn.lock | 12 +- 28 files changed, 257 insertions(+), 1111 deletions(-) delete mode 100644 packages/extension/src/iframe-manager.test.ts delete mode 100644 packages/extension/src/iframe-manager.ts create mode 100644 packages/extension/src/makeIframeVatRealm.ts delete mode 100644 packages/extension/src/message.test.ts rename packages/kernel/src/{VatManager.ts => Kernel.ts} (55%) rename packages/kernel/src/{VatBase.ts => Vat.ts} (65%) delete mode 100644 packages/kernel/src/VatIframe.ts delete mode 100644 packages/kernel/src/utils/getHtmlId.ts create mode 100644 packages/kernel/src/utils/makeCounter.test.ts rename packages/{extension => streams}/src/stream-envelope.test.ts (84%) rename packages/{extension => streams}/src/stream-envelope.ts (74%) rename packages/{kernel => streams}/src/type-guards.ts (89%) rename packages/{extension/src/message.ts => streams/src/types.ts} (51%) diff --git a/packages/extension/package.json b/packages/extension/package.json index bb730eb3b..7bfe5ec19 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -36,7 +36,6 @@ "@endo/eventual-send": "^1.2.4", "@endo/exo": "^1.5.2", "@endo/patterns": "^1.4.2", - "@endo/promise-kit": "^1.1.4", "@metamask/snaps-utils": "^7.8.0", "@metamask/utils": "^9.1.0", "@ocap/kernel": "workspace:^", diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index 3182291dc..2e2704b34 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -1,8 +1,8 @@ import type { Json } from '@metamask/utils'; - import './background-trusted-prelude.js'; -import type { ExtensionMessage } from './message.js'; -import { Command, ExtensionMessageTarget } from './message.js'; +import type { KernelMessage } from '@ocap/streams'; +import { Command, KernelMessageTarget } from '@ocap/streams'; + import { makeHandledCallback } from './shared.js'; // globalThis.kernel will exist due to dev-console.js in background-trusted-prelude.js @@ -45,7 +45,7 @@ async function sendMessage(type: string, data?: Json): Promise { await chrome.runtime.sendMessage({ type, - target: ExtensionMessageTarget.Offscreen, + target: KernelMessageTarget.Offscreen, data: data ?? null, }); } @@ -65,8 +65,8 @@ async function provideOffScreenDocument(): Promise { // Handle replies from the offscreen document chrome.runtime.onMessage.addListener( - makeHandledCallback(async (message: ExtensionMessage) => { - if (message.target !== ExtensionMessageTarget.Background) { + makeHandledCallback(async (message: KernelMessage) => { + if (message.target !== KernelMessageTarget.Background) { console.warn( `Background received message with unexpected target: "${message.target}"`, ); diff --git a/packages/extension/src/iframe-manager.test.ts b/packages/extension/src/iframe-manager.test.ts deleted file mode 100644 index d580f6b22..000000000 --- a/packages/extension/src/iframe-manager.test.ts +++ /dev/null @@ -1,543 +0,0 @@ -import './endoify.js'; -import * as snapsUtils from '@metamask/snaps-utils'; -import { delay, makePromiseKitMock } from '@ocap/test-utils'; -import { vi, describe, it, expect } from 'vitest'; - -import { IframeManager } from './iframe-manager.js'; -import type { IframeMessage } from './message.js'; -import { Command } from './message.js'; -import { wrapCommand, wrapCapTp } from './stream-envelope.js'; - -vi.mock('@endo/promise-kit', () => makePromiseKitMock()); - -vi.mock('@metamask/snaps-utils', () => ({ - createWindow: vi.fn(), -})); - -describe('IframeManager', () => { - const makeGetPort = - (port: MessagePort = new MessageChannel().port1) => - async (_window: Window): Promise => - Promise.resolve(port); - - describe('create', () => { - it('creates a new iframe', async () => { - const mockWindow = {}; - vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce( - mockWindow as Window, - ); - const manager = new IframeManager(); - const sendMessageSpy = vi - .spyOn(manager, 'sendMessage') - .mockImplementation(vi.fn()); - const [newWindow, id] = await manager.create({ getPort: makeGetPort() }); - - expect(newWindow).toBe(mockWindow); - expect(id).toBeTypeOf('string'); - expect(sendMessageSpy).toHaveBeenCalledOnce(); - expect(sendMessageSpy).toHaveBeenCalledWith(id, { - type: Command.Ping, - data: null, - }); - }); - - it('creates a new iframe with a specified id', async () => { - const mockWindow = {}; - vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce( - mockWindow as Window, - ); - - const manager = new IframeManager(); - const sendMessageSpy = vi - .spyOn(manager, 'sendMessage') - .mockImplementation(vi.fn()); - const id = 'foo'; - const [newWindow, returnedId] = await manager.create({ - id, - getPort: makeGetPort(), - }); - - expect(newWindow).toBe(mockWindow); - expect(returnedId).toBe(id); - expect(sendMessageSpy).toHaveBeenCalledOnce(); - expect(sendMessageSpy).toHaveBeenCalledWith(id, { - type: Command.Ping, - data: null, - }); - }); - - it('creates a new iframe with the default getPort function', async () => { - vi.resetModules(); - vi.doMock('@ocap/streams', async (importOriginal) => { - // @ts-expect-error This import is known to exist, and the linter erases the appropriate assertion. - const { makeStreamEnvelopeKit } = await importOriginal(); - return { - initializeMessageChannel: vi.fn(), - makeMessagePortStreamPair: vi.fn(() => ({ reader: {}, writer: {} })), - MessagePortReader: class Mock1 {}, - MessagePortWriter: class Mock2 {}, - makeStreamEnvelopeKit, - }; - }); - const IframeManager2 = (await import('./iframe-manager.js')) - .IframeManager; - - const mockWindow = {}; - vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce( - mockWindow as Window, - ); - const manager = new IframeManager2(); - const sendMessageSpy = vi - .spyOn(manager, 'sendMessage') - .mockImplementation(vi.fn()); - const [newWindow, id] = await manager.create(); - - expect(newWindow).toBe(mockWindow); - expect(id).toBeTypeOf('string'); - expect(sendMessageSpy).toHaveBeenCalledOnce(); - expect(sendMessageSpy).toHaveBeenCalledWith(id, { - type: Command.Ping, - data: null, - }); - }); - }); - - describe('delete', () => { - it('deletes an iframe', async () => { - const id = 'foo'; - const iframe = document.createElement('iframe'); - iframe.id = `ocap-iframe-${id}`; - const removeSpy = vi.spyOn(iframe, 'remove'); - - vi.mocked(snapsUtils.createWindow).mockImplementationOnce(async () => { - document.body.appendChild(iframe); - return iframe.contentWindow as Window; - }); - - const manager = new IframeManager(); - vi.spyOn(manager, 'sendMessage').mockImplementation(vi.fn()); - - await manager.create({ id, getPort: makeGetPort() }); - await manager.delete(id); - - expect(removeSpy).toHaveBeenCalledOnce(); - }); - - it('ignores attempt to delete unrecognized iframe', async () => { - const id = 'foo'; - const manager = new IframeManager(); - const iframe = document.createElement('iframe'); - - const removeSpy = vi.spyOn(iframe, 'remove'); - await manager.delete(id); - - expect(removeSpy).not.toHaveBeenCalled(); - }); - - it('rejects unresolved messages', async () => { - const id = 'foo'; - const messageCount = 7; - const awaitCount = 2; - - vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn()); - - const manager = new IframeManager(); - vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn()); - - const { port1, port2 } = new MessageChannel(); - const postMessage = (i: number): void => { - port2.postMessage({ - done: false, - value: wrapCommand({ - id: `foo-${i + 1}`, - message: { - type: Command.Evaluate, - data: `${i + 1}`, - }, - }), - }); - }; - - await manager.create({ id, getPort: makeGetPort(port1) }); - - const messagePromises = Array(messageCount) - .fill(0) - .map(async (_, i) => - manager.sendMessage(id, { type: Command.Evaluate, data: `${i}+1` }), - ); - - // resolve the first `awaitCount` promises - for (let i = 0; i < awaitCount; i++) { - postMessage(i); - await messagePromises[i]; - } - - await manager.delete(id); - - // reject the rest of the promises - for (let i = awaitCount; i < messageCount; i++) { - postMessage(i); - await expect(messagePromises[i]).rejects.toThrow('Vat was deleted'); - } - }); - }); - - describe('capTp', () => { - it('calls console.warn when receiving a capTp envelope before initialization', async () => { - const id = 'foo'; - - vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn()); - const warnSpy = vi.spyOn(console, 'warn'); - - const manager = new IframeManager(); - vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn()); - - const { port1, port2 } = new MessageChannel(); - - await manager.create({ id, getPort: makeGetPort(port1) }); - - const envelope = wrapCapTp({ - epoch: 0, - questionID: 'q-1', - type: 'CTP_BOOTSTRAP', - }); - - port2.postMessage({ - done: false, - value: envelope, - }); - - await delay(); - - expect(warnSpy).toHaveBeenCalledWith( - 'Stream envelope handler received an envelope with known but unexpected label', - envelope, - ); - }); - - it('throws if called before initialization', async () => { - const mockWindow = {}; - vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce( - mockWindow as Window, - ); - const manager = new IframeManager(); - vi.spyOn(manager, 'sendMessage').mockImplementation(vi.fn()); - const [, id] = await manager.create({ getPort: makeGetPort() }); - - await expect( - async () => - await manager.callCapTp(id, { - method: 'whatIsTheGreatFrangooly', - params: [], - }), - ).rejects.toThrow(/does not have a CapTP connection\.$/u); - }); - - it('throws if initialization is called twice on the same vat', async () => { - const id = 'frangooly'; - - const capTpInit = { - query: wrapCommand({ - id: `${id}-1`, - message: { - data: null, - type: Command.CapTpInit, - }, - }), - response: wrapCommand({ - id: `${id}-1`, - message: { - type: Command.CapTpInit, - data: null, - }, - }), - }; - - vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn()); - - const { port1, port2 } = new MessageChannel(); - const port1PostMessageSpy = vi - .spyOn(port1, 'postMessage') - .mockImplementation(vi.fn()); - - let port1PostMessageCallCounter: number = 0; - const expectSendMessageToHaveBeenCalledOnceMoreWith = ( - expectation: unknown, - ): void => { - port1PostMessageCallCounter += 1; - expect(port1PostMessageSpy).toHaveBeenCalledTimes( - port1PostMessageCallCounter, - ); - expect(port1PostMessageSpy).toHaveBeenLastCalledWith({ - done: false, - value: expectation, - }); - }; - - const mockReplyWith = (message: unknown): void => - port2.postMessage({ - done: false, - value: message, - }); - - const manager = new IframeManager(); - - vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn()); - - await manager.create({ id, getPort: makeGetPort(port1) }); - - // Init CapTP connection - const initCapTpPromise = manager.makeCapTp(id); - - expectSendMessageToHaveBeenCalledOnceMoreWith(capTpInit.query); - mockReplyWith(capTpInit.response); - - await initCapTpPromise.then((resolvedValue) => - console.debug(`CapTp initialized: ${JSON.stringify(resolvedValue)}`), - ); - - await expect(async () => await manager.makeCapTp(id)).rejects.toThrow( - /already has a CapTP connection\./u, - ); - }); - - it('does TheGreatFrangooly', async () => { - const id = 'frangooly'; - - const capTpInit = { - query: wrapCommand({ - id: `${id}-1`, - message: { - data: null, - type: Command.CapTpInit, - }, - }), - response: wrapCommand({ - id: `${id}-1`, - message: { - type: Command.CapTpInit, - data: null, - }, - }), - }; - - const greatFrangoolyBootstrap = { - query: wrapCapTp({ - epoch: 0, - questionID: 'q-1', - type: 'CTP_BOOTSTRAP', - }), - response: wrapCapTp({ - type: 'CTP_RETURN', - epoch: 0, - answerID: 'q-1', - result: { - body: '{"@qclass":"slot","iface":"Alleged: TheGreatFrangooly","index":0}', - slots: ['o+1'], - }, - }), - }; - - const greatFrangoolyCall = { - query: wrapCapTp({ - type: 'CTP_CALL', - epoch: 0, - method: { - body: '["whatIsTheGreatFrangooly",[]]', - slots: [], - }, - questionID: 'q-2', - target: 'o-1', - }), - response: wrapCapTp({ - type: 'CTP_RETURN', - epoch: 0, - answerID: 'q-2', - result: { - body: '"Crowned with Chaos"', - slots: [], - }, - }), - }; - - vi.mocked(snapsUtils.createWindow).mockImplementationOnce(vi.fn()); - - const { port1, port2 } = new MessageChannel(); - const port1PostMessageSpy = vi - .spyOn(port1, 'postMessage') - .mockImplementation(vi.fn()); - - let port1PostMessageCallCounter: number = 0; - const expectSendMessageToHaveBeenCalledOnceMoreWith = ( - expectation: unknown, - ): void => { - port1PostMessageCallCounter += 1; - expect(port1PostMessageSpy).toHaveBeenCalledTimes( - port1PostMessageCallCounter, - ); - expect(port1PostMessageSpy).toHaveBeenLastCalledWith({ - done: false, - value: expectation, - }); - }; - - const mockReplyWith = (message: unknown): void => - port2.postMessage({ - done: false, - value: message, - }); - - const manager = new IframeManager(); - - vi.spyOn(manager, 'sendMessage').mockImplementationOnce(vi.fn()); - - await manager.create({ id, getPort: makeGetPort(port1) }); - - // Init CapTP connection - const initCapTpPromise = manager.makeCapTp(id); - - expectSendMessageToHaveBeenCalledOnceMoreWith(capTpInit.query); - mockReplyWith(capTpInit.response); - - await initCapTpPromise.then((resolvedValue) => - console.debug(`CapTp initialized: ${JSON.stringify(resolvedValue)}`), - ); - - // Bootstrap TheGreatFrangooly... - const callCapTpResponse = manager.callCapTp(id, { - method: 'whatIsTheGreatFrangooly', - params: [], - }); - - expectSendMessageToHaveBeenCalledOnceMoreWith( - greatFrangoolyBootstrap.query, - ); - mockReplyWith(greatFrangoolyBootstrap.response); - - await delay().then(() => - console.debug('TheGreatFrangooly bootstrapped...'), - ); - - // ...and call it. - expectSendMessageToHaveBeenCalledOnceMoreWith(greatFrangoolyCall.query); - mockReplyWith(greatFrangoolyCall.response); - - await callCapTpResponse.then((resolvedValue) => - console.debug( - `TheGreatFrangooly called: ${JSON.stringify(resolvedValue)}`, - ), - ); - - expect(await callCapTpResponse).equals('Crowned with Chaos'); - }); - }); - - describe('sendMessage', () => { - it('sends a message to an iframe', async () => { - vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce({} as Window); - - const manager = new IframeManager(); - const sendMessageSpy = vi.spyOn(manager, 'sendMessage'); - // Intercept the ping message in create() - sendMessageSpy.mockImplementationOnce(async () => Promise.resolve()); - - const { port1, port2 } = new MessageChannel(); - const portPostMessageSpy = vi.spyOn(port1, 'postMessage'); - const id = 'foo'; - await manager.create({ id, getPort: makeGetPort(port1) }); - - const message: IframeMessage = { type: Command.Evaluate, data: '2+2' }; - const response: IframeMessage = { type: Command.Evaluate, data: '4' }; - - // sendMessage wraps the content in a Command envelope - const messagePromise = manager.sendMessage(id, message); - const messageId: string = - portPostMessageSpy.mock.lastCall?.[0]?.value?.content?.id; - expect(messageId).toBeTypeOf('string'); - - // postMessage sends the json directly, so we have to wrap it in an envelope here - port2.postMessage({ - done: false, - value: wrapCommand({ - id: messageId, - message: response, - }), - }); - - // awaiting event loop should resolve the messagePromise - expect(await messagePromise).toBe(response.data); - - // messagePromise doesn't resolve until message was posted - expect(portPostMessageSpy).toHaveBeenCalledOnce(); - expect(portPostMessageSpy).toHaveBeenCalledWith({ - done: false, - value: wrapCommand({ - id: messageId, - message, - }), - }); - }); - - it('throws if iframe not found', async () => { - const manager = new IframeManager(); - const id = 'foo'; - const message: IframeMessage = { type: Command.Ping, data: null }; - - await expect(manager.sendMessage(id, message)).rejects.toThrow( - `No vat with id "${id}"`, - ); - }); - }); - - describe('miscellaneous', () => { - it('calls console.warn when receiving unexpected message', async () => { - vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce({} as Window); - - const manager = new IframeManager(); - const warnSpy = vi.spyOn(console, 'warn'); - const sendMessageSpy = vi.spyOn(manager, 'sendMessage'); - // Intercept the ping message in create() - sendMessageSpy.mockImplementationOnce(async () => Promise.resolve()); - - const { port1, port2 } = new MessageChannel(); - await manager.create({ getPort: makeGetPort(port1) }); - - port2.postMessage({ done: false, value: 'foo' }); - await delay(10); - - expect(warnSpy).toHaveBeenCalledWith( - 'Stream envelope handler received unexpected value', - 'foo', - ); - }); - - it('calls console.error when receiving message with unknown id', async () => { - vi.mocked(snapsUtils.createWindow).mockResolvedValueOnce({} as Window); - - const manager = new IframeManager(); - const errorSpy = vi.spyOn(console, 'error'); - const sendMessageSpy = vi.spyOn(manager, 'sendMessage'); - // Intercept the ping message in create() - sendMessageSpy.mockImplementationOnce(async () => Promise.resolve()); - - const { port1, port2 } = new MessageChannel(); - await manager.create({ getPort: makeGetPort(port1) }); - - port2.postMessage({ - done: false, - value: wrapCommand({ - id: 'foo', - message: { - type: Command.Evaluate, - data: '"bar"', - }, - }), - }); - await delay(10); - - expect(errorSpy).toHaveBeenCalledWith( - 'No unresolved message with id "foo".', - ); - }); - }); -}); diff --git a/packages/extension/src/iframe-manager.ts b/packages/extension/src/iframe-manager.ts deleted file mode 100644 index a61397fea..000000000 --- a/packages/extension/src/iframe-manager.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { makeCapTP } from '@endo/captp'; -import { E } from '@endo/eventual-send'; -import type { PromiseKit } from '@endo/promise-kit'; -import { makePromiseKit } from '@endo/promise-kit'; -import { createWindow } from '@metamask/snaps-utils'; -import type { StreamPair, Reader } from '@ocap/streams'; -import { - initializeMessageChannel, - makeMessagePortStreamPair, -} from '@ocap/streams'; - -import type { - CapTpMessage, - CapTpPayload, - IframeMessage, - MessageId, -} from './message.js'; -import { Command } from './message.js'; -import { makeCounter, type VatId } from './shared.js'; -import { - makeStreamEnvelopeHandler, - wrapCapTp, - wrapCommand, -} from './stream-envelope.js'; -import type { - StreamEnvelope, - StreamEnvelopeHandler, -} from './stream-envelope.js'; - -const IFRAME_URI = 'iframe.html'; - -/** - * Get a DOM id for our iframes, for greater collision resistance. - * - * @param id - The vat id to base the DOM id on. - * @returns The DOM id. - */ -const getHtmlId = (id: VatId): string => `ocap-iframe-${id}`; - -type PromiseCallbacks = Omit, 'promise'>; - -type UnresolvedMessages = Map; - -type GetPort = (targetWindow: Window) => Promise; - -type VatRecord = { - streams: StreamPair; - messageCounter: () => number; - unresolvedMessages: UnresolvedMessages; - streamEnvelopeHandler: StreamEnvelopeHandler; - capTp?: ReturnType; -}; - -/** - * A singleton class to manage and message iframes. - */ -export class IframeManager { - readonly #vats: Map; - - readonly #vatIdCounter: () => number; - - /** - * Create a new IframeManager. - */ - constructor() { - this.#vats = new Map(); - this.#vatIdCounter = makeCounter(); - } - - /** - * Create a new vat, in the form of an iframe. - * - * @param args - Options bag. - * @param args.id - The id of the vat to create. - * @param args.getPort - A function to get the message port for the iframe. - * @returns The iframe's content window, and the id of the associated vat. - */ - async create( - args: { id?: VatId; getPort?: GetPort } = {}, - ): Promise { - const vatId = args.id ?? this.#nextVatId(); - const getPort = args.getPort ?? initializeMessageChannel; - - const newWindow = await createWindow(IFRAME_URI, getHtmlId(vatId)); - const port = await getPort(newWindow); - const streams = makeMessagePortStreamPair(port); - const unresolvedMessages = new Map(); - this.#vats.set(vatId, { - streams, - messageCounter: makeCounter(), - unresolvedMessages, - streamEnvelopeHandler: makeStreamEnvelopeHandler( - { - command: async ({ id, message }) => { - const promiseCallbacks = unresolvedMessages.get(id); - if (promiseCallbacks === undefined) { - console.error(`No unresolved message with id "${id}".`); - } else { - unresolvedMessages.delete(id); - promiseCallbacks.resolve(message.data); - } - }, - }, - console.warn, - ), - }); - /* v8 ignore next 4: Not known to be possible. */ - this.#receiveMessages(vatId, streams.reader).catch((error) => { - console.error(`Unexpected read error from vat "${vatId}"`, error); - this.delete(vatId).catch(() => undefined); - }); - - await this.sendMessage(vatId, { type: Command.Ping, data: null }); - console.debug(`Created vat with id "${vatId}"`); - return [newWindow, vatId] as const; - } - - /** - * Delete a vat and its associated iframe. - * - * @param id - The id of the vat to delete. - * @returns A promise that resolves when the iframe is deleted. - */ - async delete(id: VatId): Promise { - const vat = this.#vats.get(id); - if (vat === undefined) { - return undefined; - } - - const closeP = vat.streams.return(); - - // Handle orphaned messages - for (const [messageId, promiseCallback] of vat.unresolvedMessages) { - promiseCallback?.reject(new Error('Vat was deleted')); - vat.unresolvedMessages.delete(messageId); - } - this.#vats.delete(id); - - const iframe = document.getElementById(getHtmlId(id)); - /* v8 ignore next 6: Not known to be possible. */ - if (iframe === null) { - console.error(`iframe of vat with id "${id}" already removed from DOM`); - return undefined; - } - iframe.remove(); - - return closeP; - } - - /** - * Send a message to a vat. - * - * @param id - The id of the vat to send the message to. - * @param message - The message to send. - * @returns A promise that resolves the response to the message. - */ - async sendMessage(id: VatId, message: IframeMessage): Promise { - const vat = this.#expectGetVat(id); - const { promise, reject, resolve } = makePromiseKit(); - const messageId = this.#nextMessageId(id); - - vat.unresolvedMessages.set(messageId, { reject, resolve }); - await vat.streams.writer.next(wrapCommand({ id: messageId, message })); - return promise; - } - - async callCapTp(id: VatId, payload: CapTpPayload): Promise { - const { capTp } = this.#expectGetVat(id); - if (capTp === undefined) { - throw new Error(`Vat with id "${id}" does not have a CapTP connection.`); - } - return E(capTp.getBootstrap())[payload.method](...payload.params); - } - - async makeCapTp(id: VatId): Promise { - const vat = this.#expectGetVat(id); - if (vat.capTp !== undefined) { - throw new Error(`Vat with id "${id}" already has a CapTP connection.`); - } - - // Handle writes here. #receiveMessages() handles reads. - const { writer } = vat.streams; - // https://github.com/endojs/endo/issues/2412 - // eslint-disable-next-line @typescript-eslint/no-misused-promises - const ctp = makeCapTP(id, async (content: unknown) => { - console.log('CapTP to vat', JSON.stringify(content, null, 2)); - await writer.next(wrapCapTp(content as CapTpMessage)); - }); - - vat.capTp = ctp; - vat.streamEnvelopeHandler.contentHandlers.capTp = async (content) => { - console.log('CapTP from vat', JSON.stringify(content, null, 2)); - ctp.dispatch(content); - }; - - return this.sendMessage(id, { type: Command.CapTpInit, data: null }); - } - - async #receiveMessages( - vatId: VatId, - reader: Reader, - ): Promise { - const vat = this.#expectGetVat(vatId); - - for await (const rawMessage of reader) { - console.debug('Offscreen received message', rawMessage); - await vat.streamEnvelopeHandler.handle(rawMessage); - } - } - - /** - * Get a vat record by id, or throw an error if it doesn't exist. - * - * @param id - The id of the vat to get. - * @returns The vat record. - */ - #expectGetVat(id: VatId): VatRecord { - const vat = this.#vats.get(id); - if (vat === undefined) { - throw new Error(`No vat with id "${id}"`); - } - return vat; - } - - readonly #nextMessageId = (id: VatId): MessageId => { - return `${id}-${this.#expectGetVat(id).messageCounter()}`; - }; - - readonly #nextVatId = (): MessageId => { - return `${this.#vatIdCounter()}`; - }; -} diff --git a/packages/extension/src/iframe.ts b/packages/extension/src/iframe.ts index 294560d4a..fe23a7516 100644 --- a/packages/extension/src/iframe.ts +++ b/packages/extension/src/iframe.ts @@ -1,20 +1,20 @@ import { makeCapTP } from '@endo/captp'; import { makeExo } from '@endo/exo'; import { M } from '@endo/patterns'; -import { receiveMessagePort, makeMessagePortStreamPair } from '@ocap/streams'; - import type { + StreamEnvelope, CapTpMessage, - IframeMessage, - WrappedIframeMessage, -} from './message.js'; -import { Command } from './message.js'; -import type { StreamEnvelope } from './stream-envelope.js'; + VatMessage, + WrappedVatMessage, +} from '@ocap/streams'; import { - wrapCapTp, - wrapCommand, + receiveMessagePort, + makeMessagePortStreamPair, makeStreamEnvelopeHandler, -} from './stream-envelope.js'; + Command, + wrapCapTp, + wrapStreamCommand, +} from '@ocap/streams'; const defaultCompartment = new Compartment({ URL }); @@ -56,7 +56,7 @@ async function main(): Promise { async function handleMessage({ id, message, - }: WrappedIframeMessage): Promise { + }: WrappedVatMessage): Promise { switch (message.type) { case Command.Evaluate: { if (typeof message.data !== 'string') { @@ -109,9 +109,9 @@ async function main(): Promise { */ async function replyToMessage( id: string, - message: IframeMessage, + message: VatMessage, ): Promise { - await streams.writer.next(wrapCommand({ id, message })); + await streams.writer.next(wrapStreamCommand({ id, message })); } /** diff --git a/packages/extension/src/makeIframeVatRealm.ts b/packages/extension/src/makeIframeVatRealm.ts new file mode 100644 index 000000000..be823329e --- /dev/null +++ b/packages/extension/src/makeIframeVatRealm.ts @@ -0,0 +1,39 @@ +import { createWindow } from '@metamask/snaps-utils'; +import type { VatId, VatRealm } from '@ocap/kernel'; +import type { initializeMessageChannel, StreamEnvelope } from '@ocap/streams'; +import { makeMessagePortStreamPair } from '@ocap/streams'; + +const IFRAME_URI = 'iframe.html'; + +/** + * Get a DOM id for our iframes, for greater collision resistance. + * + * @param id - The vat id to base the DOM id on. + * @returns The DOM id. + */ +const getHtmlId = (id: VatId): string => `ocap-iframe-${id}`; + +export const makeIframeVatRealm = ( + id: VatId, + getPort: typeof initializeMessageChannel, +): VatRealm => { + return { + init: async () => { + const newWindow = await createWindow(IFRAME_URI, getHtmlId(id)); + const port = await getPort(newWindow); + const streams = makeMessagePortStreamPair(port); + + return [streams, newWindow]; + }, + delete: async (): Promise => { + const iframe = document.getElementById(getHtmlId(id)); + /* v8 ignore next 6: Not known to be possible. */ + if (iframe === null) { + console.error(`iframe of vat with id "${id}" already removed from DOM`); + return undefined; + } + iframe.remove(); + return undefined; + }, + }; +}; diff --git a/packages/extension/src/message.test.ts b/packages/extension/src/message.test.ts deleted file mode 100644 index 8fdec3480..000000000 --- a/packages/extension/src/message.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { isWrappedIframeMessage } from './message.js'; - -describe('message', () => { - describe('isWrappedIframeMessage', () => { - it('returns true for valid messages', () => { - expect( - isWrappedIframeMessage({ - id: '1', - message: { type: 'evaluate', data: '1 + 1' }, - }), - ).toBe(true); - }); - - it.each([ - [{}], - [{ id: '1' }], - [{ message: { type: 'evaluate' } }], - [{ id: '1', message: { type: 'evaluate' } }], - [{ id: '1', message: { type: 'evaluate', data: 1 } }], - ])('returns false for invalid messages: %j', (message) => { - expect(isWrappedIframeMessage(message)).toBe(false); - }); - }); -}); diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index cfbed112f..5a609d5d7 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,9 +1,12 @@ -import { createWindow } from '@metamask/snaps-utils'; -import { initializeMessageChannel } from '@ocap/streams'; -import { VatManager, VatIframe } from '@ocap/kernel'; +import { Kernel, Vat } from '@ocap/kernel'; +import { + initializeMessageChannel, + Command, + KernelMessageTarget, +} from '@ocap/streams'; +import type { KernelMessage } from '@ocap/streams'; -import type { ExtensionMessage } from './message.js'; -import { Command, ExtensionMessageTarget } from './message.js'; +import { makeIframeVatRealm } from './makeIframeVatRealm.js'; import { makeHandledCallback } from './shared.js'; main().catch(console.error); @@ -12,39 +15,37 @@ main().catch(console.error); * The main function for the offscreen script. */ async function main(): Promise { - const kernel = new VatManager(); + const kernel = new Kernel(); - const vatIframe = new VatIframe({ id: 'default' }); - const newWindow = await createWindow('iframe.html', vatIframe.iframeId); - const port = await initializeMessageChannel(newWindow); - await vatIframe.init(port); - const iframeReadyP = await vatIframe.makeCapTp(); + const vat = new Vat({ + id: 'default', + realm: makeIframeVatRealm('default', initializeMessageChannel), + }); + await vat.init(); - kernel.addVat(vatIframe); + kernel.addVat(vat); // Handle messages from the background service worker chrome.runtime.onMessage.addListener( - makeHandledCallback(async (message: ExtensionMessage) => { - if (message.target !== ExtensionMessageTarget.Offscreen) { + makeHandledCallback(async (message: KernelMessage) => { + if (message.target !== KernelMessageTarget.Offscreen) { console.warn( `Offscreen received message with unexpected target: "${message.target}"`, ); return; } - await iframeReadyP; - switch (message.type) { case Command.Evaluate: await reply(Command.Evaluate, await evaluate(message.data)); break; case Command.CapTpCall: { - const result = await vatIframe.callCapTp(message.data); + const result = await vat.callCapTp(message.data); await reply(Command.CapTpCall, JSON.stringify(result, null, 2)); break; } case Command.CapTpInit: - await vatIframe.makeCapTp(); + await vat.makeCapTp(); await reply(Command.CapTpInit, '~~~ CapTP Initialized ~~~'); break; case Command.Ping: @@ -69,7 +70,7 @@ async function main(): Promise { async function reply(type: Command, data?: string): Promise { await chrome.runtime.sendMessage({ data: data ?? null, - target: ExtensionMessageTarget.Background, + target: KernelMessageTarget.Background, type, }); } @@ -82,7 +83,7 @@ async function main(): Promise { */ async function evaluate(source: string): Promise { try { - const result = await kernel.sendMessage(vatIframe.id, { + const result = await kernel.sendMessage(vat.id, { type: Command.Evaluate, data: source, }); diff --git a/packages/extension/src/shared.test.ts b/packages/extension/src/shared.test.ts index 54c4ff8aa..c409eaec8 100644 --- a/packages/extension/src/shared.test.ts +++ b/packages/extension/src/shared.test.ts @@ -2,7 +2,7 @@ import './endoify.js'; import { delay } from '@ocap/test-utils'; import { vi, describe, it, expect } from 'vitest'; -import { makeCounter, makeHandledCallback } from './shared.js'; +import { makeHandledCallback } from './shared.js'; describe('shared', () => { describe('makeHandledCallback', () => { @@ -37,25 +37,4 @@ describe('shared', () => { ); }); }); - - describe('makeCounter', () => { - it('starts at 1 by default', () => { - const counter = makeCounter(); - expect(counter()).toBe(1); - }); - - it('starts counting from the supplied argument', () => { - const start = 50; - const counter = makeCounter(start); - expect(counter()).toStrictEqual(start + 1); - }); - - it('increments convincingly', () => { - const counter = makeCounter(); - const first = counter(); - expect(counter()).toStrictEqual(first + 1); - expect(counter()).toStrictEqual(first + 2); - expect(counter()).toStrictEqual(first + 3); - }); - }); }); diff --git a/packages/extension/src/shared.ts b/packages/extension/src/shared.ts index c41aa7abc..2bc7a4e10 100644 --- a/packages/extension/src/shared.ts +++ b/packages/extension/src/shared.ts @@ -14,17 +14,3 @@ export const makeHandledCallback = ( callback(...args).catch(console.error); }; }; - -/** - * A simple counter which increments and returns when called. - * - * @param start - One less than the first returned number. - * @returns A counter. - */ -export const makeCounter = (start: number = 0) => { - let counter: number = start; - return () => { - counter += 1; - return counter; - }; -}; diff --git a/packages/kernel/package.json b/packages/kernel/package.json index 8d3b009fb..38a784af4 100644 --- a/packages/kernel/package.json +++ b/packages/kernel/package.json @@ -7,19 +7,16 @@ "type": "git", "url": "https://github.com/MetaMask/ocap-kernel.git" }, - "sideEffects": false, "type": "module", "exports": { ".": "./src/index.ts", "./package.json": "./package.json" }, - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", "files": [ "dist/" ], "scripts": { - "build": "node scripts/bundle.js", + "build": "ts-bridge --project tsconfig.build.json --clean", "build:docs": "typedoc", "changelog:validate": "../../scripts/validate-changelog.sh @ocap/kernel", "clean": "rimraf --glob ./dist './*.tsbuildinfo'", @@ -38,21 +35,18 @@ "dependencies": { "@endo/captp": "^4.2.2", "@endo/eventual-send": "^1.2.4", - "@endo/exo": "^1.5.2", - "@endo/patterns": "^1.4.2", - "@endo/promise-kit": "^1.1.5", - "@metamask/snaps-utils": "^7.8.0", - "@metamask/utils": "^9.1.0", + "@endo/promise-kit": "^1.1.4", "@ocap/shims": "workspace:^", "@ocap/streams": "workspace:^", "ses": "^1.7.0" }, "devDependencies": { - "@endo/bundle-source": "^3.3.0", "@metamask/auto-changelog": "^3.4.4", "@metamask/eslint-config": "^13.0.0", "@metamask/eslint-config-nodejs": "^13.0.0", "@metamask/eslint-config-typescript": "^13.0.0", + "@ts-bridge/cli": "^0.5.1", + "@ts-bridge/shims": "^0.1.1", "@typescript-eslint/eslint-plugin": "^8.1.0", "@typescript-eslint/parser": "^8.1.0", "depcheck": "^1.4.7", diff --git a/packages/kernel/src/VatManager.ts b/packages/kernel/src/Kernel.ts similarity index 55% rename from packages/kernel/src/VatManager.ts rename to packages/kernel/src/Kernel.ts index ac67535e5..1899b14ed 100644 --- a/packages/kernel/src/VatManager.ts +++ b/packages/kernel/src/Kernel.ts @@ -1,9 +1,11 @@ import '@ocap/shims/endoify'; -import type { VatId, VatMessage } from './types.ts'; -import type { VatIframe } from './VatIframe.ts'; +import type { VatMessage } from '@ocap/streams'; -export class VatManager { - readonly #vats: Map; +import type { VatId } from './types.js'; +import type { Vat } from './Vat.js'; + +export class Kernel { + readonly #vats: Map; constructor() { this.#vats = new Map(); @@ -23,17 +25,11 @@ export class VatManager { * * @param vat - The vat record. */ - public addVat(vat: VatIframe): void { + public addVat(vat: Vat): void { if (this.#vats.has(vat.id)) { throw new Error(`Vat with ID ${vat.id} already exists.`); } this.#vats.set(vat.id, vat); - - /* v8 ignore next 4: Not known to be possible. */ - this.#receiveMessages(vat.id, vat.streams.reader).catch((error) => { - console.error(`Unexpected read error from vat "${vat.id}"`, error); - this.deleteVat(vat.id); - }); } /** @@ -41,9 +37,9 @@ export class VatManager { * * @param id - The ID of the vat. */ - public deleteVat(id: string): void { + public async deleteVat(id: string): Promise { const vat = this.#vats.get(id); - vat?.terminate(); + await vat?.terminate(); this.#vats.delete(id); } @@ -59,31 +55,13 @@ export class VatManager { return vat.sendMessage(message); } - /** - * Receives messages from a vat. - * - * @param vatId - The ID of the vat. - * @param reader - The reader for the messages. - */ - async #receiveMessages( - vatId: VatId, - reader: Reader, - ): Promise { - const vat = this.#getVat(vatId); - - for await (const rawMessage of reader) { - console.debug('Offscreen received message', rawMessage); - await vat.streamEnvelopeHandler.handle(rawMessage); - } - } - /** * Gets a vat from the kernel. * * @param id - The ID of the vat. * @returns The vat record. */ - #getVat(id: string): VatIframe { + #getVat(id: string): Vat { const vat = this.#vats.get(id); if (vat === undefined) { throw new Error(`Vat with ID ${id} does not exist.`); diff --git a/packages/kernel/src/VatBase.ts b/packages/kernel/src/Vat.ts similarity index 65% rename from packages/kernel/src/VatBase.ts rename to packages/kernel/src/Vat.ts index 18a0c8c5b..d253be3fe 100644 --- a/packages/kernel/src/VatBase.ts +++ b/packages/kernel/src/Vat.ts @@ -1,49 +1,60 @@ import { makeCapTP } from '@endo/captp'; import { E } from '@endo/eventual-send'; import { makePromiseKit } from '@endo/promise-kit'; -import type { StreamPair } from '@ocap/streams'; -import { makeMessagePortStreamPair } from '@ocap/streams'; - import type { + StreamPair, + StreamEnvelope, + StreamEnvelopeHandler, + Reader, CapTpMessage, CapTpPayload, - MessageId, - UnresolvedMessages, VatMessage, -} from './types.ts'; -import { Command } from './types.ts'; -import { makeCounter } from './utils/makeCounter.ts'; - -export type VatBaseProps = { + MessageId, +} from '@ocap/streams'; +import { + wrapCapTp, + wrapStreamCommand, + Command, + makeStreamEnvelopeHandler, +} from '@ocap/streams'; + +import type { UnresolvedMessages, VatRealm } from './types.js'; +import { makeCounter } from './utils/makeCounter.js'; + +export type VatProps = { id: string; + realm: VatRealm; }; -export abstract class VatBase { - readonly id: string; +export class Vat { + readonly id: VatProps['id']; + + readonly #realm: VatProps['realm']; readonly #messageCounter: () => number; readonly unresolvedMessages: UnresolvedMessages = new Map(); - streams: StreamPair; + streams?: StreamPair; - streamEnvelopeHandler: StreamEnvelopeHandler; + streamEnvelopeHandler?: StreamEnvelopeHandler; capTp?: ReturnType; - constructor({ id }: VatBaseProps) { + constructor({ id, realm }: VatProps) { this.id = id; + this.#realm = realm; this.#messageCounter = makeCounter(); } /** * Initializes the vat. * - * @param port - The message port to use for communication. * @returns A promise that resolves when the vat is initialized. */ - async init(port: MessagePort): Promise { - this.streams = makeMessagePortStreamPair(port); + async init(): Promise { + const [streams] = await this.#realm.init(); + this.streams = streams; this.streamEnvelopeHandler = makeStreamEnvelopeHandler( { command: async ({ id, message }) => { @@ -59,8 +70,28 @@ export abstract class VatBase { console.warn, ); + /* v8 ignore next 4: Not known to be possible. */ + this.#receiveMessages(this.streams.reader).catch((error) => { + console.error(`Unexpected read error from vat "${this.id}"`, error); + throw error; + }); + await this.sendMessage({ type: Command.Ping, data: null }); console.debug(`Created vat with id "${this.id}"`); + + return await this.makeCapTp(); + } + + /** + * Receives messages from a vat. + * + * @param reader - The reader for the messages. + */ + async #receiveMessages(reader: Reader): Promise { + for await (const rawMessage of reader) { + console.debug('Offscreen received message', rawMessage); + await this.streamEnvelopeHandler?.handle(rawMessage); + } } /** @@ -75,6 +106,18 @@ export abstract class VatBase { ); } + if (!this.streams) { + throw new Error( + `Vat with id "${this.id}" does not have a stream connection.`, + ); + } + + if (!this.streamEnvelopeHandler) { + throw new Error( + `Vat with id "${this.id}" does not have a stream envelope handler.`, + ); + } + // Handle writes here. #receiveMessages() handles reads. const { writer } = this.streams; // https://github.com/endojs/endo/issues/2412 @@ -86,7 +129,7 @@ export abstract class VatBase { this.capTp = ctp; this.streamEnvelopeHandler.contentHandlers.capTp = async ( - content: string, + content: CapTpMessage, ) => { console.log('CapTP from vat', JSON.stringify(content, null, 2)); ctp.dispatch(content); @@ -113,14 +156,16 @@ export abstract class VatBase { /** * Terminates the vat. */ - terminate(): void { - this.streams.return(); + async terminate(): Promise { + await this.streams?.return(); // Handle orphaned messages for (const [messageId, promiseCallback] of this.unresolvedMessages) { promiseCallback?.reject(new Error('Vat was deleted')); this.unresolvedMessages.delete(messageId); } + + await this.#realm.delete(); } /** @@ -133,14 +178,15 @@ export abstract class VatBase { const { promise, reject, resolve } = makePromiseKit(); const messageId = this.#nextMessageId(); this.unresolvedMessages.set(messageId, { reject, resolve }); - await this.streams.writer.next(wrapCommand({ id: messageId, message })); + await this.streams?.writer.next( + wrapStreamCommand({ id: messageId, message }), + ); return promise; } /** * Gets the next message ID. * - * @param id - The vat ID. * @returns The message ID. */ readonly #nextMessageId = (): MessageId => { diff --git a/packages/kernel/src/VatIframe.ts b/packages/kernel/src/VatIframe.ts deleted file mode 100644 index 12b339e27..000000000 --- a/packages/kernel/src/VatIframe.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getHtmlId } from './utils/getHtmlId.ts'; -import type { VatBaseProps } from './VatBase.ts'; -import { VatBase } from './VatBase.ts'; - -export class VatIframe extends VatBase { - readonly iframeId: string; - - constructor({ id }: VatBaseProps) { - super({ id }); - - this.iframeId = getHtmlId(id); - } - - /** - * Terminates the vat. - */ - terminate(): void { - super.terminate(); - - const iframe = document.getElementById(this.iframeId); - /* v8 ignore next 6: Not known to be possible. */ - if (iframe === null) { - console.error( - `iframe of vat with id "${this.id}" already removed from DOM`, - ); - return; - } - iframe.remove(); - } -} diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts index ca1b5f323..1b0743d01 100644 --- a/packages/kernel/src/index.ts +++ b/packages/kernel/src/index.ts @@ -1,2 +1,3 @@ -export { VatManager } from './VatManager.ts'; -export { VatIframe } from './VatIframe.ts'; +export { Kernel } from './Kernel.js'; +export { Vat } from './Vat.js'; +export type { VatId, VatRealm } from './types.js'; diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index e10d9a232..348835a69 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -1,67 +1,13 @@ -import type { Primitive } from '@endo/captp'; import type { PromiseKit } from '@endo/promise-kit'; +import type { StreamPair, MessageId, StreamEnvelope } from '@ocap/streams'; export type VatId = string; -export type MessageId = string; - -export enum KernelMessageTarget { - Background = 'background', - Offscreen = 'offscreen', - WebWorker = 'webWorker', - Node = 'node', -} +export type VatRealm = { + init: () => Promise<[StreamPair, unknown]>; + delete: () => Promise; +}; export type PromiseCallbacks = Omit, 'promise'>; export type UnresolvedMessages = Map; - -export type GetPort = (targetWindow: Window) => Promise; - -export type DataObject = - | Primitive - | Promise - | DataObject[] - | { [key: string]: DataObject }; - -type CommandLike< - CommandType extends Command, - Data extends DataObject, - TargetType extends KernelMessageTarget, -> = { - type: CommandType; - target?: TargetType; - data: Data; -}; - -export enum Command { - CapTpCall = 'callCapTp', - CapTpInit = 'makeCapTp', - Evaluate = 'evaluate', - Ping = 'ping', -} - -export type CapTpPayload = { - method: string; - params: DataObject[]; -}; - -type CommandMessage = - | CommandLike - | CommandLike - | CommandLike - | CommandLike; - -export type KernelMessage = CommandMessage; -export type VatMessage = CommandMessage; - -export type WrappedVatMessage = { - id: MessageId; - message: VatMessage; -}; - -export type CapTpMessage = { - type: Type; - epoch: number; - [key: string]: unknown; -}; diff --git a/packages/kernel/src/utils/getHtmlId.ts b/packages/kernel/src/utils/getHtmlId.ts deleted file mode 100644 index 7a20c8249..000000000 --- a/packages/kernel/src/utils/getHtmlId.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { VatId } from 'src/types.ts'; - -/** - * Get a DOM id for our iframes, for greater collision resistance. - * - * @param id - The vat id to base the DOM id on. - * @returns The DOM id. - */ -export const getHtmlId = (id: VatId): string => `ocap-iframe-${id}`; diff --git a/packages/kernel/src/utils/makeCounter.test.ts b/packages/kernel/src/utils/makeCounter.test.ts new file mode 100644 index 000000000..d40784d27 --- /dev/null +++ b/packages/kernel/src/utils/makeCounter.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; + +import { makeCounter } from './makeCounter.js'; + +describe('makeCounter', () => { + it('starts at 1 by default', () => { + const counter = makeCounter(); + expect(counter()).toBe(1); + }); + + it('starts counting from the supplied argument', () => { + const start = 50; + const counter = makeCounter(start); + expect(counter()).toStrictEqual(start + 1); + }); + + it('increments convincingly', () => { + const counter = makeCounter(); + const first = counter(); + expect(counter()).toStrictEqual(first + 1); + expect(counter()).toStrictEqual(first + 2); + expect(counter()).toStrictEqual(first + 3); + }); +}); diff --git a/packages/kernel/tsconfig.build.json b/packages/kernel/tsconfig.build.json index 2a475d693..a406f5f5a 100644 --- a/packages/kernel/tsconfig.build.json +++ b/packages/kernel/tsconfig.build.json @@ -4,7 +4,8 @@ "baseUrl": "./", "outDir": "./dist", "rootDir": "./src", - "types": ["chrome", "ses"] + "lib": ["DOM", "ES2022"], + "types": ["ses"] }, "references": [], "include": ["./src"] diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json index 3c8627035..1f81a3920 100644 --- a/packages/kernel/tsconfig.json +++ b/packages/kernel/tsconfig.json @@ -8,5 +8,5 @@ "checkJs": true }, "references": [], - "include": ["./src"] + "include": ["./src", "../streams/src/type-guards.ts"] } diff --git a/packages/streams/package.json b/packages/streams/package.json index d2e4b164a..b459b39a1 100644 --- a/packages/streams/package.json +++ b/packages/streams/package.json @@ -46,9 +46,11 @@ "test:watch": "vitest --config vitest.config.ts" }, "dependencies": { + "@endo/captp": "^4.2.2", "@endo/promise-kit": "^1.1.4", "@endo/stream": "^1.2.2", - "@metamask/utils": "^9.1.0" + "@metamask/utils": "^9.1.0", + "@ocap/shims": "workspace:^" }, "devDependencies": { "@arethetypeswrong/cli": "^0.15.3", diff --git a/packages/streams/src/index.ts b/packages/streams/src/index.ts index 7ddd045fa..b794edc38 100644 --- a/packages/streams/src/index.ts +++ b/packages/streams/src/index.ts @@ -3,16 +3,21 @@ export { receiveMessagePort, } from './message-channel.js'; export type { StreamPair, Reader, Writer } from './streams.js'; +export { makeMessagePortStreamPair } from './streams.js'; +export { makeStreamEnvelopeKit } from './envelope-kit.js'; +export type { + CapTpMessage, + CapTpPayload, + MessageId, + VatMessage, + KernelMessage, + WrappedVatMessage, +} from './types.js'; +export { KernelMessageTarget, Command } from './types.js'; export { - makeMessagePortStreamPair, - MessagePortReader, - MessagePortWriter, -} from './streams.js'; -export { - makeStreamEnvelopeKit, - type StreamEnvelopeKit, - type MakeStreamEnvelopeHandler, -} from './envelope-kit.js'; -export type { StreamEnvelopeHandler } from './envelope-handler.js'; -export type { StreamEnveloper } from './enveloper.js'; -export type { Envelope, StreamEnvelope } from './envelope.js'; + wrapStreamCommand, + wrapCapTp, + makeStreamEnvelopeHandler, + type StreamEnvelope, + type StreamEnvelopeHandler, +} from './stream-envelope.js'; diff --git a/packages/extension/src/stream-envelope.test.ts b/packages/streams/src/stream-envelope.test.ts similarity index 84% rename from packages/extension/src/stream-envelope.test.ts rename to packages/streams/src/stream-envelope.test.ts index 94677e051..e93aab36d 100644 --- a/packages/extension/src/stream-envelope.test.ts +++ b/packages/streams/src/stream-envelope.test.ts @@ -1,16 +1,16 @@ import '@ocap/shims/endoify'; import { describe, it, expect } from 'vitest'; -import type { CapTpMessage, WrappedIframeMessage } from './message.js'; -import { Command } from './message.js'; import { wrapCapTp, - wrapCommand, + wrapStreamCommand, makeStreamEnvelopeHandler, } from './stream-envelope.js'; +import type { CapTpMessage, WrappedVatMessage } from './types.js'; +import { Command } from './types.js'; describe('StreamEnvelopeHandler', () => { - const commandContent: WrappedIframeMessage = { + const commandContent: WrappedVatMessage = { id: '1', message: { type: Command.Evaluate, data: '1 + 1' }, }; @@ -21,7 +21,7 @@ describe('StreamEnvelopeHandler', () => { unreliableKey: Symbol('unreliableValue'), }; - const commandLabel = wrapCommand(commandContent).label; + const commandLabel = wrapStreamCommand(commandContent).label; const capTpLabel = wrapCapTp(capTpContent).label; const testEnvelopeHandlers = { @@ -34,9 +34,9 @@ describe('StreamEnvelopeHandler', () => { }; it.each` - wrapper | content | label - ${wrapCommand} | ${commandContent} | ${commandLabel} - ${wrapCapTp} | ${capTpContent} | ${capTpLabel} + wrapper | content | label + ${wrapStreamCommand} | ${commandContent} | ${commandLabel} + ${wrapCapTp} | ${capTpContent} | ${capTpLabel} `('handles valid StreamEnvelopes', async ({ wrapper, content, label }) => { const handler = makeStreamEnvelopeHandler( testEnvelopeHandlers, diff --git a/packages/extension/src/stream-envelope.ts b/packages/streams/src/stream-envelope.ts similarity index 74% rename from packages/extension/src/stream-envelope.ts rename to packages/streams/src/stream-envelope.ts index f120cfd70..a53b3fef4 100644 --- a/packages/extension/src/stream-envelope.ts +++ b/packages/streams/src/stream-envelope.ts @@ -1,9 +1,6 @@ -import { makeStreamEnvelopeKit } from '@ocap/streams'; - -import type { CapTpMessage, WrappedIframeMessage } from './message.js'; -import { isCapTpMessage, isWrappedIframeMessage } from './message.js'; - -// Utilitous generic types. +import { makeStreamEnvelopeKit } from './envelope-kit.js'; +import { isCapTpMessage, isWrappedVatMessage } from './type-guards.js'; +import type { CapTpMessage, WrappedVatMessage } from './types.js'; type GuardType = TypeGuard extends ( value: unknown, @@ -30,11 +27,11 @@ const envelopeLabels = Object.values(EnvelopeLabel); const envelopeKit = makeStreamEnvelopeKit< typeof envelopeLabels, { - command: WrappedIframeMessage; + command: WrappedVatMessage; capTp: CapTpMessage; } >({ - command: isWrappedIframeMessage, + command: isWrappedVatMessage, capTp: isCapTpMessage, }); @@ -45,6 +42,6 @@ export type StreamEnvelopeHandler = ReturnType< typeof makeStreamEnvelopeHandler >; -export const wrapCommand = streamEnveloper.command.wrap; +export const wrapStreamCommand = streamEnveloper.command.wrap; export const wrapCapTp = streamEnveloper.capTp.wrap; export { makeStreamEnvelopeHandler }; diff --git a/packages/kernel/src/type-guards.ts b/packages/streams/src/type-guards.ts similarity index 89% rename from packages/kernel/src/type-guards.ts rename to packages/streams/src/type-guards.ts index 10b5a538b..cec6426df 100644 --- a/packages/kernel/src/type-guards.ts +++ b/packages/streams/src/type-guards.ts @@ -1,6 +1,6 @@ import { isObject } from '@metamask/utils'; -import type { CapTpMessage, WrappedVatMessage } from './types.ts'; +import type { CapTpMessage, WrappedVatMessage } from './types.js'; export const isWrappedVatMessage = ( value: unknown, diff --git a/packages/extension/src/message.ts b/packages/streams/src/types.ts similarity index 51% rename from packages/extension/src/message.ts rename to packages/streams/src/types.ts index 3ec1eaeaf..18ec5d4c7 100644 --- a/packages/extension/src/message.ts +++ b/packages/streams/src/types.ts @@ -1,23 +1,24 @@ import type { Primitive } from '@endo/captp'; -import { isObject } from '@metamask/utils'; export type MessageId = string; -type DataObject = +export enum KernelMessageTarget { + Background = 'background', + Offscreen = 'offscreen', + WebWorker = 'webWorker', + Node = 'node', +} + +export type DataObject = | Primitive | Promise | DataObject[] | { [key: string]: DataObject }; -export enum ExtensionMessageTarget { - Background = 'background', - Offscreen = 'offscreen', -} - type CommandLike< CommandType extends Command, Data extends DataObject, - TargetType extends ExtensionMessageTarget, + TargetType extends KernelMessageTarget, > = { type: CommandType; target?: TargetType; @@ -36,37 +37,22 @@ export type CapTpPayload = { params: DataObject[]; }; -type CommandMessage = +type CommandMessage = | CommandLike | CommandLike | CommandLike | CommandLike; -export type ExtensionMessage = CommandMessage; -export type IframeMessage = CommandMessage; +export type KernelMessage = CommandMessage; +export type VatMessage = CommandMessage; -export type WrappedIframeMessage = { +export type WrappedVatMessage = { id: MessageId; - message: IframeMessage; + message: VatMessage; }; -export const isWrappedIframeMessage = ( - value: unknown, -): value is WrappedIframeMessage => - isObject(value) && - typeof value.id === 'string' && - isObject(value.message) && - typeof value.message.type === 'string' && - (typeof value.message.data === 'string' || value.message.data === null); - export type CapTpMessage = { type: Type; epoch: number; [key: string]: unknown; }; - -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/streams/tsconfig.json b/packages/streams/tsconfig.json index 4dda57d2d..f719ece8b 100644 --- a/packages/streams/tsconfig.json +++ b/packages/streams/tsconfig.json @@ -3,7 +3,11 @@ "compilerOptions": { "baseUrl": "./", "lib": ["DOM", "ES2022"], - "types": ["ses", "vitest", "vitest/jsdom"] + "types": ["ses", "vitest", "vitest/jsdom"], + "allowImportingTsExtensions": true, + "emitDeclarationOnly": true, + "allowJs": true, + "checkJs": true }, "references": [{ "path": "../test-utils" }], "include": ["./src", "./test/envelope-kit-fixtures.ts"] diff --git a/yarn.lock b/yarn.lock index 0ffecc01c..833b61b06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1372,7 +1372,6 @@ __metadata: "@endo/eventual-send": "npm:^1.2.4" "@endo/exo": "npm:^1.5.2" "@endo/patterns": "npm:^1.4.2" - "@endo/promise-kit": "npm:^1.1.4" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eslint-config": "npm:^13.0.0" "@metamask/eslint-config-nodejs": "npm:^13.0.0" @@ -1413,20 +1412,17 @@ __metadata: version: 0.0.0-use.local resolution: "@ocap/kernel@workspace:packages/kernel" dependencies: - "@endo/bundle-source": "npm:^3.3.0" "@endo/captp": "npm:^4.2.2" "@endo/eventual-send": "npm:^1.2.4" - "@endo/exo": "npm:^1.5.2" - "@endo/patterns": "npm:^1.4.2" - "@endo/promise-kit": "npm:^1.1.5" + "@endo/promise-kit": "npm:^1.1.4" "@metamask/auto-changelog": "npm:^3.4.4" "@metamask/eslint-config": "npm:^13.0.0" "@metamask/eslint-config-nodejs": "npm:^13.0.0" "@metamask/eslint-config-typescript": "npm:^13.0.0" - "@metamask/snaps-utils": "npm:^7.8.0" - "@metamask/utils": "npm:^9.1.0" "@ocap/shims": "workspace:^" "@ocap/streams": "workspace:^" + "@ts-bridge/cli": "npm:^0.5.1" + "@ts-bridge/shims": "npm:^0.1.1" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" "@typescript-eslint/parser": "npm:^8.1.0" depcheck: "npm:^1.4.7" @@ -1524,6 +1520,7 @@ __metadata: resolution: "@ocap/streams@workspace:packages/streams" dependencies: "@arethetypeswrong/cli": "npm:^0.15.3" + "@endo/captp": "npm:^4.2.2" "@endo/promise-kit": "npm:^1.1.4" "@endo/stream": "npm:^1.2.2" "@metamask/auto-changelog": "npm:^3.4.4" @@ -1531,6 +1528,7 @@ __metadata: "@metamask/eslint-config-nodejs": "npm:^13.0.0" "@metamask/eslint-config-typescript": "npm:^13.0.0" "@metamask/utils": "npm:^9.1.0" + "@ocap/shims": "workspace:^" "@ocap/test-utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1" From eed794e589eeb73bc69b223cc9b980cc8fedab3a Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Thu, 19 Sep 2024 21:19:13 +0100 Subject: [PATCH 03/19] fix assertion --- packages/extension/src/offscreen.ts | 4 +++- packages/kernel/src/Vat.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 5a609d5d7..5f973c3aa 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -21,7 +21,7 @@ async function main(): Promise { id: 'default', realm: makeIframeVatRealm('default', initializeMessageChannel), }); - await vat.init(); + const iframeReadyP = vat.init(); kernel.addVat(vat); @@ -35,6 +35,8 @@ async function main(): Promise { return; } + await iframeReadyP; + switch (message.type) { case Command.Evaluate: await reply(Command.Evaluate, await evaluate(message.data)); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index d253be3fe..6974a53e7 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -100,7 +100,7 @@ export class Vat { * @returns A promise that resolves when the CapTP connection is made. */ async makeCapTp(): Promise { - if (!this.capTp) { + if (this.capTp !== undefined) { throw new Error( `Vat with id "${this.id}" already has a CapTP connection.`, ); From 9951d0b65efe93605425bea48be969e718d6fd4f Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 11:48:27 +0100 Subject: [PATCH 04/19] fix tsconfigs, rename realm to worker and some sanity checks --- ...rameVatRealm.ts => makeIframeVatWorker.ts} | 6 ++--- packages/extension/src/offscreen.ts | 4 ++-- packages/kernel/.eslintrc.cjs | 20 ----------------- packages/kernel/package.json | 11 +++++++++- packages/kernel/src/Kernel.ts | 4 ++-- packages/kernel/src/Vat.ts | 22 ++++++++++++------- packages/kernel/src/index.ts | 2 +- packages/kernel/src/types.ts | 2 +- packages/kernel/tsconfig.json | 8 ++----- packages/streams/tsconfig.json | 2 -- 10 files changed, 35 insertions(+), 46 deletions(-) rename packages/extension/src/{makeIframeVatRealm.ts => makeIframeVatWorker.ts} (91%) diff --git a/packages/extension/src/makeIframeVatRealm.ts b/packages/extension/src/makeIframeVatWorker.ts similarity index 91% rename from packages/extension/src/makeIframeVatRealm.ts rename to packages/extension/src/makeIframeVatWorker.ts index be823329e..3282a2c22 100644 --- a/packages/extension/src/makeIframeVatRealm.ts +++ b/packages/extension/src/makeIframeVatWorker.ts @@ -1,5 +1,5 @@ import { createWindow } from '@metamask/snaps-utils'; -import type { VatId, VatRealm } from '@ocap/kernel'; +import type { VatId, VatWorker } from '@ocap/kernel'; import type { initializeMessageChannel, StreamEnvelope } from '@ocap/streams'; import { makeMessagePortStreamPair } from '@ocap/streams'; @@ -13,10 +13,10 @@ const IFRAME_URI = 'iframe.html'; */ const getHtmlId = (id: VatId): string => `ocap-iframe-${id}`; -export const makeIframeVatRealm = ( +export const makeIframeVatWorker = ( id: VatId, getPort: typeof initializeMessageChannel, -): VatRealm => { +): VatWorker => { return { init: async () => { const newWindow = await createWindow(IFRAME_URI, getHtmlId(id)); diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 5f973c3aa..61d897b74 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -6,7 +6,7 @@ import { } from '@ocap/streams'; import type { KernelMessage } from '@ocap/streams'; -import { makeIframeVatRealm } from './makeIframeVatRealm.js'; +import { makeIframeVatWorker } from './makeIframeVatWorker.js'; import { makeHandledCallback } from './shared.js'; main().catch(console.error); @@ -19,7 +19,7 @@ async function main(): Promise { const vat = new Vat({ id: 'default', - realm: makeIframeVatRealm('default', initializeMessageChannel), + worker: makeIframeVatWorker('default', initializeMessageChannel), }); const iframeReadyP = vat.init(); diff --git a/packages/kernel/.eslintrc.cjs b/packages/kernel/.eslintrc.cjs index c747e0b88..165e7042e 100644 --- a/packages/kernel/.eslintrc.cjs +++ b/packages/kernel/.eslintrc.cjs @@ -1,23 +1,3 @@ module.exports = { extends: ['../../.eslintrc.cjs'], - - overrides: [ - { - files: ['src/**/*.ts'], - globals: { - chrome: 'readonly', - clients: 'readonly', - Compartment: 'readonly', - }, - }, - - { - files: ['vite.config.ts'], - parserOptions: { - sourceType: 'module', - tsconfigRootDir: __dirname, - project: ['./tsconfig.scripts.json'], - }, - }, - ], }; diff --git a/packages/kernel/package.json b/packages/kernel/package.json index 38a784af4..febd735c3 100644 --- a/packages/kernel/package.json +++ b/packages/kernel/package.json @@ -9,7 +9,16 @@ }, "type": "module", "exports": { - ".": "./src/index.ts", + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, "./package.json": "./package.json" }, "files": [ diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 1899b14ed..10be2b733 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -38,8 +38,8 @@ export class Kernel { * @param id - The ID of the vat. */ public async deleteVat(id: string): Promise { - const vat = this.#vats.get(id); - await vat?.terminate(); + const vat = this.#getVat(id); + await vat.terminate(); this.#vats.delete(id); } diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index 6974a53e7..a2a1b8f1e 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -18,18 +18,18 @@ import { makeStreamEnvelopeHandler, } from '@ocap/streams'; -import type { UnresolvedMessages, VatRealm } from './types.js'; +import type { UnresolvedMessages, VatWorker } from './types.js'; import { makeCounter } from './utils/makeCounter.js'; export type VatProps = { id: string; - realm: VatRealm; + worker: VatWorker; }; export class Vat { readonly id: VatProps['id']; - readonly #realm: VatProps['realm']; + readonly #worker: VatProps['worker']; readonly #messageCounter: () => number; @@ -41,9 +41,9 @@ export class Vat { capTp?: ReturnType; - constructor({ id, realm }: VatProps) { + constructor({ id, worker }: VatProps) { this.id = id; - this.#realm = realm; + this.#worker = worker; this.#messageCounter = makeCounter(); } @@ -53,7 +53,7 @@ export class Vat { * @returns A promise that resolves when the vat is initialized. */ async init(): Promise { - const [streams] = await this.#realm.init(); + const [streams] = await this.#worker.init(); this.streams = streams; this.streamEnvelopeHandler = makeStreamEnvelopeHandler( { @@ -157,7 +157,13 @@ export class Vat { * Terminates the vat. */ async terminate(): Promise { - await this.streams?.return(); + if (!this.streams) { + throw new Error( + `Vat with id "${this.id}" does not have a stream connection.`, + ); + } + + await this.streams.return(); // Handle orphaned messages for (const [messageId, promiseCallback] of this.unresolvedMessages) { @@ -165,7 +171,7 @@ export class Vat { this.unresolvedMessages.delete(messageId); } - await this.#realm.delete(); + await this.#worker.delete(); } /** diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts index 1b0743d01..4985294f7 100644 --- a/packages/kernel/src/index.ts +++ b/packages/kernel/src/index.ts @@ -1,3 +1,3 @@ export { Kernel } from './Kernel.js'; export { Vat } from './Vat.js'; -export type { VatId, VatRealm } from './types.js'; +export type { VatId, VatWorker } from './types.js'; diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 348835a69..5b8d304f6 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -3,7 +3,7 @@ import type { StreamPair, MessageId, StreamEnvelope } from '@ocap/streams'; export type VatId = string; -export type VatRealm = { +export type VatWorker = { init: () => Promise<[StreamPair, unknown]>; delete: () => Promise; }; diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json index 1f81a3920..6f1d89de4 100644 --- a/packages/kernel/tsconfig.json +++ b/packages/kernel/tsconfig.json @@ -1,12 +1,8 @@ { "extends": "../../tsconfig.packages.json", "compilerOptions": { - "baseUrl": "./", - "allowImportingTsExtensions": true, - "emitDeclarationOnly": true, - "allowJs": true, - "checkJs": true + "baseUrl": "./" }, "references": [], - "include": ["./src", "../streams/src/type-guards.ts"] + "include": ["./src"] } diff --git a/packages/streams/tsconfig.json b/packages/streams/tsconfig.json index f719ece8b..89a01da45 100644 --- a/packages/streams/tsconfig.json +++ b/packages/streams/tsconfig.json @@ -4,8 +4,6 @@ "baseUrl": "./", "lib": ["DOM", "ES2022"], "types": ["ses", "vitest", "vitest/jsdom"], - "allowImportingTsExtensions": true, - "emitDeclarationOnly": true, "allowJs": true, "checkJs": true }, From 4dfef8dc6bc4cd4f660a2bbafc8145c1202f5e4c Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 15:18:52 +0100 Subject: [PATCH 05/19] fix stream tests --- packages/streams/src/index.test.ts | 10 +- packages/streams/src/stream-envelope.test.ts | 1 - packages/streams/src/type-guards.test.ts | 102 +++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 packages/streams/src/type-guards.test.ts diff --git a/packages/streams/src/index.test.ts b/packages/streams/src/index.test.ts index 3ddc0dee0..625957abd 100644 --- a/packages/streams/src/index.test.ts +++ b/packages/streams/src/index.test.ts @@ -6,11 +6,15 @@ describe('index', () => { it('has the expected exports', () => { expect(Object.keys(indexModule)).toStrictEqual( expect.arrayContaining([ - 'makeMessagePortStreamPair', - 'MessagePortReader', - 'MessagePortWriter', 'initializeMessageChannel', 'receiveMessagePort', + 'makeMessagePortStreamPair', + 'makeStreamEnvelopeKit', + 'KernelMessageTarget', + 'Command', + 'wrapStreamCommand', + 'wrapCapTp', + 'makeStreamEnvelopeHandler', ]), ); }); diff --git a/packages/streams/src/stream-envelope.test.ts b/packages/streams/src/stream-envelope.test.ts index e93aab36d..50256a55f 100644 --- a/packages/streams/src/stream-envelope.test.ts +++ b/packages/streams/src/stream-envelope.test.ts @@ -1,4 +1,3 @@ -import '@ocap/shims/endoify'; import { describe, it, expect } from 'vitest'; import { diff --git a/packages/streams/src/type-guards.test.ts b/packages/streams/src/type-guards.test.ts new file mode 100644 index 000000000..8e871b5b2 --- /dev/null +++ b/packages/streams/src/type-guards.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect } from 'vitest'; + +import { isWrappedVatMessage, isCapTpMessage } from './type-guards.js'; +import type { CapTpMessage, WrappedVatMessage } from './types.js'; +import { Command } from './types.js'; + +describe('type-guards', () => { + describe('isWrappedVatMessage', () => { + it('returns true for a valid wrapped vat message', () => { + const value: WrappedVatMessage = { + id: 'some-id', + message: { + type: Command.Ping, + data: null, + }, + }; + expect(isWrappedVatMessage(value)).toBe(true); + }); + + it('returns false for an invalid wrapped vat message', () => { + const value1 = 123; + const value2 = { id: true, message: {} }; + const value3 = { id: 'some-id', message: null }; + + expect(isWrappedVatMessage(value1)).toBe(false); + expect(isWrappedVatMessage(value2)).toBe(false); + expect(isWrappedVatMessage(value3)).toBe(false); + }); + + it('returns false for a wrapped vat message with invalid id', () => { + const value = { + id: 123, + message: { + type: Command.Ping, + data: null, + }, + }; + expect(isWrappedVatMessage(value)).toBe(false); + }); + + it('returns false for a wrapped vat message with invalid message', () => { + const value1 = { id: 'some-id' }; + const value2 = { id: 'some-id', message: 123 }; + + expect(isWrappedVatMessage(value1)).toBe(false); + expect(isWrappedVatMessage(value2)).toBe(false); + }); + + it('returns false for a wrapped vat message with invalid type in the message', () => { + const value = { + id: 'some-id', + message: { + type: 123, + data: null, + }, + }; + expect(isWrappedVatMessage(value)).toBe(false); + }); + + it('returns false for a wrapped vat message with invalid data in the message', () => { + const value1 = { id: 'some-id' }; + const value2 = { id: 'some-id', message: null }; + + expect(isWrappedVatMessage(value1)).toBe(false); + expect(isWrappedVatMessage(value2)).toBe(false); + }); + }); + + describe('isCapTpMessage', () => { + it('returns true for a valid cap tp message', () => { + const value: CapTpMessage = { + type: 'CTP_some-type', + epoch: 123, + }; + expect(isCapTpMessage(value)).toBe(true); + }); + + it('returns false for an invalid cap tp message', () => { + const value1 = { type: true, epoch: null }; + const value2 = { type: 'some-type' }; + + expect(isCapTpMessage(value1)).toBe(false); + expect(isCapTpMessage(value2)).toBe(false); + }); + + it('returns false for a cap tp message with invalid type', () => { + const value = { + type: 123, + epoch: null, + }; + expect(isCapTpMessage(value)).toBe(false); + }); + + it('returns false for a cap tp message with invalid epoch', () => { + const value1 = { type: 'CTP_some-type' }; + const value2 = { type: 'CTP_some-type', epoch: true }; + + expect(isCapTpMessage(value1)).toBe(false); + expect(isCapTpMessage(value2)).toBe(false); + }); + }); +}); From eae41e56c0f5420d5b9c6c114cae2273f7f6465d Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 15:47:22 +0100 Subject: [PATCH 06/19] add kernel tests --- packages/kernel/src/Kernel.test.ts | 112 +++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 packages/kernel/src/Kernel.test.ts diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts new file mode 100644 index 000000000..603a9ecd1 --- /dev/null +++ b/packages/kernel/src/Kernel.test.ts @@ -0,0 +1,112 @@ +import type { VatMessage } from '@ocap/streams'; +import { describe, it, expect, vi } from 'vitest'; + +import { Kernel } from './Kernel.js'; +import type { Vat } from './Vat.js'; + +describe('Kernel', () => { + describe('#getVatIDs()', () => { + it('returns an empty array when no vats are added', () => { + const kernel = new Kernel(); + expect(kernel.getVatIDs()).toStrictEqual([]); + }); + + it('returns the vat IDs after adding a vat', () => { + const kernel = new Kernel(); + kernel.addVat({ id: 'vat-id-1' } as Vat); + expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1']); + }); + + it('returns multiple vat IDs after adding multiple vats', () => { + const kernel = new Kernel(); + kernel.addVat({ id: 'vat-id-1' } as Vat); + kernel.addVat({ id: 'vat-id-2' } as Vat); + expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1', 'vat-id-2']); + }); + }); + + describe('#addVat()', () => { + it('adds a vat to the kernel without errors when no vat with the same ID exists', () => { + const kernel = new Kernel(); + kernel.addVat({ id: 'vat-id' } as Vat); + expect(kernel.getVatIDs()).toStrictEqual(['vat-id']); + }); + + it('throws an error when adding a vat that already exists in the kernel', () => { + const kernel = new Kernel(); + kernel.addVat({ id: 'vat-id-1' } as Vat); + expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1']); + expect(() => kernel.addVat({ id: 'vat-id-1' } as Vat)).toThrow( + 'Vat with ID vat-id-1 already exists.', + ); + expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1']); + }); + }); + + describe('#deleteVat()', () => { + it('deletes a vat from the kernel without errors when the vat exists', async () => { + const kernel = new Kernel(); + kernel.addVat({ id: 'vat-id', terminate: vi.fn() } as unknown as Vat); + expect(kernel.getVatIDs()).toStrictEqual(['vat-id']); + await kernel.deleteVat('vat-id'); + expect(kernel.getVatIDs()).toStrictEqual([]); + }); + + it('throws an error when deleting a vat that does not exist in the kernel', async () => { + const kernel = new Kernel(); + await expect(async () => + kernel.deleteVat('non-existent-vat-id'), + ).rejects.toThrow('Vat with ID non-existent-vat-id does not exist.'); + }); + + it('throws an error when a vat terminate method throws', async () => { + const kernel = new Kernel(); + kernel.addVat({ + id: 'vat-id', + terminate: () => { + throw new Error('Test error'); + }, + } as unknown as Vat); + await expect(async () => kernel.deleteVat('vat-id')).rejects.toThrow( + 'Test error', + ); + }); + }); + + describe('#sendMessage()', () => { + it('sends a message to the vat without errors when the vat exists', async () => { + const kernel = new Kernel(); + kernel.addVat({ + id: 'vat-id', + sendMessage: async (prop) => Promise.resolve(prop), + } as Vat); + expect( + await kernel.sendMessage('vat-id', 'test' as unknown as VatMessage), + ).toBe('test'); + }); + + it('throws an error when sending a message to the vat that does not exist in the kernel', async () => { + const kernel = new Kernel(); + await expect(async () => + kernel.sendMessage('non-existent-vat-id', {} as VatMessage), + ).rejects.toThrow('Vat with ID non-existent-vat-id does not exist.'); + }); + + it('throws an error when sending a message to the vat throws', async () => { + const kernel = new Kernel(); + kernel.addVat({ + id: 'vat-id', + sendMessage: async () => Promise.reject(new Error('Test error')), + } as unknown as Vat); + await expect(async () => + kernel.sendMessage('vat-id', {} as VatMessage), + ).rejects.toThrow('Test error'); + }); + }); + + describe('#constructor()', () => { + it('initializes the kernel without errors', () => { + expect(async () => new Kernel()).not.toThrow(); + }); + }); +}); From be067badc4983c78544cc5d8d08c64e2ec0e5ed4 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 15:54:42 +0100 Subject: [PATCH 07/19] add index tests --- packages/kernel/src/index.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 packages/kernel/src/index.test.ts diff --git a/packages/kernel/src/index.test.ts b/packages/kernel/src/index.test.ts new file mode 100644 index 000000000..f4ef29a61 --- /dev/null +++ b/packages/kernel/src/index.test.ts @@ -0,0 +1,11 @@ +import { describe, it, expect } from 'vitest'; + +import * as indexModule from './index.js'; + +describe('index', () => { + it('has the expected exports', () => { + expect(Object.keys(indexModule)).toStrictEqual( + expect.arrayContaining(['Kernel', 'Vat']), + ); + }); +}); From 8a6eda643ccd88a7309485002dee4ed024896405 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 19:28:03 +0100 Subject: [PATCH 08/19] CHanged addVat to launchVat --- packages/extension/src/offscreen.ts | 17 +++--- packages/kernel/src/Kernel.test.ts | 94 +++++++++++++++++------------ packages/kernel/src/Kernel.ts | 23 ++++--- packages/kernel/src/Vat.ts | 62 +++++++++---------- packages/kernel/src/types.ts | 5 ++ 5 files changed, 109 insertions(+), 92 deletions(-) diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 61d897b74..e904a2d79 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,4 +1,4 @@ -import { Kernel, Vat } from '@ocap/kernel'; +import { Kernel } from '@ocap/kernel'; import { initializeMessageChannel, Command, @@ -16,14 +16,10 @@ main().catch(console.error); */ async function main(): Promise { const kernel = new Kernel(); - - const vat = new Vat({ + const iframeReadyP = kernel.launchVat({ id: 'default', worker: makeIframeVatWorker('default', initializeMessageChannel), }); - const iframeReadyP = vat.init(); - - kernel.addVat(vat); // Handle messages from the background service worker chrome.runtime.onMessage.addListener( @@ -35,11 +31,11 @@ async function main(): Promise { return; } - await iframeReadyP; + const vat = await iframeReadyP; switch (message.type) { case Command.Evaluate: - await reply(Command.Evaluate, await evaluate(message.data)); + await reply(Command.Evaluate, await evaluate(vat.id, message.data)); break; case Command.CapTpCall: { const result = await vat.callCapTp(message.data); @@ -80,12 +76,13 @@ async function main(): Promise { /** * Evaluate a string in the default iframe. * + * @param vatId - The ID of the vat to send the message to. * @param source - The source string to evaluate. * @returns The result of the evaluation, or an error message. */ - async function evaluate(source: string): Promise { + async function evaluate(vatId: string, source: string): Promise { try { - const result = await kernel.sendMessage(vat.id, { + const result = await kernel.sendMessage(vatId, { type: Command.Evaluate, data: source, }); diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 603a9ecd1..13dec7796 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -1,55 +1,76 @@ import type { VatMessage } from '@ocap/streams'; -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Kernel } from './Kernel.js'; -import type { Vat } from './Vat.js'; +import type { VatLaunchProps, VatWorker } from './types.js'; +import { Vat } from './Vat.js'; describe('Kernel', () => { - describe('#getVatIDs()', () => { + let mockWorker: VatWorker; + let initMock: unknown; + let terminateMock: unknown; + beforeEach(() => { + // Reset all mocks before each test + vi.resetAllMocks(); + mockWorker = { + init: vi.fn().mockResolvedValue([{}]), + delete: vi.fn(), + }; + + initMock = vi.spyOn(Vat.prototype, 'init').mockImplementation(vi.fn()); + terminateMock = vi + .spyOn(Vat.prototype, 'terminate') + .mockImplementation(vi.fn()); + }); + + describe('#getVatIds()', () => { it('returns an empty array when no vats are added', () => { const kernel = new Kernel(); - expect(kernel.getVatIDs()).toStrictEqual([]); + expect(kernel.getVatIds()).toStrictEqual([]); }); - it('returns the vat IDs after adding a vat', () => { + it('returns the vat IDs after adding a vat', async () => { const kernel = new Kernel(); - kernel.addVat({ id: 'vat-id-1' } as Vat); - expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1']); + await kernel.launchVat({ id: 'vat-id-1', worker: mockWorker }); + expect(kernel.getVatIds()).toStrictEqual(['vat-id-1']); }); - it('returns multiple vat IDs after adding multiple vats', () => { + it('returns multiple vat IDs after adding multiple vats', async () => { const kernel = new Kernel(); - kernel.addVat({ id: 'vat-id-1' } as Vat); - kernel.addVat({ id: 'vat-id-2' } as Vat); - expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1', 'vat-id-2']); + await kernel.launchVat({ id: 'vat-id-1', worker: mockWorker }); + await kernel.launchVat({ id: 'vat-id-2', worker: mockWorker }); + expect(kernel.getVatIds()).toStrictEqual(['vat-id-1', 'vat-id-2']); }); }); - describe('#addVat()', () => { - it('adds a vat to the kernel without errors when no vat with the same ID exists', () => { + describe('#launchVat()', () => { + it('adds a vat to the kernel without errors when no vat with the same ID exists', async () => { const kernel = new Kernel(); - kernel.addVat({ id: 'vat-id' } as Vat); - expect(kernel.getVatIDs()).toStrictEqual(['vat-id']); + await kernel.launchVat({ id: 'vat-id-1', worker: mockWorker }); + expect(initMock).toHaveBeenCalledOnce(); + expect(mockWorker.init).toHaveBeenCalled(); + expect(kernel.getVatIds()).toStrictEqual(['vat-id-1']); }); - it('throws an error when adding a vat that already exists in the kernel', () => { + it('throws an error when launching a vat that already exists in the kernel', async () => { const kernel = new Kernel(); - kernel.addVat({ id: 'vat-id-1' } as Vat); - expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1']); - expect(() => kernel.addVat({ id: 'vat-id-1' } as Vat)).toThrow( - 'Vat with ID vat-id-1 already exists.', - ); - expect(kernel.getVatIDs()).toStrictEqual(['vat-id-1']); + await kernel.launchVat({ id: 'vat-id-1', worker: mockWorker }); + expect(kernel.getVatIds()).toStrictEqual(['vat-id-1']); + await expect( + kernel.launchVat({ id: 'vat-id-1' } as VatLaunchProps), + ).rejects.toThrow('Vat with ID vat-id-1 already exists.'); + expect(kernel.getVatIds()).toStrictEqual(['vat-id-1']); }); }); describe('#deleteVat()', () => { it('deletes a vat from the kernel without errors when the vat exists', async () => { const kernel = new Kernel(); - kernel.addVat({ id: 'vat-id', terminate: vi.fn() } as unknown as Vat); - expect(kernel.getVatIDs()).toStrictEqual(['vat-id']); + await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); + expect(kernel.getVatIds()).toStrictEqual(['vat-id']); await kernel.deleteVat('vat-id'); - expect(kernel.getVatIDs()).toStrictEqual([]); + expect(terminateMock).toHaveBeenCalledOnce(); + expect(kernel.getVatIds()).toStrictEqual([]); }); it('throws an error when deleting a vat that does not exist in the kernel', async () => { @@ -57,16 +78,13 @@ describe('Kernel', () => { await expect(async () => kernel.deleteVat('non-existent-vat-id'), ).rejects.toThrow('Vat with ID non-existent-vat-id does not exist.'); + expect(terminateMock).not.toHaveBeenCalled(); }); it('throws an error when a vat terminate method throws', async () => { const kernel = new Kernel(); - kernel.addVat({ - id: 'vat-id', - terminate: () => { - throw new Error('Test error'); - }, - } as unknown as Vat); + await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); + vi.spyOn(Vat.prototype, 'terminate').mockRejectedValueOnce('Test error'); await expect(async () => kernel.deleteVat('vat-id')).rejects.toThrow( 'Test error', ); @@ -76,10 +94,8 @@ describe('Kernel', () => { describe('#sendMessage()', () => { it('sends a message to the vat without errors when the vat exists', async () => { const kernel = new Kernel(); - kernel.addVat({ - id: 'vat-id', - sendMessage: async (prop) => Promise.resolve(prop), - } as Vat); + await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); + vi.spyOn(Vat.prototype, 'sendMessage').mockResolvedValueOnce('test'); expect( await kernel.sendMessage('vat-id', 'test' as unknown as VatMessage), ).toBe('test'); @@ -94,13 +110,11 @@ describe('Kernel', () => { it('throws an error when sending a message to the vat throws', async () => { const kernel = new Kernel(); - kernel.addVat({ - id: 'vat-id', - sendMessage: async () => Promise.reject(new Error('Test error')), - } as unknown as Vat); + await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); + vi.spyOn(Vat.prototype, 'sendMessage').mockRejectedValueOnce('error'); await expect(async () => kernel.sendMessage('vat-id', {} as VatMessage), - ).rejects.toThrow('Test error'); + ).rejects.toThrow('error'); }); }); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 10be2b733..2b5bb2178 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -1,8 +1,8 @@ import '@ocap/shims/endoify'; import type { VatMessage } from '@ocap/streams'; -import type { VatId } from './types.js'; -import type { Vat } from './Vat.js'; +import type { VatId, VatLaunchProps } from './types.js'; +import { Vat } from './Vat.js'; export class Kernel { readonly #vats: Map; @@ -16,20 +16,27 @@ export class Kernel { * * @returns An array of vat IDs. */ - public getVatIDs(): VatId[] { + public getVatIds(): VatId[] { return Array.from(this.#vats.keys()); } /** - * Adds a vat to the kernel. + * Launches a vat in the kernel. * - * @param vat - The vat record. + * @param options - The options for launching the vat. + * @param options.id - The ID of the vat. + * @param options.worker - The worker to use for the vat. + * @returns A promise that resolves the vat. */ - public addVat(vat: Vat): void { - if (this.#vats.has(vat.id)) { - throw new Error(`Vat with ID ${vat.id} already exists.`); + public async launchVat({ id, worker }: VatLaunchProps): Promise { + if (this.#vats.has(id)) { + throw new Error(`Vat with ID ${id} already exists.`); } + const [streams] = await worker.init(); + const vat = new Vat({ id, streams, deleteWorker: worker.delete }); this.#vats.set(vat.id, vat); + await vat.init(); + return vat; } /** diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index a2a1b8f1e..8a1b81620 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -18,32 +18,34 @@ import { makeStreamEnvelopeHandler, } from '@ocap/streams'; -import type { UnresolvedMessages, VatWorker } from './types.js'; +import type { UnresolvedMessages, VatLaunchProps } from './types.js'; import { makeCounter } from './utils/makeCounter.js'; -export type VatProps = { - id: string; - worker: VatWorker; +type VatConstructorProps = { + id: VatLaunchProps['id']; + streams: StreamPair; + deleteWorker: VatLaunchProps['worker']['delete']; }; export class Vat { - readonly id: VatProps['id']; + readonly id: VatConstructorProps['id']; - readonly #worker: VatProps['worker']; + readonly #streams: VatConstructorProps['streams']; + + readonly #deleteWorker: VatConstructorProps['deleteWorker']; readonly #messageCounter: () => number; readonly unresolvedMessages: UnresolvedMessages = new Map(); - streams?: StreamPair; - - streamEnvelopeHandler?: StreamEnvelopeHandler; + #streamEnvelopeHandler?: StreamEnvelopeHandler; - capTp?: ReturnType; + #capTp?: ReturnType; - constructor({ id, worker }: VatProps) { + constructor({ id, streams, deleteWorker }: VatConstructorProps) { this.id = id; - this.#worker = worker; + this.#streams = streams; + this.#deleteWorker = deleteWorker; this.#messageCounter = makeCounter(); } @@ -53,9 +55,7 @@ export class Vat { * @returns A promise that resolves when the vat is initialized. */ async init(): Promise { - const [streams] = await this.#worker.init(); - this.streams = streams; - this.streamEnvelopeHandler = makeStreamEnvelopeHandler( + this.#streamEnvelopeHandler = makeStreamEnvelopeHandler( { command: async ({ id, message }) => { const promiseCallbacks = this.unresolvedMessages.get(id); @@ -71,7 +71,7 @@ export class Vat { ); /* v8 ignore next 4: Not known to be possible. */ - this.#receiveMessages(this.streams.reader).catch((error) => { + this.#receiveMessages(this.#streams.reader).catch((error) => { console.error(`Unexpected read error from vat "${this.id}"`, error); throw error; }); @@ -90,7 +90,7 @@ export class Vat { async #receiveMessages(reader: Reader): Promise { for await (const rawMessage of reader) { console.debug('Offscreen received message', rawMessage); - await this.streamEnvelopeHandler?.handle(rawMessage); + await this.#streamEnvelopeHandler?.handle(rawMessage); } } @@ -100,26 +100,20 @@ export class Vat { * @returns A promise that resolves when the CapTP connection is made. */ async makeCapTp(): Promise { - if (this.capTp !== undefined) { + if (this.#capTp !== undefined) { throw new Error( `Vat with id "${this.id}" already has a CapTP connection.`, ); } - if (!this.streams) { - throw new Error( - `Vat with id "${this.id}" does not have a stream connection.`, - ); - } - - if (!this.streamEnvelopeHandler) { + if (!this.#streamEnvelopeHandler) { throw new Error( `Vat with id "${this.id}" does not have a stream envelope handler.`, ); } // Handle writes here. #receiveMessages() handles reads. - const { writer } = this.streams; + const { writer } = this.#streams; // https://github.com/endojs/endo/issues/2412 // eslint-disable-next-line @typescript-eslint/no-misused-promises const ctp = makeCapTP(this.id, async (content: unknown) => { @@ -127,8 +121,8 @@ export class Vat { await writer.next(wrapCapTp(content as CapTpMessage)); }); - this.capTp = ctp; - this.streamEnvelopeHandler.contentHandlers.capTp = async ( + this.#capTp = ctp; + this.#streamEnvelopeHandler.contentHandlers.capTp = async ( content: CapTpMessage, ) => { console.log('CapTP from vat', JSON.stringify(content, null, 2)); @@ -145,25 +139,25 @@ export class Vat { * @returns A promise that resolves the result of the CapTP call. */ async callCapTp(payload: CapTpPayload): Promise { - if (!this.capTp) { + if (!this.#capTp) { throw new Error( `Vat with id "${this.id}" does not have a CapTP connection.`, ); } - return E(this.capTp.getBootstrap())[payload.method](...payload.params); + return E(this.#capTp.getBootstrap())[payload.method](...payload.params); } /** * Terminates the vat. */ async terminate(): Promise { - if (!this.streams) { + if (!this.#streams) { throw new Error( `Vat with id "${this.id}" does not have a stream connection.`, ); } - await this.streams.return(); + await this.#streams.return(); // Handle orphaned messages for (const [messageId, promiseCallback] of this.unresolvedMessages) { @@ -171,7 +165,7 @@ export class Vat { this.unresolvedMessages.delete(messageId); } - await this.#worker.delete(); + await this.#deleteWorker(); } /** @@ -184,7 +178,7 @@ export class Vat { const { promise, reject, resolve } = makePromiseKit(); const messageId = this.#nextMessageId(); this.unresolvedMessages.set(messageId, { reject, resolve }); - await this.streams?.writer.next( + await this.#streams?.writer.next( wrapStreamCommand({ id: messageId, message }), ); return promise; diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 5b8d304f6..42be17531 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -11,3 +11,8 @@ export type VatWorker = { export type PromiseCallbacks = Omit, 'promise'>; export type UnresolvedMessages = Map; + +export type VatLaunchProps = { + id: VatId; + worker: VatWorker; +}; From 4d962a1b8d5307978d892aac2c007b0d844ce66f Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 19:28:19 +0100 Subject: [PATCH 09/19] cleanup --- packages/kernel/src/Kernel.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 13dec7796..4dbe70b48 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -10,8 +10,8 @@ describe('Kernel', () => { let initMock: unknown; let terminateMock: unknown; beforeEach(() => { - // Reset all mocks before each test vi.resetAllMocks(); + mockWorker = { init: vi.fn().mockResolvedValue([{}]), delete: vi.fn(), From f1356f9b96f3d901d331fbdceb8e8846326e5ada Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 19:28:34 +0100 Subject: [PATCH 10/19] add some space on test --- packages/kernel/src/Kernel.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 4dbe70b48..e45c53a5f 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -9,6 +9,7 @@ describe('Kernel', () => { let mockWorker: VatWorker; let initMock: unknown; let terminateMock: unknown; + beforeEach(() => { vi.resetAllMocks(); From 84e3c7f4ee971401e1c06edbd1ea6e9fa4668324 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 19:32:31 +0100 Subject: [PATCH 11/19] remove unsused --- packages/streams/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/streams/package.json b/packages/streams/package.json index b459b39a1..6ba009be2 100644 --- a/packages/streams/package.json +++ b/packages/streams/package.json @@ -49,8 +49,7 @@ "@endo/captp": "^4.2.2", "@endo/promise-kit": "^1.1.4", "@endo/stream": "^1.2.2", - "@metamask/utils": "^9.1.0", - "@ocap/shims": "workspace:^" + "@metamask/utils": "^9.1.0" }, "devDependencies": { "@arethetypeswrong/cli": "^0.15.3", From b0c0c9ab9a56b84c0e39a016a50130aa078a5fb4 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 19:37:16 +0100 Subject: [PATCH 12/19] make type guard tests parameterized --- packages/streams/src/type-guards.test.ts | 120 ++++++----------------- 1 file changed, 30 insertions(+), 90 deletions(-) diff --git a/packages/streams/src/type-guards.test.ts b/packages/streams/src/type-guards.test.ts index 8e871b5b2..96175bde1 100644 --- a/packages/streams/src/type-guards.test.ts +++ b/packages/streams/src/type-guards.test.ts @@ -1,102 +1,42 @@ import { describe, it, expect } from 'vitest'; import { isWrappedVatMessage, isCapTpMessage } from './type-guards.js'; -import type { CapTpMessage, WrappedVatMessage } from './types.js'; import { Command } from './types.js'; describe('type-guards', () => { describe('isWrappedVatMessage', () => { - it('returns true for a valid wrapped vat message', () => { - const value: WrappedVatMessage = { - id: 'some-id', - message: { - type: Command.Ping, - data: null, - }, - }; - expect(isWrappedVatMessage(value)).toBe(true); - }); - - it('returns false for an invalid wrapped vat message', () => { - const value1 = 123; - const value2 = { id: true, message: {} }; - const value3 = { id: 'some-id', message: null }; - - expect(isWrappedVatMessage(value1)).toBe(false); - expect(isWrappedVatMessage(value2)).toBe(false); - expect(isWrappedVatMessage(value3)).toBe(false); - }); - - it('returns false for a wrapped vat message with invalid id', () => { - const value = { - id: 123, - message: { - type: Command.Ping, - data: null, - }, - }; - expect(isWrappedVatMessage(value)).toBe(false); - }); - - it('returns false for a wrapped vat message with invalid message', () => { - const value1 = { id: 'some-id' }; - const value2 = { id: 'some-id', message: 123 }; - - expect(isWrappedVatMessage(value1)).toBe(false); - expect(isWrappedVatMessage(value2)).toBe(false); - }); - - it('returns false for a wrapped vat message with invalid type in the message', () => { - const value = { - id: 'some-id', - message: { - type: 123, - data: null, - }, - }; - expect(isWrappedVatMessage(value)).toBe(false); - }); - - it('returns false for a wrapped vat message with invalid data in the message', () => { - const value1 = { id: 'some-id' }; - const value2 = { id: 'some-id', message: null }; - - expect(isWrappedVatMessage(value1)).toBe(false); - expect(isWrappedVatMessage(value2)).toBe(false); - }); + it.each` + value | expectedResult | description + ${{ id: 'some-id', message: { type: Command.Ping, data: null } }} | ${true} | ${'valid wrapped vat message'} + ${123} | ${false} | ${'invalid wrapped vat message: primitive number'} + ${{ id: true, message: {} }} | ${false} | ${'invalid wrapped vat message: invalid id and empty message'} + ${{ id: 'some-id', message: null }} | ${false} | ${'invalid wrapped vat message: message is null'} + ${{ id: 123, message: { type: Command.Ping, data: null } }} | ${false} | ${'invalid wrapped vat message: invalid id type'} + ${{ id: 'some-id' }} | ${false} | ${'invalid wrapped vat message: missing message'} + ${{ id: 'some-id', message: 123 }} | ${false} | ${'invalid wrapped vat message: message is a primitive number'} + ${{ id: 'some-id', message: { type: 123, data: null } }} | ${false} | ${'invalid wrapped vat message: invalid type in message'} + `( + 'returns $expectedResult for $description', + ({ value, expectedResult }) => { + expect(isWrappedVatMessage(value)).toBe(expectedResult); + }, + ); }); describe('isCapTpMessage', () => { - it('returns true for a valid cap tp message', () => { - const value: CapTpMessage = { - type: 'CTP_some-type', - epoch: 123, - }; - expect(isCapTpMessage(value)).toBe(true); - }); - - it('returns false for an invalid cap tp message', () => { - const value1 = { type: true, epoch: null }; - const value2 = { type: 'some-type' }; - - expect(isCapTpMessage(value1)).toBe(false); - expect(isCapTpMessage(value2)).toBe(false); - }); - - it('returns false for a cap tp message with invalid type', () => { - const value = { - type: 123, - epoch: null, - }; - expect(isCapTpMessage(value)).toBe(false); - }); - - it('returns false for a cap tp message with invalid epoch', () => { - const value1 = { type: 'CTP_some-type' }; - const value2 = { type: 'CTP_some-type', epoch: true }; - - expect(isCapTpMessage(value1)).toBe(false); - expect(isCapTpMessage(value2)).toBe(false); - }); + it.each` + value | expectedResult | description + ${{ type: 'CTP_some-type', epoch: 123 }} | ${true} | ${'valid cap tp message'} + ${{ type: true, epoch: null }} | ${false} | ${'invalid cap tp message: invalid type and epoch'} + ${{ type: 'some-type' }} | ${false} | ${'invalid cap tp message: missing epoch'} + ${{ type: 123, epoch: null }} | ${false} | ${'invalid cap tp message: invalid type'} + ${{ type: 'CTP_some-type' }} | ${false} | ${'invalid cap tp message: missing epoch'} + ${{ type: 'CTP_some-type', epoch: true }} | ${false} | ${'invalid cap tp message: invalid epoch type'} + `( + 'returns $expectedResult for $description', + ({ value, expectedResult }) => { + expect(isCapTpMessage(value)).toBe(expectedResult); + }, + ); }); }); From 85ca274ba0a649867bcd7e62b4a4a4f747b3c2a1 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Fri, 20 Sep 2024 23:36:41 +0100 Subject: [PATCH 13/19] add Vat tests --- packages/kernel/package.json | 1 + packages/kernel/src/Vat.test.ts | 167 ++++++++++++++++++++++++++++++++ packages/kernel/src/Vat.ts | 27 +++--- yarn.lock | 2 +- 4 files changed, 180 insertions(+), 17 deletions(-) create mode 100644 packages/kernel/src/Vat.test.ts diff --git a/packages/kernel/package.json b/packages/kernel/package.json index febd735c3..32d2d4245 100644 --- a/packages/kernel/package.json +++ b/packages/kernel/package.json @@ -54,6 +54,7 @@ "@metamask/eslint-config": "^13.0.0", "@metamask/eslint-config-nodejs": "^13.0.0", "@metamask/eslint-config-typescript": "^13.0.0", + "@ocap/test-utils": "workspace:^", "@ts-bridge/cli": "^0.5.1", "@ts-bridge/shims": "^0.1.1", "@typescript-eslint/eslint-plugin": "^8.1.0", diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts new file mode 100644 index 000000000..822897619 --- /dev/null +++ b/packages/kernel/src/Vat.test.ts @@ -0,0 +1,167 @@ +import '@ocap/shims/endoify'; +import type { StreamPair, StreamEnvelope, VatMessage } from '@ocap/streams'; +import { makePromiseKitMock } from '@ocap/test-utils'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { Vat } from './Vat.js'; + +vi.mock('@endo/promise-kit', () => makePromiseKitMock()); + +vi.mock('@endo/captp', () => { + return { + makeCapTP: vi.fn(() => ({ + getBootstrap: vi.fn(() => ({ + testMethod: vi.fn().mockResolvedValue('test-result'), + })), + })), + }; +}); + +describe('Vat', () => { + let mockStreams: StreamPair; + let mockDeleteWorker: () => Promise; + let vat: Vat; + + beforeEach(() => { + vi.resetAllMocks(); + + // Mock the streams + mockStreams = { + reader: { + // @ts-expect-error We are mocking the async iterator + async *[Symbol.asyncIterator]() { + yield { + label: 'test-label', + message: {} as VatMessage, + }; + }, + return: vi.fn().mockResolvedValue(undefined), + throw: vi.fn(), + }, + writer: { + next: vi.fn().mockResolvedValue({ done: false }), + return: vi.fn().mockResolvedValue(undefined), + throw: vi.fn(), + // @ts-expect-error We are mocking the async iterator + async *[Symbol.asyncIterator]() { + yield {}; + }, + }, + return: vi.fn().mockResolvedValue(undefined), + throw: vi.fn().mockResolvedValue(undefined), + }; + + // Mock the delete worker function + mockDeleteWorker = vi.fn().mockResolvedValue(undefined); + + // Create a new instance of the Vat class + vat = new Vat({ + id: 'test-vat', + streams: mockStreams, + deleteWorker: mockDeleteWorker, + }); + }); + + describe('#init', () => { + it('initializes the vat and sends a ping message', async () => { + const sendMessageMock = vi + .spyOn(vat, 'sendMessage') + .mockResolvedValueOnce(undefined); + const makeCapTpMock = vi + .spyOn(vat, 'makeCapTp') + .mockResolvedValueOnce(undefined); + + await vat.init(); + + expect(sendMessageMock).toHaveBeenCalledWith({ + type: 'ping', + data: null, + }); + expect(makeCapTpMock).toHaveBeenCalled(); + }); + }); + + describe('#sendMessage', () => { + it('sends a message and resolves the promise', async () => { + const mockMessage = { type: 'makeCapTp', data: null } as VatMessage; + const sendMessagePromise = vat.sendMessage(mockMessage); + vat.unresolvedMessages.get('test-vat-1')?.resolve('test-response'); + const result = await sendMessagePromise; + expect(result).toBe('test-response'); + }); + }); + + describe('#terminate', () => { + it('terminates the vat and resolves/rejects unresolved messages', async () => { + const mockMessageId = 'test-vat-1'; + const mockPromiseKit = makePromiseKitMock().makePromiseKit(); + const mockSpy = vi.spyOn(mockPromiseKit, 'reject'); + vat.unresolvedMessages.set(mockMessageId, mockPromiseKit); + await vat.terminate(); + expect(mockDeleteWorker).toHaveBeenCalled(); + expect(mockSpy).toHaveBeenCalledWith(expect.any(Error)); + }); + }); + + describe('#makeCapTp', () => { + it('throws an error if CapTP connection already exists', async () => { + // @ts-expect-error - Simulating an existing CapTP + vat.capTp = {}; + + await expect(vat.makeCapTp()).rejects.toThrow( + `Vat with id "${vat.id}" already has a CapTP connection.`, + ); + }); + + it('throws an error if stream envelope handler is not initialized', async () => { + // @ts-expect-error - Simulate the stream envelope handler not being set + vat.streamEnvelopeHandler = undefined; + + await expect(vat.makeCapTp()).rejects.toThrow( + `Vat with id "${vat.id}" does not have a stream envelope handler.`, + ); + }); + + it('creates a CapTP connection and sends CapTpInit message', async () => { + const streamEnvelopeHandlerMock = { + contentHandlers: { capTp: undefined }, + }; + // @ts-expect-error - Set the streamEnvelopeHandler in the vat instance + vat.streamEnvelopeHandler = streamEnvelopeHandlerMock; + + const sendMessageSpy = vi + .spyOn(vat, 'sendMessage') + .mockResolvedValue(undefined); + + vi.spyOn(mockStreams.writer, 'next').mockResolvedValue({ + done: false, + value: undefined, + }); + + await vat.makeCapTp(); + + expect(sendMessageSpy).toHaveBeenCalledWith({ + type: 'makeCapTp', + data: null, + }); + expect(streamEnvelopeHandlerMock.contentHandlers.capTp).toBeDefined(); + }); + }); + + describe('#callCapTp', () => { + it('throws an error if no CapTP connection exists', async () => { + // Ensure no CapTP connection exists in the vat instance + // @ts-expect-error - Simulate the CapTP connection not being set + vat.capTp = undefined; + + const payload = { + method: 'testMethod', + params: ['param1', 'param2'], + }; + + await expect(vat.callCapTp(payload)).rejects.toThrow( + `Vat with id "${vat.id}" does not have a CapTP connection.`, + ); + }); + }); +}); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index 8a1b81620..762015cee 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -38,9 +38,9 @@ export class Vat { readonly unresolvedMessages: UnresolvedMessages = new Map(); - #streamEnvelopeHandler?: StreamEnvelopeHandler; + streamEnvelopeHandler?: StreamEnvelopeHandler; - #capTp?: ReturnType; + capTp?: ReturnType; constructor({ id, streams, deleteWorker }: VatConstructorProps) { this.id = id; @@ -55,7 +55,7 @@ export class Vat { * @returns A promise that resolves when the vat is initialized. */ async init(): Promise { - this.#streamEnvelopeHandler = makeStreamEnvelopeHandler( + this.streamEnvelopeHandler = makeStreamEnvelopeHandler( { command: async ({ id, message }) => { const promiseCallbacks = this.unresolvedMessages.get(id); @@ -90,7 +90,7 @@ export class Vat { async #receiveMessages(reader: Reader): Promise { for await (const rawMessage of reader) { console.debug('Offscreen received message', rawMessage); - await this.#streamEnvelopeHandler?.handle(rawMessage); + await this.streamEnvelopeHandler?.handle(rawMessage); } } @@ -100,13 +100,13 @@ export class Vat { * @returns A promise that resolves when the CapTP connection is made. */ async makeCapTp(): Promise { - if (this.#capTp !== undefined) { + if (this.capTp !== undefined) { throw new Error( `Vat with id "${this.id}" already has a CapTP connection.`, ); } - if (!this.#streamEnvelopeHandler) { + if (!this.streamEnvelopeHandler) { throw new Error( `Vat with id "${this.id}" does not have a stream envelope handler.`, ); @@ -121,8 +121,8 @@ export class Vat { await writer.next(wrapCapTp(content as CapTpMessage)); }); - this.#capTp = ctp; - this.#streamEnvelopeHandler.contentHandlers.capTp = async ( + this.capTp = ctp; + this.streamEnvelopeHandler.contentHandlers.capTp = async ( content: CapTpMessage, ) => { console.log('CapTP from vat', JSON.stringify(content, null, 2)); @@ -139,24 +139,18 @@ export class Vat { * @returns A promise that resolves the result of the CapTP call. */ async callCapTp(payload: CapTpPayload): Promise { - if (!this.#capTp) { + if (!this.capTp) { throw new Error( `Vat with id "${this.id}" does not have a CapTP connection.`, ); } - return E(this.#capTp.getBootstrap())[payload.method](...payload.params); + return E(this.capTp.getBootstrap())[payload.method](...payload.params); } /** * Terminates the vat. */ async terminate(): Promise { - if (!this.#streams) { - throw new Error( - `Vat with id "${this.id}" does not have a stream connection.`, - ); - } - await this.#streams.return(); // Handle orphaned messages @@ -175,6 +169,7 @@ export class Vat { * @returns A promise that resolves the response to the message. */ public async sendMessage(message: VatMessage): Promise { + console.debug(`Sending message to vat "${this.id}"`, message); const { promise, reject, resolve } = makePromiseKit(); const messageId = this.#nextMessageId(); this.unresolvedMessages.set(messageId, { reject, resolve }); diff --git a/yarn.lock b/yarn.lock index 833b61b06..879db5dad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1421,6 +1421,7 @@ __metadata: "@metamask/eslint-config-typescript": "npm:^13.0.0" "@ocap/shims": "workspace:^" "@ocap/streams": "workspace:^" + "@ocap/test-utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1" "@typescript-eslint/eslint-plugin": "npm:^8.1.0" @@ -1528,7 +1529,6 @@ __metadata: "@metamask/eslint-config-nodejs": "npm:^13.0.0" "@metamask/eslint-config-typescript": "npm:^13.0.0" "@metamask/utils": "npm:^9.1.0" - "@ocap/shims": "workspace:^" "@ocap/test-utils": "workspace:^" "@ts-bridge/cli": "npm:^0.5.1" "@ts-bridge/shims": "npm:^0.1.1" From 00e9849164c9328f32fc01e88c4b30c5ccab091a Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Sat, 21 Sep 2024 00:10:41 +0100 Subject: [PATCH 14/19] Manage worker from kernel --- packages/kernel/src/Kernel.test.ts | 8 ++++++-- packages/kernel/src/Kernel.ts | 26 ++++++++++++++------------ packages/kernel/src/Vat.test.ts | 6 ------ packages/kernel/src/Vat.ts | 12 +++--------- 4 files changed, 23 insertions(+), 29 deletions(-) diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index e45c53a5f..aa83e0b08 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -2,7 +2,7 @@ import type { VatMessage } from '@ocap/streams'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Kernel } from './Kernel.js'; -import type { VatLaunchProps, VatWorker } from './types.js'; +import type { VatWorker } from './types.js'; import { Vat } from './Vat.js'; describe('Kernel', () => { @@ -58,7 +58,10 @@ describe('Kernel', () => { await kernel.launchVat({ id: 'vat-id-1', worker: mockWorker }); expect(kernel.getVatIds()).toStrictEqual(['vat-id-1']); await expect( - kernel.launchVat({ id: 'vat-id-1' } as VatLaunchProps), + kernel.launchVat({ + id: 'vat-id-1', + worker: mockWorker, + }), ).rejects.toThrow('Vat with ID vat-id-1 already exists.'); expect(kernel.getVatIds()).toStrictEqual(['vat-id-1']); }); @@ -71,6 +74,7 @@ describe('Kernel', () => { expect(kernel.getVatIds()).toStrictEqual(['vat-id']); await kernel.deleteVat('vat-id'); expect(terminateMock).toHaveBeenCalledOnce(); + expect(mockWorker.delete).toHaveBeenCalledOnce(); expect(kernel.getVatIds()).toStrictEqual([]); }); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 2b5bb2178..b35fa4646 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -1,11 +1,11 @@ import '@ocap/shims/endoify'; import type { VatMessage } from '@ocap/streams'; -import type { VatId, VatLaunchProps } from './types.js'; +import type { VatId, VatLaunchProps, VatWorker } from './types.js'; import { Vat } from './Vat.js'; export class Kernel { - readonly #vats: Map; + readonly #vats: Map; constructor() { this.#vats = new Map(); @@ -33,8 +33,8 @@ export class Kernel { throw new Error(`Vat with ID ${id} already exists.`); } const [streams] = await worker.init(); - const vat = new Vat({ id, streams, deleteWorker: worker.delete }); - this.#vats.set(vat.id, vat); + const vat = new Vat({ id, streams }); + this.#vats.set(vat.id, { vat, worker }); await vat.init(); return vat; } @@ -45,8 +45,10 @@ export class Kernel { * @param id - The ID of the vat. */ public async deleteVat(id: string): Promise { - const vat = this.#getVat(id); + const vatRecord = this.#getVatRecord(id); + const { vat, worker } = vatRecord; await vat.terminate(); + await worker.delete(); this.#vats.delete(id); } @@ -58,21 +60,21 @@ export class Kernel { * @returns A promise that resolves the response to the message. */ public async sendMessage(id: VatId, message: VatMessage): Promise { - const vat = this.#getVat(id); + const { vat } = this.#getVatRecord(id); return vat.sendMessage(message); } /** - * Gets a vat from the kernel. + * Gets a vat record from the kernel. * * @param id - The ID of the vat. - * @returns The vat record. + * @returns The vat record (vat and worker). */ - #getVat(id: string): Vat { - const vat = this.#vats.get(id); - if (vat === undefined) { + #getVatRecord(id: string): { vat: Vat; worker: VatWorker } { + const vatRecord = this.#vats.get(id); + if (vatRecord === undefined) { throw new Error(`Vat with ID ${id} does not exist.`); } - return vat; + return vatRecord; } } diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts index 822897619..00139e572 100644 --- a/packages/kernel/src/Vat.test.ts +++ b/packages/kernel/src/Vat.test.ts @@ -19,7 +19,6 @@ vi.mock('@endo/captp', () => { describe('Vat', () => { let mockStreams: StreamPair; - let mockDeleteWorker: () => Promise; let vat: Vat; beforeEach(() => { @@ -51,14 +50,10 @@ describe('Vat', () => { throw: vi.fn().mockResolvedValue(undefined), }; - // Mock the delete worker function - mockDeleteWorker = vi.fn().mockResolvedValue(undefined); - // Create a new instance of the Vat class vat = new Vat({ id: 'test-vat', streams: mockStreams, - deleteWorker: mockDeleteWorker, }); }); @@ -98,7 +93,6 @@ describe('Vat', () => { const mockSpy = vi.spyOn(mockPromiseKit, 'reject'); vat.unresolvedMessages.set(mockMessageId, mockPromiseKit); await vat.terminate(); - expect(mockDeleteWorker).toHaveBeenCalled(); expect(mockSpy).toHaveBeenCalledWith(expect.any(Error)); }); }); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index 762015cee..9389bd727 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -18,13 +18,12 @@ import { makeStreamEnvelopeHandler, } from '@ocap/streams'; -import type { UnresolvedMessages, VatLaunchProps } from './types.js'; +import type { UnresolvedMessages, VatId } from './types.js'; import { makeCounter } from './utils/makeCounter.js'; type VatConstructorProps = { - id: VatLaunchProps['id']; + id: VatId; streams: StreamPair; - deleteWorker: VatLaunchProps['worker']['delete']; }; export class Vat { @@ -32,8 +31,6 @@ export class Vat { readonly #streams: VatConstructorProps['streams']; - readonly #deleteWorker: VatConstructorProps['deleteWorker']; - readonly #messageCounter: () => number; readonly unresolvedMessages: UnresolvedMessages = new Map(); @@ -42,10 +39,9 @@ export class Vat { capTp?: ReturnType; - constructor({ id, streams, deleteWorker }: VatConstructorProps) { + constructor({ id, streams }: VatConstructorProps) { this.id = id; this.#streams = streams; - this.#deleteWorker = deleteWorker; this.#messageCounter = makeCounter(); } @@ -158,8 +154,6 @@ export class Vat { promiseCallback?.reject(new Error('Vat was deleted')); this.unresolvedMessages.delete(messageId); } - - await this.#deleteWorker(); } /** From 1edbfa072fa32ada90b59b3849af97b004f33fd4 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Sat, 21 Sep 2024 00:32:48 +0100 Subject: [PATCH 15/19] resolve endoify for tests --- packages/kernel/vitest.config.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/kernel/vitest.config.ts b/packages/kernel/vitest.config.ts index 6b2328fbd..177cd87c8 100644 --- a/packages/kernel/vitest.config.ts +++ b/packages/kernel/vitest.config.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line spaced-comment /// +import path from 'path'; import { defineConfig, mergeConfig } from 'vite'; import { getDefaultConfig } from '../../vitest.config.packages.js'; @@ -12,6 +13,13 @@ const config = mergeConfig( defineConfig({ test: { pool: 'vmThreads', + alias: [ + { + find: '@ocap/shims/endoify', + replacement: path.resolve('../shims/src/endoify.js'), + customResolver: (id) => ({ external: true, id }), + }, + ], }, }), ); From 430b2f4ca748bc7a6e526e849afb43585f837850 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Sat, 21 Sep 2024 00:52:05 +0100 Subject: [PATCH 16/19] remove types --- packages/kernel/src/Kernel.ts | 10 ++++++++-- packages/kernel/src/types.ts | 5 ----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index b35fa4646..a302182e6 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -1,7 +1,7 @@ import '@ocap/shims/endoify'; import type { VatMessage } from '@ocap/streams'; -import type { VatId, VatLaunchProps, VatWorker } from './types.js'; +import type { VatId, VatWorker } from './types.js'; import { Vat } from './Vat.js'; export class Kernel { @@ -28,7 +28,13 @@ export class Kernel { * @param options.worker - The worker to use for the vat. * @returns A promise that resolves the vat. */ - public async launchVat({ id, worker }: VatLaunchProps): Promise { + public async launchVat({ + id, + worker, + }: { + id: VatId; + worker: VatWorker; + }): Promise { if (this.#vats.has(id)) { throw new Error(`Vat with ID ${id} already exists.`); } diff --git a/packages/kernel/src/types.ts b/packages/kernel/src/types.ts index 42be17531..5b8d304f6 100644 --- a/packages/kernel/src/types.ts +++ b/packages/kernel/src/types.ts @@ -11,8 +11,3 @@ export type VatWorker = { export type PromiseCallbacks = Omit, 'promise'>; export type UnresolvedMessages = Map; - -export type VatLaunchProps = { - id: VatId; - worker: VatWorker; -}; From 1eddbcacb08eac788989a63051ed26274a17cee0 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Sep 2024 17:21:10 +0100 Subject: [PATCH 17/19] fixing vat tests --- packages/kernel/src/Kernel.test.ts | 6 +- packages/kernel/src/Kernel.ts | 10 +- packages/kernel/src/Vat.test.ts | 118 +++++------------- packages/kernel/src/Vat.ts | 51 ++++---- .../test-utils/src/{utils.ts => delay.ts} | 0 packages/test-utils/src/index.ts | 5 +- packages/test-utils/src/makeCapTpMock.ts | 27 ++++ .../src/{mocks.ts => makePromiseKitMock.ts} | 0 8 files changed, 90 insertions(+), 127 deletions(-) rename packages/test-utils/src/{utils.ts => delay.ts} (100%) create mode 100644 packages/test-utils/src/makeCapTpMock.ts rename packages/test-utils/src/{mocks.ts => makePromiseKitMock.ts} (100%) diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index aa83e0b08..c6573da48 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -24,7 +24,7 @@ describe('Kernel', () => { .mockImplementation(vi.fn()); }); - describe('#getVatIds()', () => { + describe('getVatIds()', () => { it('returns an empty array when no vats are added', () => { const kernel = new Kernel(); expect(kernel.getVatIds()).toStrictEqual([]); @@ -67,7 +67,7 @@ describe('Kernel', () => { }); }); - describe('#deleteVat()', () => { + describe('deleteVat()', () => { it('deletes a vat from the kernel without errors when the vat exists', async () => { const kernel = new Kernel(); await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); @@ -96,7 +96,7 @@ describe('Kernel', () => { }); }); - describe('#sendMessage()', () => { + describe('sendMessage()', () => { it('sends a message to the vat without errors when the vat exists', async () => { const kernel = new Kernel(); await kernel.launchVat({ id: 'vat-id', worker: mockWorker }); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index a302182e6..0d540919e 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -16,7 +16,7 @@ export class Kernel { * * @returns An array of vat IDs. */ - public getVatIds(): VatId[] { + getVatIds(): VatId[] { return Array.from(this.#vats.keys()); } @@ -28,7 +28,7 @@ export class Kernel { * @param options.worker - The worker to use for the vat. * @returns A promise that resolves the vat. */ - public async launchVat({ + async launchVat({ id, worker, }: { @@ -50,7 +50,7 @@ export class Kernel { * * @param id - The ID of the vat. */ - public async deleteVat(id: string): Promise { + async deleteVat(id: VatId): Promise { const vatRecord = this.#getVatRecord(id); const { vat, worker } = vatRecord; await vat.terminate(); @@ -65,7 +65,7 @@ export class Kernel { * @param message - The message to send. * @returns A promise that resolves the response to the message. */ - public async sendMessage(id: VatId, message: VatMessage): Promise { + async sendMessage(id: VatId, message: VatMessage): Promise { const { vat } = this.#getVatRecord(id); return vat.sendMessage(message); } @@ -76,7 +76,7 @@ export class Kernel { * @param id - The ID of the vat. * @returns The vat record (vat and worker). */ - #getVatRecord(id: string): { vat: Vat; worker: VatWorker } { + #getVatRecord(id: VatId): { vat: Vat; worker: VatWorker } { const vatRecord = this.#vats.get(id); if (vatRecord === undefined) { throw new Error(`Vat with ID ${id} does not exist.`); diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts index 00139e572..06123ee58 100644 --- a/packages/kernel/src/Vat.test.ts +++ b/packages/kernel/src/Vat.test.ts @@ -1,82 +1,55 @@ import '@ocap/shims/endoify'; -import type { StreamPair, StreamEnvelope, VatMessage } from '@ocap/streams'; -import { makePromiseKitMock } from '@ocap/test-utils'; +import { + makeMessagePortStreamPair, + makeStreamEnvelopeHandler, + Command, +} from '@ocap/streams'; +import type { StreamEnvelope, VatMessage } from '@ocap/streams'; +import { makeCapTpMock, makePromiseKitMock } from '@ocap/test-utils'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { Vat } from './Vat.js'; -vi.mock('@endo/promise-kit', () => makePromiseKitMock()); - -vi.mock('@endo/captp', () => { - return { - makeCapTP: vi.fn(() => ({ - getBootstrap: vi.fn(() => ({ - testMethod: vi.fn().mockResolvedValue('test-result'), - })), - })), - }; -}); +vi.mock('@endo/captp', () => makeCapTpMock()); describe('Vat', () => { - let mockStreams: StreamPair; let vat: Vat; + let port1: MessagePort; beforeEach(() => { vi.resetAllMocks(); - // Mock the streams - mockStreams = { - reader: { - // @ts-expect-error We are mocking the async iterator - async *[Symbol.asyncIterator]() { - yield { - label: 'test-label', - message: {} as VatMessage, - }; - }, - return: vi.fn().mockResolvedValue(undefined), - throw: vi.fn(), - }, - writer: { - next: vi.fn().mockResolvedValue({ done: false }), - return: vi.fn().mockResolvedValue(undefined), - throw: vi.fn(), - // @ts-expect-error We are mocking the async iterator - async *[Symbol.asyncIterator]() { - yield {}; - }, - }, - return: vi.fn().mockResolvedValue(undefined), - throw: vi.fn().mockResolvedValue(undefined), - }; + const messageChannel = new MessageChannel(); + port1 = messageChannel.port1; + + const streams = makeMessagePortStreamPair(port1); - // Create a new instance of the Vat class vat = new Vat({ id: 'test-vat', - streams: mockStreams, + streams, }); }); - describe('#init', () => { + describe('init', () => { it('initializes the vat and sends a ping message', async () => { const sendMessageMock = vi .spyOn(vat, 'sendMessage') .mockResolvedValueOnce(undefined); - const makeCapTpMock = vi + const capTpMock = vi .spyOn(vat, 'makeCapTp') .mockResolvedValueOnce(undefined); await vat.init(); expect(sendMessageMock).toHaveBeenCalledWith({ - type: 'ping', + type: Command.Ping, data: null, }); - expect(makeCapTpMock).toHaveBeenCalled(); + expect(capTpMock).toHaveBeenCalled(); }); }); - describe('#sendMessage', () => { + describe('sendMessage', () => { it('sends a message and resolves the promise', async () => { const mockMessage = { type: 'makeCapTp', data: null } as VatMessage; const sendMessagePromise = vat.sendMessage(mockMessage); @@ -86,7 +59,7 @@ describe('Vat', () => { }); }); - describe('#terminate', () => { + describe('terminate', () => { it('terminates the vat and resolves/rejects unresolved messages', async () => { const mockMessageId = 'test-vat-1'; const mockPromiseKit = makePromiseKitMock().makePromiseKit(); @@ -101,60 +74,31 @@ describe('Vat', () => { it('throws an error if CapTP connection already exists', async () => { // @ts-expect-error - Simulating an existing CapTP vat.capTp = {}; - await expect(vat.makeCapTp()).rejects.toThrow( `Vat with id "${vat.id}" already has a CapTP connection.`, ); }); - it('throws an error if stream envelope handler is not initialized', async () => { - // @ts-expect-error - Simulate the stream envelope handler not being set - vat.streamEnvelopeHandler = undefined; - - await expect(vat.makeCapTp()).rejects.toThrow( - `Vat with id "${vat.id}" does not have a stream envelope handler.`, - ); - }); - it('creates a CapTP connection and sends CapTpInit message', async () => { - const streamEnvelopeHandlerMock = { - contentHandlers: { capTp: undefined }, - }; - // @ts-expect-error - Set the streamEnvelopeHandler in the vat instance - vat.streamEnvelopeHandler = streamEnvelopeHandlerMock; - - const sendMessageSpy = vi + vat.streamEnvelopeHandler = makeStreamEnvelopeHandler({}, console.warn); + const sendMessageMock = vi .spyOn(vat, 'sendMessage') - .mockResolvedValue(undefined); - - vi.spyOn(mockStreams.writer, 'next').mockResolvedValue({ - done: false, - value: undefined, - }); - + .mockResolvedValueOnce(undefined); await vat.makeCapTp(); - - expect(sendMessageSpy).toHaveBeenCalledWith({ - type: 'makeCapTp', + expect(vat.streamEnvelopeHandler.contentHandlers.capTp).toBeDefined(); + expect(sendMessageMock).toHaveBeenCalledWith({ + type: Command.CapTpInit, data: null, }); - expect(streamEnvelopeHandlerMock.contentHandlers.capTp).toBeDefined(); }); }); describe('#callCapTp', () => { - it('throws an error if no CapTP connection exists', async () => { - // Ensure no CapTP connection exists in the vat instance - // @ts-expect-error - Simulate the CapTP connection not being set - vat.capTp = undefined; - - const payload = { - method: 'testMethod', - params: ['param1', 'param2'], - }; - - await expect(vat.callCapTp(payload)).rejects.toThrow( - `Vat with id "${vat.id}" does not have a CapTP connection.`, + it('throws an error if CapTP connection is not established', async () => { + await expect( + vat.callCapTp({ method: 'testMethod', params: [] }), + ).rejects.toThrow( + `Vat with id "test-vat" does not have a CapTP connection.`, ); }); }); diff --git a/packages/kernel/src/Vat.ts b/packages/kernel/src/Vat.ts index 9389bd727..ae2439028 100644 --- a/packages/kernel/src/Vat.ts +++ b/packages/kernel/src/Vat.ts @@ -29,45 +29,44 @@ type VatConstructorProps = { export class Vat { readonly id: VatConstructorProps['id']; - readonly #streams: VatConstructorProps['streams']; + readonly streams: VatConstructorProps['streams']; readonly #messageCounter: () => number; readonly unresolvedMessages: UnresolvedMessages = new Map(); - streamEnvelopeHandler?: StreamEnvelopeHandler; + streamEnvelopeHandler: StreamEnvelopeHandler; capTp?: ReturnType; constructor({ id, streams }: VatConstructorProps) { this.id = id; - this.#streams = streams; + this.streams = streams; this.#messageCounter = makeCounter(); - } - - /** - * Initializes the vat. - * - * @returns A promise that resolves when the vat is initialized. - */ - async init(): Promise { this.streamEnvelopeHandler = makeStreamEnvelopeHandler( { - command: async ({ id, message }) => { - const promiseCallbacks = this.unresolvedMessages.get(id); + command: async ({ id: messageId, message }) => { + const promiseCallbacks = this.unresolvedMessages.get(messageId); if (promiseCallbacks === undefined) { - console.error(`No unresolved message with id "${id}".`); + console.error(`No unresolved message with id "${messageId}".`); } else { - this.unresolvedMessages.delete(id); + this.unresolvedMessages.delete(messageId); promiseCallbacks.resolve(message.data); } }, }, console.warn, ); + } + /** + * Initializes the vat. + * + * @returns A promise that resolves when the vat is initialized. + */ + async init(): Promise { /* v8 ignore next 4: Not known to be possible. */ - this.#receiveMessages(this.#streams.reader).catch((error) => { + this.#receiveMessages(this.streams.reader).catch((error) => { console.error(`Unexpected read error from vat "${this.id}"`, error); throw error; }); @@ -85,8 +84,8 @@ export class Vat { */ async #receiveMessages(reader: Reader): Promise { for await (const rawMessage of reader) { - console.debug('Offscreen received message', rawMessage); - await this.streamEnvelopeHandler?.handle(rawMessage); + console.debug('Vat received message', rawMessage); + await this.streamEnvelopeHandler.handle(rawMessage); } } @@ -102,16 +101,8 @@ export class Vat { ); } - if (!this.streamEnvelopeHandler) { - throw new Error( - `Vat with id "${this.id}" does not have a stream envelope handler.`, - ); - } - // Handle writes here. #receiveMessages() handles reads. - const { writer } = this.#streams; - // https://github.com/endojs/endo/issues/2412 - // eslint-disable-next-line @typescript-eslint/no-misused-promises + const { writer } = this.streams; const ctp = makeCapTP(this.id, async (content: unknown) => { console.log('CapTP to vat', JSON.stringify(content, null, 2)); await writer.next(wrapCapTp(content as CapTpMessage)); @@ -147,7 +138,7 @@ export class Vat { * Terminates the vat. */ async terminate(): Promise { - await this.#streams.return(); + await this.streams.return(); // Handle orphaned messages for (const [messageId, promiseCallback] of this.unresolvedMessages) { @@ -162,12 +153,12 @@ export class Vat { * @param message - The message to send. * @returns A promise that resolves the response to the message. */ - public async sendMessage(message: VatMessage): Promise { + async sendMessage(message: VatMessage): Promise { console.debug(`Sending message to vat "${this.id}"`, message); const { promise, reject, resolve } = makePromiseKit(); const messageId = this.#nextMessageId(); this.unresolvedMessages.set(messageId, { reject, resolve }); - await this.#streams?.writer.next( + await this.streams.writer.next( wrapStreamCommand({ id: messageId, message }), ); return promise; diff --git a/packages/test-utils/src/utils.ts b/packages/test-utils/src/delay.ts similarity index 100% rename from packages/test-utils/src/utils.ts rename to packages/test-utils/src/delay.ts diff --git a/packages/test-utils/src/index.ts b/packages/test-utils/src/index.ts index 6aa734f18..af7069437 100644 --- a/packages/test-utils/src/index.ts +++ b/packages/test-utils/src/index.ts @@ -1,2 +1,3 @@ -export * from './mocks.js'; -export * from './utils.js'; +export { makePromiseKitMock } from './makePromiseKitMock.js'; +export { makeCapTpMock } from './makeCapTpMock.js'; +export { delay } from './delay.js'; diff --git a/packages/test-utils/src/makeCapTpMock.ts b/packages/test-utils/src/makeCapTpMock.ts new file mode 100644 index 000000000..f660e0d67 --- /dev/null +++ b/packages/test-utils/src/makeCapTpMock.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +/** + * Create a module mock for `@endo/captp`. + * + * @returns The mock. + */ +export const makeCapTpMock = () => ({ + makeCapTP: ( + id: string, + send: (message: unknown) => Promise, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + bootstrapObj?: any, + ) => { + const capTp = { + id, + send, + dispatch: () => undefined, + getBootstrap: () => undefined, + bootstrapObj: bootstrapObj ?? { + testMethod: Promise.resolve('bootstrap-result'), + }, + }; + capTp.getBootstrap = () => capTp.bootstrapObj; + return capTp; + }, +}); diff --git a/packages/test-utils/src/mocks.ts b/packages/test-utils/src/makePromiseKitMock.ts similarity index 100% rename from packages/test-utils/src/mocks.ts rename to packages/test-utils/src/makePromiseKitMock.ts From 8df2e92f6452c42466e468f6154413004f79ed25 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Sep 2024 18:02:37 +0100 Subject: [PATCH 18/19] fix test typos --- packages/kernel/src/Kernel.test.ts | 4 ++-- packages/kernel/src/Vat.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index c6573da48..36971c827 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -44,7 +44,7 @@ describe('Kernel', () => { }); }); - describe('#launchVat()', () => { + describe('launchVat()', () => { it('adds a vat to the kernel without errors when no vat with the same ID exists', async () => { const kernel = new Kernel(); await kernel.launchVat({ id: 'vat-id-1', worker: mockWorker }); @@ -123,7 +123,7 @@ describe('Kernel', () => { }); }); - describe('#constructor()', () => { + describe('constructor()', () => { it('initializes the kernel without errors', () => { expect(async () => new Kernel()).not.toThrow(); }); diff --git a/packages/kernel/src/Vat.test.ts b/packages/kernel/src/Vat.test.ts index 06123ee58..867d69766 100644 --- a/packages/kernel/src/Vat.test.ts +++ b/packages/kernel/src/Vat.test.ts @@ -70,7 +70,7 @@ describe('Vat', () => { }); }); - describe('#makeCapTp', () => { + describe('makeCapTp', () => { it('throws an error if CapTP connection already exists', async () => { // @ts-expect-error - Simulating an existing CapTP vat.capTp = {}; @@ -93,7 +93,7 @@ describe('Vat', () => { }); }); - describe('#callCapTp', () => { + describe('callCapTp', () => { it('throws an error if CapTP connection is not established', async () => { await expect( vat.callCapTp({ method: 'testMethod', params: [] }), From 6ec897cc3e58c92b390e699995d850d7c07675f5 Mon Sep 17 00:00:00 2001 From: Dimitris Marlagkoutsos Date: Mon, 23 Sep 2024 21:47:12 +0100 Subject: [PATCH 19/19] apply comments --- packages/streams/tsconfig.json | 4 +--- packages/test-utils/src/makeCapTpMock.ts | 14 +++++--------- packages/test-utils/src/makePromiseKitMock.ts | 3 +-- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/streams/tsconfig.json b/packages/streams/tsconfig.json index 89a01da45..4dda57d2d 100644 --- a/packages/streams/tsconfig.json +++ b/packages/streams/tsconfig.json @@ -3,9 +3,7 @@ "compilerOptions": { "baseUrl": "./", "lib": ["DOM", "ES2022"], - "types": ["ses", "vitest", "vitest/jsdom"], - "allowJs": true, - "checkJs": true + "types": ["ses", "vitest", "vitest/jsdom"] }, "references": [{ "path": "../test-utils" }], "include": ["./src", "./test/envelope-kit-fixtures.ts"] diff --git a/packages/test-utils/src/makeCapTpMock.ts b/packages/test-utils/src/makeCapTpMock.ts index f660e0d67..7911f69f0 100644 --- a/packages/test-utils/src/makeCapTpMock.ts +++ b/packages/test-utils/src/makeCapTpMock.ts @@ -1,27 +1,23 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ - /** * Create a module mock for `@endo/captp`. * * @returns The mock. */ + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export const makeCapTpMock = () => ({ makeCapTP: ( id: string, send: (message: unknown) => Promise, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - bootstrapObj?: any, + bootstrapObj?: unknown, ) => { const capTp = { id, send, + bootstrapObj, dispatch: () => undefined, - getBootstrap: () => undefined, - bootstrapObj: bootstrapObj ?? { - testMethod: Promise.resolve('bootstrap-result'), - }, + getBootstrap: () => capTp.bootstrapObj, }; - capTp.getBootstrap = () => capTp.bootstrapObj; return capTp; }, }); diff --git a/packages/test-utils/src/makePromiseKitMock.ts b/packages/test-utils/src/makePromiseKitMock.ts index 0f77249d2..f71bfec7c 100644 --- a/packages/test-utils/src/makePromiseKitMock.ts +++ b/packages/test-utils/src/makePromiseKitMock.ts @@ -1,10 +1,9 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ - /** * Create a module mock for `@endo/promise-kit`. * * @returns The mock. */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type export const makePromiseKitMock = () => ({ makePromiseKit: () => { let resolve: (value: unknown) => void, reject: (reason?: unknown) => void;