Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6621252
refactor: Move sqlite-wasm code to extension
rekmarks Jan 18, 2025
196a7c1
Merge branch 'main' into rekm/sqlite-wasm-to-extension
grypez Jan 21, 2025
0102680
draft
grypez Jan 9, 2025
f55c9ce
fix vitest endoify resolution
grypez Jan 10, 2025
6df657b
align nodejs vat-worker with extension vat iframe
grypez Jan 11, 2025
9f034bb
draft
grypez Jan 14, 2025
13e860c
XtrEme uNiT tESTing
grypez Jan 22, 2025
935e700
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 22, 2025
2ecbebd
mock make-kernel.test kvStore
grypez Jan 22, 2025
ca85077
thresholds
grypez Jan 22, 2025
eea9684
give getPort test more time
grypez Jan 22, 2025
35a2137
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 22, 2025
b17eece
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 22, 2025
df25f8d
more refined
grypez Jan 22, 2025
a18237c
fix endo mocking
grypez Jan 23, 2025
5ce6c79
remove endoify from Kernel.ts
grypez Jan 23, 2025
4f7fdce
further e2e testing
grypez Jan 24, 2025
5ff625f
trace dispatch error
grypez Jan 24, 2025
2bd4b2c
get plucky
grypez Jan 24, 2025
196f216
cleanup
grypez Jan 24, 2025
9c257db
remove debug logs
grypez Jan 24, 2025
e1fc71d
more cleanup
grypez Jan 24, 2025
09db0f2
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 24, 2025
6cab757
return type test func
grypez Jan 24, 2025
73dc899
appease linter
grypez Jan 24, 2025
2b33baf
update extensions
grypez Jan 24, 2025
90c0d2a
thresholds
grypez Jan 24, 2025
82bb2bd
simplify test a bit
grypez Jan 24, 2025
d760a69
isolate CI failure
grypez Jan 24, 2025
a5b9154
test(nodejs): reallocate coverage between unit and e2e phases
grypez Jan 27, 2025
7fc4b8e
thresholds
grypez Jan 27, 2025
e5cc333
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 27, 2025
039a8f7
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 27, 2025
ad3ec78
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 27, 2025
d95a4ff
tidy files
grypez Jan 28, 2025
d9387c9
Merge branch 'main' into grypez/nodejs-unit-tests
grypez Jan 28, 2025
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
18 changes: 18 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,24 @@ const config = createConfig([
globals: { lockdown: 'readonly' },
},
},

{
files: [
'packages/nodejs/**/*-worker.ts',
'packages/nodejs/test/workers/**/*',
],
rules: {
// Node workers have reasonable cause to read from process.env
'n/no-process-env': 'off',
},
},
{
files: ['packages/nodejs/test/workers/**/*'],
rules: {
// Test node worker files can resolve these imports, even if eslint cannot.
'import-x/no-unresolved': 'off',
},
},
]);

