Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/nodejs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# `@ocap/nodejs`

For running Ocap Kernel experiments in a nodejs environment.

## 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).
79 changes: 79 additions & 0 deletions packages/nodejs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{
"name": "@ocap/nodejs",
"version": "0.0.0",
"private": true,
"description": "For running Ocap Kernel experiments in a nodejs environment",
"repository": {
"type": "git",
"url": "https://github.com/MetaMask/ocap-kernel.git"
},
"type": "module",
"files": [
"dist/"
],
"scripts": {
"build": "ts-bridge --project tsconfig.build.json --clean",
"changelog:validate": "../../scripts/validate-changelog.sh @ocap/nodejs",
"clean": "rimraf --glob ./dist './*.tsbuildinfo'",
"lint": "yarn lint:ts && yarn lint:eslint && yarn lint:misc --check && yarn constraints && yarn lint:dependencies",
"lint:dependencies": "depcheck",
"lint:eslint": "eslint . --cache",
"lint:fix": "yarn lint:ts && yarn lint:eslint --fix && yarn lint:misc --write && yarn constraints --fix && yarn lint:dependencies",
"lint:misc": "prettier --no-error-on-unmatched-pattern '**/*.json' '**/*.md' '**/*.html' '!**/CHANGELOG.old.md' '**/*.yml' '!.yarnrc.yml' '!merged-packages/**' --ignore-path ../../.gitignore",
"lint:ts": "tsc --project tsconfig.lint.json",
"publish:preview": "yarn npm publish --tag preview",
"start": "yarn build:vite:dev --watch",
"test": "yarn build && vitest",
"test:clean": "yarn test --no-cache --coverage.clean",
"test:dev": "yarn test --coverage false",
"test:verbose": "yarn test --reporter verbose"
},
"dependencies": {
"@endo/eventual-send": "^1.2.6",
"@endo/exo": "^1.5.4",
"@endo/patterns": "^1.4.4",
"@endo/promise-kit": "^1.1.6",
"@metamask/snaps-utils": "^8.3.0",
"@metamask/utils": "^9.3.0",
"@ocap/errors": "workspace:^",
"@ocap/kernel": "workspace:^",
"@ocap/shims": "workspace:^",
"@ocap/streams": "workspace:^",
"@ocap/utils": "workspace:^",
"@sqlite.org/sqlite-wasm": "3.47.0-build1",
"ses": "^1.9.0"
},
"devDependencies": {
"@arethetypeswrong/cli": "^0.16.4",
"@metamask/auto-changelog": "^3.4.4",
"@metamask/eslint-config": "^14.0.0",
"@metamask/eslint-config-nodejs": "^14.0.0",
"@metamask/eslint-config-typescript": "^14.0.0",
"@ocap/cli": "workspace:^",
"@ocap/test-utils": "workspace:^",
"@ts-bridge/cli": "^0.5.1",
"@types/chrome": "^0.0.268",
"@typescript-eslint/eslint-plugin": "^8.8.1",
"@typescript-eslint/parser": "^8.8.1",
"@typescript-eslint/utils": "^8.8.1",
"depcheck": "^1.4.7",
"eslint": "^9.12.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import-x": "^4.3.1",
"eslint-plugin-jsdoc": "^50.3.1",
"eslint-plugin-n": "^17.11.1",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-promise": "^7.1.0",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"typedoc": "^0.26.8",
"typescript": "~5.5.4",
"typescript-eslint": "^8.8.1",
"vite": "^5.3.5",
"vitest": "^2.1.2"
},
"engines": {
"node": "^18.18 || >=20"
}
}
55 changes: 55 additions & 0 deletions packages/nodejs/src/kernel/VatWorkerService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { VatWorkerService, VatId } from '@ocap/kernel';
import { NodeWorkerMultiplexer } from '@ocap/streams';
import type { StreamMultiplexer } from '@ocap/streams';
import { makeLogger } from '@ocap/utils';
import type { Logger } from '@ocap/utils';
import { Worker as NodeWorker } from 'node:worker_threads';

// Worker file loads from the built dist directory, requires rebuild after change
// Note: Worker runs in same process and may be subject to spectre-style attacks
const workerFileURL = new URL('../../dist/vat/inside.mjs', import.meta.url)
.pathname;

export class NodejsVatWorkerService implements VatWorkerService {
// eslint-disable-next-line no-unused-private-class-members
readonly #logger: Logger;

workers = new Map<
VatId,
{ worker: NodeWorker; multiplexer: StreamMultiplexer }
>();

/**
* The vat worker service, intended to be constructed in
* the kernel worker.
*
* @param logger - An optional {@link Logger}. Defaults to a new logger labeled '[vat worker client]'.
*/
constructor(logger?: Logger) {
this.#logger = logger ?? makeLogger('[vat worker service]');
}

async launch(vatId: VatId): Promise<StreamMultiplexer> {
const worker = new NodeWorker(workerFileURL);
const multiplexer = new NodeWorkerMultiplexer(worker);
this.workers.set(vatId, { worker, multiplexer });
return multiplexer;
}

async terminate(vatId: VatId): Promise<undefined> {
const workerEntry = this.workers.get(vatId);
assert(workerEntry, `No worker found for vatId ${vatId}`);
const { worker, multiplexer } = workerEntry;
await multiplexer.return();
await worker.terminate();
this.workers.delete(vatId);
return undefined;
}

async terminateAll(): Promise<void> {
for (const vatId of this.workers.keys()) {
await this.terminate(vatId);
}
}
}
harden(NodejsVatWorkerService);
58 changes: 58 additions & 0 deletions packages/nodejs/src/kernel/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import '@ocap/shims/endoify';
import fs from 'fs';

