Skip to content

Commit 3a4138a

Browse files
committed
feat: Wasm runtime inside a web worker (#37)
1 parent 0d750a3 commit 3a4138a

File tree

28 files changed

+1133
-404
lines changed

28 files changed

+1133
-404
lines changed

typescript/common/runtime/package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"@commontools/data": "^0.0.1",
3232
"@commontools/io": "^0.0.1",
3333
"@commontools/module": "^0.0.1",
34-
"@commontools/usuba-rt": "^0.0.1"
34+
"@commontools/usuba-rt": "^0.0.1",
35+
"ses": "^1.5.0"
3536
},
3637
"devDependencies": {
3738
"tslib": "^2.6.2",
@@ -55,6 +56,12 @@
5556
"command": "tsc --build -f"
5657
},
5758
"clean": {
59+
"dependencies": [
60+
"../../packages/usuba-rt:clean",
61+
"../data:clean",
62+
"../io:clean",
63+
"../module:clean"
64+
],
5865
"command": "rm -rf ./lib ./.wireit"
5966
}
6067
}

typescript/common/runtime/src/dictionary.ts renamed to typescript/common/runtime/src/common/data/dictionary.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {
22
Dictionary as CommonDictionary,
33
Value,
44
} from '@commontools/data/interfaces/common-data-types.js';
5-
import { IO as CommonIO } from './io.js';
5+
import { IO as CommonIO } from '../../state/io/index.js';
66
import { infer } from './infer.js';
77
import { Reference } from './reference.js';
88

typescript/common/runtime/src/reference.ts renamed to typescript/common/runtime/src/common/data/reference.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {
22
Value,
33
Reference as CommonReference,
44
} from '@commontools/data/interfaces/common-data-types.js';
5-
import { IO } from './io.js';
5+
import { IO } from '../../state/io/index.js';
66