export default config;
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"prepack": "./scripts/prepack.sh",
"test": "vitest run",
"test:clean": "yarn test --no-cache --coverage.clean",
"test:dev": "yarn test --coverage false ",
"test:dev": "yarn test --coverage false",
"test:e2e": "yarn workspaces foreach --all run test:e2e",
"test:e2e:ci": "yarn workspaces foreach --all run test:e2e:ci",
"test:verbose": "yarn test --reporter verbose",
"test:watch": "vitest",
Expand Down
4 changes: 2 additions & 2 deletions packages/nodejs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"publish:preview": "yarn npm publish --tag preview",
"test": "vitest run --config vitest.config.ts",
"test:e2e": "vitest run --config vitest.config.e2e.ts",
"test:e2e:ci": "echo 'skipped tests' || ./scripts/test-e2e-ci.sh",
"test:e2e:ci": "./scripts/test-e2e-ci.sh",
"test:clean": "yarn test --no-cache --coverage.clean",
"test:dev": "yarn test --coverage false",
"test:verbose": "yarn test --reporter verbose",
Expand All @@ -47,6 +47,7 @@
"@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.6.2",
"@ts-bridge/shims": "^0.1.1",
"@types/better-sqlite3": "^7.6.12",
Expand Down Expand Up @@ -77,7 +78,6 @@
},
"dependencies": {
"@endo/promise-kit": "^1.1.6",
"@metamask/utils": "^11.0.1",
"@ocap/kernel": "workspace:^",
"@ocap/shims": "workspace:^",
"@ocap/streams": "workspace:^",
Expand Down
2 changes: 0 additions & 2 deletions packages/nodejs/src/env/kernel-worker-trusted-prelude.js

This file was deleted.

98 changes: 96 additions & 2 deletions packages/nodejs/src/kernel/VatWorkerService.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,106 @@
import '@ocap/shims/endoify';

import { describe, expect, it } from 'vitest';
import type { VatId } from '@ocap/kernel';
import { makeCounter } from '@ocap/utils';
import { describe, expect, it, vi } from 'vitest';

import { NodejsVatWorkerService } from './VatWorkerService.js';

const mocks = vi.hoisted(() => ({
worker: {
once: (_: string, callback: () => unknown) => {
callback();
},
terminate: vi.fn(async () => undefined),
},
stream: {
synchronize: vi.fn(async () => undefined).mockResolvedValue(undefined),
return: vi.fn(async () => ({})),
},
}));

vi.mock('@ocap/streams', () => ({
NodeWorkerDuplexStream: vi.fn(() => mocks.stream),
}));

vi.mock('node:worker_threads', () => ({
Worker: vi.fn(() => mocks.worker),
}));

describe('NodejsVatWorkerService', () => {
it('constructs an instance without any arguments', () => {
const instance = new NodejsVatWorkerService();
const instance = new NodejsVatWorkerService({});
expect(instance).toBeInstanceOf(NodejsVatWorkerService);
});

const workerFilePath = 'unused';
const vatIdCounter = makeCounter();
const getTestVatId = (): VatId => `v${vatIdCounter()}`;

describe('launch', () => {
it('creates a NodeWorker and returns a NodeWorkerDuplexStream', async () => {
const service = new NodejsVatWorkerService({
workerFilePath,
});
const testVatId: VatId = getTestVatId();
const stream = await service.launch(testVatId);

expect(stream).toStrictEqual(mocks.stream);
});

it('rejects if synchronize fails', async () => {
const rejected = 'test-reject-value';
mocks.stream.synchronize.mockRejectedValue(rejected);
const service = new NodejsVatWorkerService({ workerFilePath });
const testVatId: VatId = getTestVatId();
await expect(async () => await service.launch(testVatId)).rejects.toThrow(
rejected,
);
});
});

describe('terminate', () => {
it('terminates the target vat', async () => {
const service = new NodejsVatWorkerService({
workerFilePath,
});
const testVatId: VatId = getTestVatId();

await service.launch(testVatId);
expect(service.workers.has(testVatId)).toBe(true);

await service.terminate(testVatId);
expect(service.workers.has(testVatId)).toBe(false);
});

it('throws when terminating an unknown vat', async () => {
const service = new NodejsVatWorkerService({
workerFilePath,
});
const testVatId: VatId = getTestVatId();

await expect(
async () => await service.terminate(testVatId),
).rejects.toThrow(/No worker found/u);
});
});

describe('terminateAll', () => {
it('terminates all vats', async () => {
const service = new NodejsVatWorkerService({
workerFilePath,
});
const vatIds: VatId[] = [getTestVatId(), getTestVatId(), getTestVatId()];

await Promise.all(
vatIds.map(async (vatId) => await service.launch(vatId)),
);

expect(Array.from(service.workers.values())).toHaveLength(vatIds.length);

await service.terminateAll();

expect(Array.from(service.workers.values())).toHaveLength(0);
});
});
});
27 changes: 18 additions & 9 deletions packages/nodejs/src/kernel/VatWorkerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,16 @@ 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/vat-worker.mjs', import.meta.url)
.pathname;
const DEFAULT_WORKER_FILE = new URL(
'../../dist/vat/vat-worker.mjs',
import.meta.url,
).pathname;

export class NodejsVatWorkerService implements VatWorkerService {
readonly #logger: Logger;

readonly #workerFilePath: string;

workers = new Map<
VatId,
{ worker: NodeWorker; stream: DuplexStream<VatCommandReply, VatCommand> }
Expand All @@ -29,24 +33,29 @@ export class NodejsVatWorkerService implements VatWorkerService {
* 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]'.
* @param args - A bag of optional arguments.
* @param args.workerFilePath - An optional path to a file defining the worker's routine. Defaults to 'vat-worker.mjs'.
* @param args.logger - An optional {@link Logger}. Defaults to a new logger labeled '[vat worker client]'.
*/
constructor(logger?: Logger) {
this.#logger = logger ?? makeLogger('[vat worker service]');
constructor(args: {
workerFilePath?: string | undefined;
logger?: Logger | undefined;
}) {
this.#workerFilePath = args.workerFilePath ?? DEFAULT_WORKER_FILE;
this.#logger = args.logger ?? makeLogger('[vat worker service]');
}

async launch(
vatId: VatId,
): Promise<DuplexStream<VatCommandReply, VatCommand>> {
this.#logger.debug('launching vat', vatId);
const { promise, resolve, reject } =
makePromiseKit<DuplexStream<VatCommandReply, VatCommand>>();
this.#logger.debug('launching', vatId);
const worker = new NodeWorker(workerFileURL, {
const worker = new NodeWorker(this.#workerFilePath, {
env: {
NODE_VAT_ID: vatId,
},
});
this.#logger.debug('launched', vatId);
worker.once('online', () => {
const stream = new NodeWorkerDuplexStream<VatCommandReply, VatCommand>(
worker,
Expand All @@ -57,7 +66,7 @@ export class NodejsVatWorkerService implements VatWorkerService {
.synchronize()
.then(() => {
resolve(stream);
this.#logger.debug('connected', vatId);
this.#logger.debug('connected to kernel');
return undefined;
})
.catch((error) => {
Expand Down
87 changes: 0 additions & 87 deletions packages/nodejs/src/kernel/kernel-worker.ts

This file was deleted.

31 changes: 31 additions & 0 deletions packages/nodejs/src/kernel/make-kernel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import '@ocap/shims/endoify';

import { Kernel } from '@ocap/kernel';
import {
MessagePort as NodeMessagePort,
MessageChannel as NodeMessageChannel,
} from 'node:worker_threads';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { makeKernel } from './make-kernel.js';

vi.mock('./sqlite-kv-store.js', async () => {
const { makeMapKVStore } = await import('../../../kernel/test/storage.js');
return {
makeSQLKVStore: makeMapKVStore,
};
});

describe('makeKernel', () => {
let kernelPort: NodeMessagePort;

beforeEach(() => {
kernelPort = new NodeMessageChannel().port1;
});

it('should return a Kernel', async () => {
const kernel = await makeKernel(kernelPort);

expect(kernel).toBeInstanceOf(Kernel);
});
});
34 changes: 34 additions & 0 deletions packages/nodejs/src/kernel/make-kernel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { KernelCommand, KernelCommandReply } from '@ocap/kernel';
import { Kernel } from '@ocap/kernel';
import { NodeWorkerDuplexStream } from '@ocap/streams';
import { MessagePort as NodeMessagePort } from 'node: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
* @param workerFilePath - The path to a file defining each vat worker's routine.
* @returns The kernel, initialized.
*/
export async function makeKernel(
port: NodeMessagePort,
workerFilePath?: string,
): Promise<Kernel> {
const nodeStream = new NodeWorkerDuplexStream<
KernelCommand,
KernelCommandReply
>(port);
const vatWorkerClient = new NodejsVatWorkerService({ workerFilePath });

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

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

return kernel;
}
Loading