//
// these modificiations are used by the "@sqlite.org/sqlite-wasm" package
//

/**
*
* @param uri - file:// URI
* @param mimeType - MIME type, default is 'text/plain'
* @returns - fetch response
*/
async function fetchFile(
uri: string,
mimeType: string = 'text/plain',
// eslint-disable-next-line n/no-unsupported-features/node-builtins
): Promise<Response> {
if (uri.startsWith('file://')) {
try {
const path = new URL(uri).pathname;
const data = await fs.promises.readFile(path);
// eslint-disable-next-line n/no-unsupported-features/node-builtins
return new Response(data, {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': mimeType },
});
} catch (error) {
console.error('Error reading file:', error);
// eslint-disable-next-line n/no-unsupported-features/node-builtins
return new Response(null, { status: 404, statusText: 'File Not Found' });
}
}
// eslint-disable-next-line n/no-unsupported-features/node-builtins
return new Response(null, {
status: 500,
statusText: 'Only file:// supported',
});
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).self = globalThis;

const globalFetch = globalThis.fetch;
const wrappedFetchForSqliteWasm = async (
url: string,
options: RequestInit = {},
// eslint-disable-next-line n/no-unsupported-features/node-builtins
): Promise<Response> => {
if (url.endsWith('@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm')) {
return fetchFile(url, 'application/wasm');
}
console.log(`fetching ${url}`);
return globalFetch(url, options);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).fetch = wrappedFetchForSqliteWasm;
29 changes: 29 additions & 0 deletions packages/nodejs/src/kernel/kernel-worker.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { MessageChannel as NodeMessageChannel } from 'node:worker_threads';
import { describe, it, expect } from 'vitest';

import { makeKernel, runVatLifecycle } from './kernel-worker.js';

describe('Kernel Worker', () => {
it('should handle the lifecycle of multiple vats', async () => {
const kernelChannel = new NodeMessageChannel();
const { port1: kernelPort } = kernelChannel;
console.log('Creating kernel...');
const kernel = await makeKernel(kernelPort);
console.log('Kernel created.');

console.log('Handling the lifecycle of multiple vats...');
await runVatLifecycle(kernel, ['v1', 'v2', 'v3']);
console.log('Lifecycle of multiple vats handled.');

// console.log('Adding default vat...');
// await kernel.launchVat({ id: 'v0' });
// console.log('Default vat added.');

// console.log('Shutting down the default vat...');
// await kernel.terminateVat('v0');
// console.log('Default vat shut down.');

console.log('Test passed.');
expect(true).toBe(true);
});
});
79 changes: 79 additions & 0 deletions packages/nodejs/src/kernel/kernel-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import './env.js';
import type { NonEmptyArray } from '@metamask/utils';
import type { KernelCommand, KernelCommandReply, VatId } from '@ocap/kernel';
import { Kernel, VatCommandMethod } from '@ocap/kernel';
import { NodeWorkerDuplexStream } from '@ocap/streams';
import { MessagePort as NodeMessagePort } from 'worker_threads';

import { makeSQLKVStore } from './sqlite-kv-store.js';
import { NodejsVatWorkerService } from './VatWorkerService.js';

/**
* The main function for the kernel worker.
*
* @param port - The kernel's end of a node:worker_threads MessageChannel
* @returns The kernel, initialized.
*/
export async function makeKernel(port: NodeMessagePort): Promise<Kernel> {
const nodeStream = new NodeWorkerDuplexStream<
KernelCommand,
KernelCommandReply
>(port);
const vatWorkerClient = new NodejsVatWorkerService();

// Initialize kernel store.
const kvStore = await makeSQLKVStore();

// Create and start kernel.
const kernel = new Kernel(nodeStream, vatWorkerClient, kvStore);
await kernel.init();

return kernel;
}

/**
* Runs the full lifecycle of an array of vats, including their creation,
* restart, message passing, and termination.
*
* @param kernel The kernel instance.
* @param vats An array of VatIds to be managed.
*/
export async function runVatLifecycle(
kernel: Kernel,
vats: NonEmptyArray<VatId>,
): Promise<void> {
console.time(`Created vats: ${vats.join(', ')}`);
await Promise.all(
vats.map(async () =>
kernel.launchVat({
bundleName: 'sample-vat',
parameters: { name: 'Nodeen' },
}),
),
);
console.timeEnd(`Created vats: ${vats.join(', ')}`);

console.log('Kernel vats:', kernel.getVatIds().join(', '));

// Restart a randomly selected vat from the array.
const vatToRestart = vats[Math.floor(Math.random() * vats.length)] as VatId;
console.time(`Vat "${vatToRestart}" restart`);
await kernel.restartVat(vatToRestart);
console.timeEnd(`Vat "${vatToRestart}" restart`);

// Send a "Ping" message to a randomly selected vat.
const vatToPing = vats[Math.floor(Math.random() * vats.length)] as VatId;
console.time(`Ping Vat "${vatToPing}"`);
await kernel.sendMessage(vatToPing, {
method: VatCommandMethod.ping,
params: null,
});
console.timeEnd(`Ping Vat "${vatToPing}"`);

const vatIds = kernel.getVatIds().join(', ');
console.time(`Terminated vats: ${vatIds}`);
await kernel.terminateAllVats();
console.timeEnd(`Terminated vats: ${vatIds}`);

console.log(`Kernel has ${kernel.getVatIds().length} vats`);
}
Loading
Loading