77
export class Reference implements CommonReference {
88
#io;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export type ElementUnion<T extends readonly any[]> = T[number];
2+
3+
export type EventMap<Events> = {
4+
[E in keyof Events]: never;
5+
};
6+
7+
export const assertNever = (value: never): never => {
8+
throw new Error(`Unhandled value: ${value}`);
9+
};
10+
11+
export const downcast = <T>(value: unknown): T => {
12+
return value as T;
13+
};
14+
15+
export const isEvent = <AllEvents, Event extends AllEvents>(
16+
event: AllEvents,
17+
check: Event
18+
): event is Event => {
19+
return event === check;
20+
};
21+
22+
export const isError = (candidate: unknown): candidate is { error: string } => {
23+
return (
24+
candidate != null &&
25+
typeof candidate == 'object' &&
26+
'error' in candidate &&
27+
typeof candidate.error != 'undefined'
28+
);
29+
};
30+
31+
export const throwIfError = (
32+
candidate: unknown
33+
): candidate is { error: string } => {
34+
if (isError(candidate)) {
35+
throw new Error(candidate.error);
36+
}
37+
return false;
38+
};
Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,95 @@
1-
import { Runtime as UsubaRuntime } from '@commontools/usuba-rt';
2-
import { wit as commonDataWit } from '@commontools/data';
3-
import { wit as commonIoWit } from '@commontools/io';
4-
import { wit as commonModuleWit } from '@commontools/module';
5-
import type { Value } from '@commontools/data/interfaces/common-data-types.js';
6-
import type { IO } from './io.js';
7-
import { Reference } from './reference.js';
8-
import { Dictionary } from './dictionary.js';
1+
import { WorkerPool } from './worker/pool.js';
2+
import { Input } from './state/input.js';
3+
import { HostToModuleRPC } from './rpc/index.js';
4+
import { Output } from './state/output.js';
5+
import { assertNever, throwIfError } from './helpers.js';
6+
import { HostModuleEventHandler } from './rpc/host.js';
97

108
export type { Value } from '@commontools/data/interfaces/common-data-types.js';
119

12-
export * from './io.js';
13-
export * from './dictionary.js';
14-
export * from './reference.js';
15-
export * from './infer.js';
10+
export * from './state/io/index.js';
11+
export * from './state/input.js';
12+
export * from './state/output.js';
13+
export * from './state/storage/index.js';
14+
export * from './state/storage/localstorage.js';
15+
export * from './common/data/dictionary.js';
16+
export * from './common/data/reference.js';
17+
export * from './common/data/infer.js';
1618

1719
export type ContentType = 'text/javascript';
1820

19-
export interface Module {
20-
run: () => void;
21+
export const SES_SANDBOX = 'ses';
22+
export const WASM_SANDBOX = 'wasm';
23+
export const CONFIDENTIAL_COMPUTE_SANDBOX = 'confidential-compute';
24+
25+
export type Sandbox =
26+
| typeof SES_SANDBOX
27+
| typeof WASM_SANDBOX
28+
| typeof CONFIDENTIAL_COMPUTE_SANDBOX;
29+
30+
export class Module {
31+
#rpc;
32+
#input;
33+
34+
constructor(port: MessagePort, input: Input) {
35+
this.#rpc = new HostToModuleRPC(port, this.#handleModuleRPCEvent);
36+
this.#input = input;
37+
}
38+
39+
async run(): Promise<void> {
40+
throwIfError(await this.#rpc.send('module:run', undefined));
41+
}
42+
43+
output(keys: string[]) {
44+
return new Output(this.#rpc, keys);
45+
}
46+
47+
#handleModuleRPCEvent = (async (event, detail) => {
48+
switch (event) {
49+
case 'host:storage:read':
50+
try {
51+
return {
52+
value: await this.#input.read(detail.key),
53+
};
54+
} catch (error) {
55+
return {
56+
error: `${error}`,
57+
};
58+
}
59+
}
60+
61+
return assertNever(event as never);
62+
}) as HostModuleEventHandler;
2163
}
2264

2365
export class Runtime {
24-
#inner = new UsubaRuntime([commonDataWit, commonIoWit]);
66+
#workerPool = new WorkerPool();
2567

2668
async eval(
27-
contentType: ContentType,
69+
id: string,
70+
sandbox: Sandbox,
71+
contentType: 'text/javascript',
2872
sourceCode: string,
29-
io: IO
73+
input: Input
3074
): Promise<Module> {
31-
const blueprint = await this.#inner.defineModule<{
32-
module: { create(): { run: () => void } };
33-
}>({
34-
contentType,
35-
sourceCode,
36-
wit: commonModuleWit,
37-
});
38-
39-
const { module } = await blueprint.instantiate({
40-
'common:data/types': {
41-
Reference,
42-
Dictionary,
43-
},
44-
'common:io/state': {
45-
read(name: string) {
46-
return new Reference(io, name);
47-
},
48-
write(name: string, value: Value) {
49-
io.write(name, value);
75+
const { rpc: runtimeRpc } = await this.#workerPool.get(sandbox);
76+
const { port1: moduleTx, port2: moduleRx } = new MessageChannel();
77+
const module = new Module(moduleTx, input);
78+
79+
throwIfError(
80+
await runtimeRpc.send(
81+
'runtime:eval',
82+
{
83+
id,
84+
contentType,
85+
sourceCode,
86+
inputKeys: input.keys,
87+
port: moduleRx,
5088
},
51-
},
52-
});
89+
[moduleRx]
90+
)
91+
);
5392

54-
return module.create();
93+
return module;
5594
}
5695
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// import { Value } from '@commontools/data/interfaces/common-data-types.js';
2+
3+
import { ElementUnion, EventMap } from '../helpers.js';
4+
import { Value } from '../index.js';
5+
import { RPCEventHandler } from './index.js';
6+
7+
/**
8+
* Messages sent to the host runtime from a worker
9+
*/
10+
11+
export const HOST_WORKER_EVENTS = ['rpc:handshake:confirmed'] as const;
12+
13+
export type HostWorkerEvents = ElementUnion<typeof HOST_WORKER_EVENTS>;
14+
15+
export type HostWorkerRequests = EventMap<HostWorkerEvents> & {
16+
'rpc:handshake:confirmed': void;
17+
};
18+
19+
export type HostWorkerResponses = EventMap<HostWorkerEvents> & {
20+
'rpc:handshake:confirmed': void;
21+
};
22+
23+
export type HostWorkerEventHandler = RPCEventHandler<
24+
HostWorkerEvents,
25+
HostWorkerRequests,
26+
HostWorkerResponses
27+
>;
28+
29+
/**
30+
* Messages sent to the host runtime from a module-within-a-worker
31+
*/
32+
33+
export const HOST_MODULE_EVENTS = ['host:storage:read'] as const;
34+
35+
export type HostModuleEvents = ElementUnion<typeof HOST_MODULE_EVENTS>;
36+
37+
export type HostModuleRequests = EventMap<HostModuleEvents> & {
38+
'host:storage:read': {
39+
key: string;
40+
};
41+
};
42+
43+
export type HostModuleResponses = EventMap<HostModuleEvents> & {
44+
'host:storage:read':
45+
| {
46+
error: string;
47+
}
48+
| {
49+
value: Value;
50+
};
51+
};
52+
53+
export type HostModuleEventHandler = RPCEventHandler<
54+
HostModuleEvents,
55+
HostModuleRequests,
56+
HostModuleResponses
57+
>;

0 commit comments

Comments
 (0)