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
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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should have an issue to add a lint rule requiring such hygiene.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! Agoric had just such a lint rule, so we can grab the requisite eslint voodoo from the agoric-sdk repo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issues welcome 😄


'@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 @@
"@ocap/shims": "workspace:^",
"@ocap/streams": "workspace:^",
"@ocap/utils": "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 @@ -27,6 +27,13 @@ Object.defineProperties(globalThis.kernel, {
sendMessage: {
value: sendCommand,
},
kvGet: {
value: async (key: string) => sendCommand(CommandMethod.KVGet, key),
},
kvSet: {
value: async (key: string, value: string) =>
sendCommand(CommandMethod.KVSet, { key, value }),
},
});
harden(globalThis.kernel);

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

main().catch(console.error);

type Queue<Type> = Type[];

type VatId = `v${number}`;
type RemoteId = `r${number}`;
type EndpointId = VatId | RemoteId;

type RefTypeTag = 'o' | 'p';
type RefDirectionTag = '+' | '-';
type InnerKRef = `${RefTypeTag}${number}`;
type InnerERef = `${RefTypeTag}${RefDirectionTag}${number}`;

type KRef = `k${InnerKRef}`;
type VRef = `v${InnerERef}`;
type RRef = `r${InnerERef}`;
type ERef = VRef | RRef;

type CapData = {
body: string;
slots: string[];
};

type Message = {
target: ERef | KRef;
method: string;
params: CapData;
};

// Per-endpoint persistent state
type EndpointState<IdType> = {
name: string;
id: IdType;
nextExportObjectIdCounter: number;
nextExportPromiseIdCounter: number;
eRefToKRef: Map<ERef, KRef>;
kRefToERef: Map<KRef, ERef>;
};

type VatState = {
messagePort: MessagePort;
state: EndpointState<VatId>;
source: string;
kvTable: Map<string, string>;
};

type RemoteState = {
state: EndpointState<RemoteId>;
connectToURL: string;
// more here about maintaining connection...
};

// Kernel persistent state
type KernelObject = {
owner: EndpointId;
reachableCount: number;
recognizableCount: number;
};

type PromiseState = 'unresolved' | 'fulfilled' | 'rejected';

type KernelPromise = {
decider: EndpointId;
state: PromiseState;
referenceCount: number;
messageQueue: Queue<Message>;
value: undefined | CapData;
};

// export temporarily to shut up lint whinges about unusedness
export type KernelState = {
runQueue: Queue<Message>;
nextVatIdCounter: number;
vats: Map<VatId, VatState>;
nextRemoteIdCounter: number;
remotes: Map<RemoteId, RemoteState>;
nextKernelObjectIdCounter: number;
kernelObjects: Map<KRef, KernelObject>;
nextKernePromiseIdCounter: number;
kernelPromises: Map<KRef, KernelPromise>;
};

/**
* 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');
}

/**
* The main function for the offscreen script.
*/
async function main(): Promise<void> {
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();
}

// Handle messages from the console service worker
onmessage = async (event) => {
const message = event.data;
const { method, params } = message as Command;
console.log('received message: ', method, params);
switch (method) {
case CommandMethod.Evaluate:
reply(CommandMethod.Evaluate, await evaluate(params));
break;
case CommandMethod.CapTpCall: {
reply(
CommandMethod.CapTpCall,
'Error: CapTpCall not implemented here (yet)',
);
break;
}
case CommandMethod.CapTpInit:
reply(
CommandMethod.CapTpInit,
'Error: CapTpInit not implemented here (yet)',
);
break;
case CommandMethod.Ping:
reply(CommandMethod.Ping, 'pong');
break;
case CommandMethod.KVSet: {
const { key, value } = params;
kvSet(key, value);
reply(CommandMethod.KVSet, `~~~ set "${key}" to "${value}" ~~~`);
break;
}
case CommandMethod.KVGet: {
try {
const result = kvGet(params);
reply(CommandMethod.KVGet, result);
} catch (problem) {
reply(CommandMethod.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 method in message: "${method}"`,
);
}
};

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

/**
* 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 implemented here (yet)`;
}
}
50 changes: 49 additions & 1 deletion packages/extension/src/offscreen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Kernel } from '@ocap/kernel';
import { initializeMessageChannel } from '@ocap/streams';
import { CommandMethod } from '@ocap/utils';
import type { Command, CapTpPayload } from '@ocap/utils';

import { makeIframeVatWorker } from './makeIframeVatWorker.js';
import {
Expand All @@ -21,7 +22,41 @@ async function main(): Promise<void> {
worker: makeIframeVatWorker('default', initializeMessageChannel),
});

// Handle messages from the background service worker
const receiveFromKernel = async (event: MessageEvent): Promise<void> => {
// 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 Command;
const { method, params } = message;
let result: string;
const possibleError = params as unknown as Error;
if (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 = params as string;
}
await replyToCommand(method, result);
};

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(async (message: unknown) => {
if (!isExtensionRuntimeMessage(message)) {
Expand Down Expand Up @@ -64,6 +99,10 @@ async function main(): Promise<void> {
case CommandMethod.Ping:
await replyToCommand(CommandMethod.Ping, 'pong');
break;
case CommandMethod.KVGet:
case CommandMethod.KVSet:
sendKernelMessage(payload as unknown as CapTpPayload);
break;
default:
console.error(
// @ts-expect-error The type of `payload` is `never`, but this could happen at runtime.
Expand Down Expand Up @@ -114,4 +153,13 @@ async function main(): Promise<void> {
return `Error: Unknown error during evaluation.`;
}
}

/**
* Send a message to the kernel worker.
*
* @param payload - The message to send.
*/
function sendKernelMessage(payload: CapTpPayload): void {
kernelWorker.postMessage(payload);
}
}
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
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type {
CapTpMessage,
CapTpPayload,
Command,
CommandParams,
VatMessage,
} from './types.js';
export { CommandMethod } from './types.js';
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/type-guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const isCommand = (value: unknown): value is Command =>
typeof value.method === 'string' &&
(typeof value.params === 'string' ||
value.params === null ||
isObject(value.params) || // XXX certainly wrong, needs better TypeScript magic
isCapTpPayload(value.params));

export const isVatMessage = (value: unknown): value is VatMessage =>
Expand Down
8 changes: 6 additions & 2 deletions packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export enum CommandMethod {
CapTpInit = 'makeCapTp',
Evaluate = 'evaluate',
Ping = 'ping',
KVSet = 'kvSet',
KVGet = 'kvGet',
}

type CommandParams =
export type CommandParams =
| Primitive
| Promise<CommandParams>
| CommandParams[]
Expand All @@ -27,7 +29,9 @@ export type Command =
| CommandLike<CommandMethod.Ping, null | 'pong'>
| CommandLike<CommandMethod.Evaluate, string>
| CommandLike<CommandMethod.CapTpInit, null>
| CommandLike<CommandMethod.CapTpCall, CapTpPayload>;
| CommandLike<CommandMethod.CapTpCall, CapTpPayload>
| CommandLike<CommandMethod.KVGet, string>
| CommandLike<CommandMethod.KVSet, { key: string; value: string }>;

export type VatMessage = {
id: string;
Expand Down
Loading