Skip to content
Merged
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
255 changes: 10 additions & 245 deletions packages/extension/src/kernel-worker.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,19 @@
import './kernel-worker-trusted-prelude.js';
import type { KernelCommand, KernelCommandReply, VatId } from '@ocap/kernel';
import { isKernelCommand, Kernel, KernelCommandMethod } from '@ocap/kernel';
import { Kernel } from '@ocap/kernel';
import { PostMessageDuplexStream, receiveMessagePort } from '@ocap/streams';
import { makeLogger, stringify } from '@ocap/utils';
import type { Database } from '@sqlite.org/sqlite-wasm';
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';

import { makeKernelStore } from './sqlite-kernel-store.js';
import { ExtensionVatWorkerClient } from './VatWorkerClient.js';

type MainArgs = { defaultVatId: VatId };

const logger = makeLogger('[kernel worker]');

main({ defaultVatId: 'v0' }).catch(console.error);
main('v0').catch(console.error);

/**
* Ensure that SQLite is initialized.
* The main function for the kernel worker.
*
* @returns The SQLite database object.
* @param defaultVatId - The id to give the default vat.
*/
async function initDB(): Promise<Database> {
const sqlite3 = await sqlite3InitModule();
if (sqlite3.oo1.OpfsDb) {
return new sqlite3.oo1.OpfsDb('/testdb.sqlite', 'cwt');
}
console.warn(`OPFS not enabled, database will be ephemeral`);
return new sqlite3.oo1.DB('/testdb.sqlite', 'cwt');
}

