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
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ module.exports = {
},
],

// This rule complains about hygienically necessary `await null`
'@typescript-eslint/await-thenable': 'off',

'@typescript-eslint/no-explicit-any': 'error',

// This rule is broken, and without the `allowAny` option, it reports a lot
Expand Down
1 change: 1 addition & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@metamask/utils": "^9.1.0",
"@ocap/shims": "workspace:^",
"@ocap/streams": "workspace:^",
"@sqlite.org/sqlite-wasm": "3.46.1-build3",
"ses": "^1.7.0"
},
"devDependencies": {
Expand Down
9 changes: 9 additions & 0 deletions packages/extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ Object.defineProperties(globalThis.kernel, {
sendMessage: {
value: sendMessage,
},
kvGet: {
value: async (key: string) => sendMessage(Command.KVGet, key),
},
kvSet: {
value: async (key: string, value: string) =>
sendMessage(Command.KVSet, { key, value }),
},
});
harden(globalThis.kernel);

Expand Down Expand Up @@ -78,6 +85,8 @@ chrome.runtime.onMessage.addListener(
case Command.CapTpCall:
case Command.CapTpInit:
case Command.Ping:
case Command.KVGet:
case Command.KVSet:
console.log(message.data);
break;
default:
Expand Down
187 changes: 187 additions & 0 deletions packages/extension/src/kernel-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import type { Database } from '@sqlite.org/sqlite-wasm';
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
import './endoify.js';

//import { IframeManager } from './iframe-manager.js';
import { IframeMessage, Command } from './message.js';
import { makeHandledCallback } from './shared.js';

main().catch(console.error);

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

/**
* The main function for the offscreen script.
*/
async function main(): Promise<void> {
// 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 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();
console.log(`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 {
console.log(`kernel set '${key}' to '${value}'`);
sqlKVSet.bind([key, value]);
sqlKVSet.step();
sqlKVSet.reset();
}

//await iframeReadyP;

// Handle messages from the console service worker
onmessage = async (event) => {
const message = event.data as IframeMessage;
const { type, data } = message;
console.log('received message: ', type, data);
switch (type) {
case Command.Evaluate:
reply(Command.Evaluate, await evaluate(data as string));
break;
case Command.CapTpCall: {
/*
const result = await iframeManager.callCapTp(
IFRAME_ID,
// @ts-expect-error TODO: Type assertions
data.method,
// @ts-expect-error TODO: Type assertions
...data.params,
);
reply(Command.CapTpCall, JSON.stringify(result, null, 2));
*/
break;
}
case Command.CapTpInit:
/*
await iframeManager.makeCapTp(IFRAME_ID);
reply(Command.CapTpInit, '~~~ CapTP Initialized ~~~');
*/
break;
case Command.Ping:
reply(Command.Ping, 'pong');
break;
case Command.KVSet: {
// TODO all this goofing around with type casts could be avoided by giving each Command value
// a type def for its params
const arg = data as Record<string, unknown>;
const key = arg.key as string;
const value = arg.value as string;
kvSet(key, value);
reply(Command.KVSet, `~~~ set "${key}" to "${value}" ~~~`);
break;
}
case Command.KVGet: {
try {
const result = kvGet(data as string);
reply(Command.KVGet, result);
} catch (problem) {
reply(Command.KVGet, problem as string); // cast is a lie, it really is an Error
}
break;
}
default:
console.error(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`kernel received unexpected message type: "${type}"`,
);
}
};

/**
* Reply to the background script.
*
* @param type - The message type.
* @param data - The message data.
*/
function reply(type: Command, data?: string): void {
postMessage({ type, data });
}

/**
* Evaluate a string in the default iframe.
*
* @param source - The source string to evaluate.
* @returns The result of the evaluation, or an error message.
*/
async function evaluate(source: string): Promise<string> {
return `Error: evaluate not yet implemented`;
/*
try {
const result = await iframeManager.sendMessage(IFRAME_ID, {
type: Command.Evaluate,
data: source,
});
return String(result);
} catch (error) {
if (error instanceof Error) {
return `Error: ${error.message}`;
}
return `Error: Unknown error during evaluation.`;
}
*/
}
}
6 changes: 5 additions & 1 deletion packages/extension/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isObject } from '@metamask/utils';

export type MessageId = string;

type DataObject =
export type DataObject =
| Primitive
| Promise<DataObject>
| DataObject[]
Expand All @@ -29,6 +29,8 @@ export enum Command {
CapTpInit = 'makeCapTp',
Evaluate = 'evaluate',
Ping = 'ping',
KVSet = 'kvSet',
KVGet = 'kvGet',
}

export type CapTpPayload = {
Expand All @@ -41,6 +43,8 @@ type CommandMessage<TargetType extends ExtensionMessageTarget> =
| CommandLike<Command.Evaluate, string, TargetType>
| CommandLike<Command.CapTpInit, null, TargetType>
| CommandLike<Command.CapTpCall, CapTpPayload, TargetType>;
| CommandLike<Command.KVGet, string, TargetType>
| CommandLike<Command.KVSet, { key: string, value: string }, TargetType>;

export type ExtensionMessage = CommandMessage<ExtensionMessageTarget>;
export type IframeMessage = CommandMessage<never>;
Expand Down
113 changes: 77 additions & 36 deletions packages/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { IframeManager } from './iframe-manager.js';
import type { ExtensionMessage } from './message.js';
import { Command, ExtensionMessageTarget } from './message.js';
import type { ExtensionMessage, IframeMessage } from './message.js';
import { Command, DataObject, ExtensionMessageTarget } from './message.js';
import { makeHandledCallback } from './shared.js';

main().catch(console.error);
Expand All @@ -16,43 +16,80 @@ async function main(): Promise<void> {
.create({ id: IFRAME_ID })
.then(async () => iframeManager.makeCapTp(IFRAME_ID));

// Handle messages from the background service worker
chrome.runtime.onMessage.addListener(
makeHandledCallback(async (message: ExtensionMessage) => {
if (message.target !== ExtensionMessageTarget.Offscreen) {
console.warn(
`Offscreen received message with unexpected target: "${message.target}"`,
);
return;
}
const receiveFromKernel = async (event: MessageEvent) => {
// For the time being, the only messages that come from the kernel worker are replies to actions
// initiated from the console, so just forward these replies to the console. This will need to
// change once this offscreen script is providing services to the kernel worker that don't
// involve the user (e.g., for things the worker can't do for itself, such as create an
// offscreen iframe).

// XXX TODO: Using the IframeMessage type here assumes that the set of response messages is the
// same as (and aligns perfectly with) the set of command messages, which is horribly, terribly,
// awfully wrong. Need to add types to account for the replies.
const message = event.data as IframeMessage;
const { type, data } = message;
let result: string;
const possibleError = data as unknown as Error;
if (possibleError && possibleError.message && possibleError.stack) {
// XXX TODO: The following is an egregious hack which is barely good enough for manual testing
// but not acceptable for serious use. We should be passing some kind of proper error
// indication back so that the recipient will experience a thrown exception or rejected
// promise, instead of having to look for a magic string. This is tolerable only so long as
// the sole eventual recipient is a human eyeball, and even then it's questionable.
result = `ERROR: ${possibleError.message}`;
} else {
result = data as string;
}
await reply(type, result);
};

const receiveFromControllerSW = async (message: ExtensionMessage) => {
if (message.target !== 'offscreen') {
console.warn(
`Offscreen received message with unexpected target: "${message.target}"`,
);
return;
}

await iframeReadyP;
await iframeReadyP;

switch (message.type) {
case Command.Evaluate:
await reply(Command.Evaluate, await evaluate(message.data));
break;
case Command.CapTpCall: {
const result = await iframeManager.callCapTp(IFRAME_ID, message.data);
await reply(Command.CapTpCall, JSON.stringify(result, null, 2));
break;
}
case Command.CapTpInit:
await iframeManager.makeCapTp(IFRAME_ID);
await reply(Command.CapTpInit, '~~~ CapTP Initialized ~~~');
break;
case Command.Ping:
await reply(Command.Ping, 'pong');
break;
default:
console.error(
// @ts-expect-error The type of `message` is `never`, but this could happen at runtime.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Offscreen received unexpected message type: "${message.type}"`,
);
switch (message.type) {
case Command.Evaluate:
await reply(Command.Evaluate, await evaluate(message.data));
break;
case Command.CapTpCall: {
const result = await iframeManager.callCapTp(IFRAME_ID, message.data);
await reply(Command.CapTpCall, JSON.stringify(result, null, 2));
break;
}
}),
);
case Command.CapTpInit:
await iframeManager.makeCapTp(IFRAME_ID);
await reply(Command.CapTpInit, '~~~ CapTP Initialized ~~~');
break;
case Command.Ping:
await reply(Command.Ping, 'pong');
break;
case Command.KVGet:
sendKernelMessage(message.type, message.data);
break;
case Command.KVSet:
sendKernelMessage(message.type, message.data);
break;
default:
console.error(
// @ts-expect-error The type of `message` is `never`, but this could happen at runtime.
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`Offscreen received unexpected message type: "${message.type}"`,
);
}
};

const kernelWorker = new Worker('kernel-worker.js', { type: 'module' });
kernelWorker.addEventListener('message', makeHandledCallback(receiveFromKernel));

// Handle messages from the background service worker, which for the time being stands in for the
// user console.
chrome.runtime.onMessage.addListener(makeHandledCallback(receiveFromControllerSW));

/**
* Reply to the background script.
Expand Down Expand Up @@ -88,4 +125,8 @@ async function main(): Promise<void> {
return `Error: Unknown error during evaluation.`;
}
}

function sendKernelMessage(type: string, data: DataObject): void {
kernelWorker.postMessage({ type, data });
};
}
1 change: 1 addition & 0 deletions packages/extension/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default defineConfig(({ mode }) => ({
rollupOptions: {
input: {
background: path.resolve(projectRoot, 'background.ts'),
'kernel-worker': path.resolve(projectRoot, 'kernel-worker.ts'),
offscreen: path.resolve(projectRoot, 'offscreen.html'),
iframe: path.resolve(projectRoot, 'iframe.html'),
},
Expand Down
Loading