/**
* The main function for the offscreen script.
*
* @param options - The options bag.
* @param options.defaultVatId - The id to give the default vat.
*/
async function main({ defaultVatId }: MainArgs): Promise<void> {
async function main(defaultVatId: VatId): Promise<void> {
// Note we must setup the worker MessageChannel before initializing the stream,
// because the stream will close if it receives an unrecognized message.
const clientPort = await receiveMessagePort(
Expand All @@ -49,8 +28,6 @@ async function main({ defaultVatId }: MainArgs): Promise<void> {
},
);

const startTime = performance.now();

const kernelStream = new PostMessageDuplexStream<
KernelCommand,
KernelCommandReply
Expand All @@ -62,222 +39,10 @@ async function main({ defaultVatId }: MainArgs): Promise<void> {

// Initialize kernel store.

const { sqlKVGet, sqlKVSet } = await initDb();

// Create kernel.

const kernel = new Kernel(vatWorkerClient);
const vatReadyP = kernel.launchVat({ id: defaultVatId });

await reply({
method: KernelCommandMethod.InitKernel,
params: {
defaultVat: defaultVatId,
initTime: performance.now() - startTime,
},
});

// Handle messages from the console service worker
await kernelStream.drain(handleKernelCommand);

/**
* Handle a KernelCommand sent from the offscreen.
*
* @param command - The KernelCommand to handle.
*/
async function handleKernelCommand(command: KernelCommand): Promise<void> {
if (!isKernelCommand(command)) {
logger.error('Received unexpected message', command);
return;
}

const { method, params } = command;

switch (method) {
case KernelCommandMethod.InitKernel:
throw new Error('The kernel starts itself.');
case KernelCommandMethod.Ping:
await reply({ method, params: 'pong' });
break;
case KernelCommandMethod.Evaluate:
await handleVatTestCommand({ method, params });
break;
case KernelCommandMethod.CapTpCall:
await handleVatTestCommand({ method, params });
break;
case KernelCommandMethod.KVSet:
kvSet(params.key, params.value);
await reply({
method,
params: `~~~ set "${params.key}" to "${params.value}" ~~~`,
});
break;
case KernelCommandMethod.KVGet: {
try {
const result = kvGet(params);
await reply({
method,
params: result,
});
} catch (problem) {
// TODO: marshal
await reply({
method,
params: String(asError(problem)),
});
}
break;
}
default:
console.error(
'kernel worker received unexpected command',
// @ts-expect-error Runtime does not respect "never".
{ method: method.valueOf(), params },
);
}
}

/**
* Handle a command implemented by the test vat.
*
* @param command - The command to handle.
*/
async function handleVatTestCommand(
command: Extract<
KernelCommand,
| { method: typeof KernelCommandMethod.Evaluate }
| { method: typeof KernelCommandMethod.CapTpCall }
>,
): Promise<void> {
const { method, params } = command;
const vat = await vatReadyP;
switch (method) {
case KernelCommandMethod.Evaluate:
await reply({
method,
params: await evaluate(vat.id, params),
});
break;
case KernelCommandMethod.CapTpCall:
await reply({
method,
params: stringify(await vat.callCapTp(params)),
});
break;
default:
console.error(
'Offscreen received unexpected vat command',
// @ts-expect-error Runtime does not respect "never".
{ method: method.valueOf(), params },
);
}
}

/**
* Reply to the background script.
*
* @param payload - The payload to reply with.
*/
async function reply(payload: KernelCommandReply): Promise<void> {
await kernelStream.write(payload);
}

/**
* 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(vatId: VatId, source: string): Promise<string> {
try {
const result = await kernel.sendMessage(vatId, {
method: KernelCommandMethod.Evaluate,
params: source,
});
return String(result);
} catch (error) {
if (error instanceof Error) {
return `Error: ${error.message}`;
}
return `Error: Unknown error during evaluation.`;
}
}

/**
* Coerce an unknown problem into an Error object.
*
* @param problem - Whatever was caught.
* @returns The problem if it is an Error, or a new Error with the problem as the cause.
*/
function asError(problem: unknown): Error {
return problem instanceof Error
? problem
: new Error('Unknown', { cause: problem });
}

/**
* Initialize the database and some prepared statements.
*
* @returns The prepared database statements.
*/
async function initDb(): Promise<{
sqlKVGet: ReturnType<typeof db.prepare>;
sqlKVSet: ReturnType<typeof db.prepare>;
}> {
const db = await initDB();
db.exec(`
CREATE TABLE IF NOT EXISTS kv (
key TEXT,
value TEXT,
PRIMARY KEY(key)
)
`);

return {
sqlKVGet: db.prepare(`
SELECT value
FROM kv
WHERE key = ?
`),
sqlKVSet: db.prepare(`
INSERT INTO kv (key, value)
VALUES (?, ?)
ON CONFLICT DO UPDATE SET value = excluded.value
`),
};
}
const kernelStore = await makeKernelStore();

/**
* Exercise reading from the database.
*
* @param key - A key to fetch.
* @returns The value at that key.
*/
function kvGet(key: string): string {
sqlKVGet.bind([key]);
if (sqlKVGet.step()) {
const result = sqlKVGet.getString(0);
if (result) {
sqlKVGet.reset();
console.log(`kernel get '${key}' as '${result}'`);
return result;
}
}
sqlKVGet.reset();
throw Error(`no record matching key '${key}'`);
}
// Create and start kernel.

/**
* Exercise writing to the database.
*
* @param key - A key to assign.
* @param value - The value to assign to it.
*/
function kvSet(key: string, value: string): void {
console.log(`kernel set '${key}' to '${value}'`);
sqlKVSet.bind([key, value]);
sqlKVSet.step();
sqlKVSet.reset();
}
const kernel = new Kernel(kernelStream, vatWorkerClient, kernelStore);
await kernel.init({ defaultVatId });
}
2 changes: 1 addition & 1 deletion packages/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ async function main(): Promise<void> {
// involve the user.
for await (const message of workerStream) {
if (!isKernelCommandReply(message)) {
logger.error('Kernel received unexpected message', message);
logger.error('Kernel sent unexpected reply', message);
continue;
}

Expand Down
89 changes: 89 additions & 0 deletions packages/extension/src/sqlite-kernel-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type { KernelStore } from '@ocap/kernel';
import { makeLogger } from '@ocap/utils';
import type { Database } from '@sqlite.org/sqlite-wasm';
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';

/**
* Ensure that SQLite is initialized.
*
* @returns The SQLite database object.
*/
async function initDB(): Promise<Database> {
const sqlite3 = await sqlite3InitModule();
if (sqlite3.oo1.OpfsDb) {
return new sqlite3.oo1.OpfsDb('/testdb.sqlite', 'cwt');
}
console.warn(`OPFS not enabled, database will be ephemeral`);
return new sqlite3.oo1.DB('/testdb.sqlite', 'cwt');
}

/**
* Makes a {@link KernelStore} for persistent storage.
*
* @param label - A logger prefix label. Defaults to '[sqlite]'.
* @returns The kernel store.
*/
export async function makeKernelStore(
label: string = '[sqlite]',
): Promise<KernelStore> {
const logger = makeLogger(label);
const db = await initDB();

db.exec(`
CREATE TABLE IF NOT EXISTS kv (
key TEXT,
value TEXT,
PRIMARY KEY(key)
)
`);

const sqlKVGet = db.prepare(`
SELECT value
FROM kv
WHERE key = ?
`);

/**
* Exercise reading from the database.
*
* @param key - A key to fetch.
* @returns The value at that key.
*/
function kvGet(key: string): string {
sqlKVGet.bind([key]);
if (sqlKVGet.step()) {
const result = sqlKVGet.getString(0);
if (result) {
sqlKVGet.reset();
logger.debug(`kernel get '${key}' as '${result}'`);
return result;
}
}
sqlKVGet.reset();
throw Error(`no record matching key '${key}'`);
}

const sqlKVSet = db.prepare(`
INSERT INTO kv (key, value)
VALUES (?, ?)
ON CONFLICT DO UPDATE SET value = excluded.value
`);

/**
* Exercise writing to the database.
*
* @param key - A key to assign.
* @param value - The value to assign to it.
*/
function kvSet(key: string, value: string): void {
logger.debug(`kernel set '${key}' to '${value}'`);
sqlKVSet.bind([key, value]);
sqlKVSet.step();
sqlKVSet.reset();
}

return {
kvGet,
kvSet,
};
}
Loading