Skip to content

Commit 4491145

Browse files
committed
feat(xsnap): Add Node.js shell
1 parent 42912a7 commit 4491145

16 files changed

+1028
-0
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"packages/deployment",
4343
"packages/notifier",
4444
"packages/xs-vat-worker",
45+
"packages/xsnap",
4546
"packages/deploy-script-support"
4647
],
4748
"devDependencies": {

packages/xsnap/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
build

packages/xsnap/README.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# xsnap
2+
3+
Xsnap is a utility for taking resumable snapshots of a running JavaScript
4+
worker, using Moddable’s XS JavaScript engine.
5+
6+
Xsnap provides a Node.js API for controlling Xsnap workers.
7+
8+
```js
9+
const worker = xsnap();
10+
await worker.evaluate(`
11+
// Incrementer, running on XS.
12+
function answerSysCall(message) {
13+
const number = Number(String.fromArrayBuffer(message));
14+
return ArrayBuffer.fromString(String(number + 1));
15+
}
16+
`);
17+
await worker.snapshot('bootstrap.xss');
18+
await worker.close();
19+
```
20+
21+
Some time later, possibly on a different computer…
22+
23+
```js
24+
const decoder = new TextDecoder();
25+
const worker = xsnap({ snapshot: 'bootstrap.xss' });
26+
const answer = await worker.sysCall('1');
27+
console.log(decoder.decode(answer)); // 2
28+
await worker.close();
29+
```
30+
31+
The parent and child communicate using "syscalls".
32+
33+
- The XS child uses the synchronous `sysCall` function to send a request and
34+
receive as response from the Node.js parent.
35+
- The XS child can implement a synchronous `answserSysCall` function to respond
36+
to syscalls from the Node.js parent.
37+
- The Node.js parent uses an asynchronous `sysCall` method to send a request
38+
and receive a response from the XS child.
39+
- The Node.js parent can implement an asynchronous `answerSysCall` function to
40+
respond to syscalls from the XS child.

packages/xsnap/jsconfig.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// This file can contain .js-specific Typescript compiler config.
2+
{
3+
"compilerOptions": {
4+
"target": "esnext",
5+
6+
"noEmit": true,
7+
/*
8+
// The following flags are for creating .d.ts files:
9+
"noEmit": false,
10+
"declaration": true,
11+
"emitDeclarationOnly": true,
12+
*/
13+
"downlevelIteration": true,
14+
"strictNullChecks": true,
15+
"moduleResolution": "node",
16+
},
17+
"include": ["src/**/*.js", "exported.js", "tools/**/*.js"],
18+
}

packages/xsnap/package.json

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "@agoric/xsnap",
3+
"version": "0.0.0+1-dev",
4+
"description": "Description forthcoming.",
5+
"author": "Agoric",
6+
"license": "Apache-2.0",
7+
"parsers": {
8+
"js": "mjs"
9+
},
10+
"main": "./src/xsnap.js",
11+
"scripts": {
12+
"build": "node -r esm src/build.js",
13+
"clean": "rm -rf build",
14+
"lint": "yarn lint:js && yarn lint:types",
15+
"lint:js": "eslint 'src/**/*.js'",
16+
"lint:types": "tsc -p jsconfig.json",
17+
"lint-fix": "eslint --fix 'src/**/*.js'",
18+
"lint-check": "yarn lint",
19+
"test": "ava",
20+
"postinstall": "yarn build"
21+
},
22+
"dependencies": {},
23+
"devDependencies": {
24+
"@rollup/plugin-node-resolve": "^6.1.0",
25+
"ava": "^3.12.1",
26+
"esm": "^3.2.5",
27+
"rollup-plugin-terser": "^5.1.3"
28+
},
29+
"files": [
30+
"LICENSE*",
31+
"makefiles",
32+
"src"
33+
],
34+
"publishConfig": {
35+
"access": "public"
36+
},
37+
"eslintConfig": {
38+
"extends": [
39+
"@agoric"
40+
],
41+
"ignorePatterns": [
42+
"examples/**/*.js"
43+
]
44+
},
45+
"ava": {
46+
"files": [
47+
"test/**/test-*.js"
48+
],
49+
"require": [
50+
"esm"
51+
],
52+
"timeout": "2m"
53+
},
54+
"prettier": {
55+
"trailingComma": "all",
56+
"singleQuote": true
57+
}
58+
}

packages/xsnap/src/build.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as childProcess from 'child_process';
2+
import os from 'os';
3+
4+
function exec(command, cwd) {
5+
const child = childProcess.spawn(command, {
6+
cwd,
7+
stdio: ['inherit', 'inherit', 'inherit'],
8+
});
9+
return new Promise((resolve, reject) => {
10+
child.on('close', () => {
11+
resolve();
12+
});
13+
child.on('error', err => {
14+
reject(new Error(`${command} error ${err}`));
15+
});
16+
child.on('exit', code => {
17+
if (code !== 0) {
18+
reject(new Error(`${command} exited with code ${code}`));
19+
}
20+
});
21+
});
22+
}
23+
24+
(async () => {
25+
// Run command depending on the OS
26+
if (os.type() === 'Linux') {
27+
await exec('make', 'makefiles/lin');
28+
} else if (os.type() === 'Darwin') {
29+
await exec('make', 'makefiles/mac');
30+
} else if (os.type() === 'Windows_NT') {
31+
await exec('nmake', 'makefiles/win');
32+
} else {
33+
throw new Error(`Unsupported OS found: ${os.type()}`);
34+
}
35+
})();

packages/xsnap/src/defer.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// @ts-check
2+
3+
// eslint-disable-next-line jsdoc/require-returns-check
4+
/**
5+
* @param {boolean} _flag
6+
* @returns {asserts _flag}
7+
*/
8+
function assert(_flag) {}
9+
10+
/**
11+
* @template T
12+
* @typedef {{
13+
* resolve(value?: T | Promise<T>): void,
14+
* reject(error: Error): void,
15+
* promise: Promise<T>
16+
* }} Deferred
17+
*/
18+
19+
/**
20+
* @template T
21+
* @returns {Deferred<T>}
22+
*/
23+
export function defer() {
24+
let resolve;
25+
let reject;
26+
const promise = new Promise((res, rej) => {
27+
resolve = res;
28+
reject = rej;
29+
});
30+
assert(resolve !== undefined);
31+
assert(reject !== undefined);
32+
return { promise, resolve, reject };
33+
}

packages/xsnap/src/netstring.js

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// @ts-check
2+
3+
/**
4+
* @template T
5+
* @template U
6+
* @template V
7+
* @typedef {import('./stream.js').Stream<T, U, V>} Stream
8+
*/
9+
10+
const COLON = ':'.charCodeAt(0);
11+
const COMMA = ','.charCodeAt(0);
12+
13+
const decoder = new TextDecoder();
14+
const encoder = new TextEncoder();
15+
16+
/**
17+
* @param {AsyncIterable<Uint8Array>} input
18+
* @param {string=} name
19+
* @param {number=} capacity
20+
* @returns {AsyncIterableIterator<Uint8Array>} input
21+
*/
22+
export async function* reader(input, name = '<unknown>', capacity = 1024) {
23+
let length = 0;
24+
let buffer = new Uint8Array(capacity);
25+
let offset = 0;
26+
27+
for await (const chunk of input) {
28+
if (length + chunk.byteLength >= capacity) {
29+
while (length + chunk.byteLength >= capacity) {
30+
capacity *= 2;
31+
}
32+
const replacement = new Uint8Array(capacity);
33+
replacement.set(buffer, 0);
34+
buffer = replacement;
35+
}
36+
buffer.set(chunk, length);
37+
length += chunk.byteLength;
38+
39+
let drained = false;
40+
while (!drained && length > 0) {
41+
const colon = buffer.indexOf(COLON);
42+
if (colon === 0) {
43+
throw new Error(
44+
`Expected number before colon at offset ${offset} of ${name}`,
45+
);
46+
} else if (colon > 0) {
47+
const prefixBytes = buffer.subarray(0, colon);
48+
const prefixString = decoder.decode(prefixBytes);
49+
const contentLength = +prefixString;
50+
if (Number.isNaN(contentLength)) {
51+
throw new Error(
52+
`Invalid netstring prefix length ${prefixString} at offset ${offset} of ${name}`,
53+
);
54+
}
55+
const messageLength = colon + contentLength + 2;
56+
if (messageLength <= length) {
57+
yield buffer.subarray(colon + 1, colon + 1 + contentLength);
58+
buffer.copyWithin(0, messageLength);
59+
length -= messageLength;
60+
offset += messageLength;
61+
} else {
62+
drained = true;
63+
}
64+
} else {
65+
drained = true;
66+
}
67+
}
68+
}
69+
70+
if (length > 0) {
71+
throw new Error(
72+
`Unexpected dangling message at offset ${offset} of ${name}`,
73+
);
74+
}
75+
}
76+
77+
/**
78+
* @param {Stream<void, Uint8Array, void>} output
79+
* @returns {Stream<void, Uint8Array, void>}
80+
*/
81+
export function writer(output) {
82+
const scratch = new Uint8Array(8);
83+
84+
return {
85+
async next(message) {
86+
const { written: length = 0 } = encoder.encodeInto(
87+
`${message.byteLength}`,
88+
scratch,
89+
);
90+
scratch[length] = COLON;
91+
92+
const { done: done1 } = await output.next(
93+
scratch.subarray(0, length + 1),
94+
);
95+
if (done1) {
96+
return output.return();
97+
}
98+
99+
const { done: done2 } = await output.next(message);
100+
if (done2) {
101+
return output.return();
102+
}
103+
104+
scratch[0] = COMMA;
105+
return output.next(scratch.subarray(0, 1));
106+
},
107+
async return() {
108+
return output.return();
109+
},
110+
async throw(error) {
111+
return output.throw(error);
112+
},
113+
[Symbol.asyncIterator]() {
114+
return this;
115+
},
116+
};
117+
}

packages/xsnap/src/node-stream.js

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// @ts-check
2+
3+
/**
4+
* @template T
5+
* @template U
6+
* @template V
7+
* @typedef {import('./stream.js').Stream<T, U, V>} Stream
8+
*/
9+
10+
/**
11+
* @template T
12+
* @typedef {import('./defer.js').Deferred<T>} Deferred
13+
*/
14+
import { defer } from './defer';
15+
16+
const continues = { value: undefined };
17+
18+
/**
19+
* Adapts a Node.js writable stream to a JavaScript
20+
* async iterator of Uint8Array data chunks.
21+
* Back pressure emerges from awaiting on the promise
22+
* returned by `next` before calling `next` again.
23+
*
24+
* @param {NodeJS.WritableStream} output
25+
* @returns {Stream<void, Uint8Array, void>}
26+
*/
27+
export function writer(output) {
28+
/**
29+
* @type {Deferred<IteratorResult<void>>}
30+
*/
31+
let drained = defer();
32+
drained.resolve(continues);
33+
34+
output.on('error', err => {
35+
console.log('err', err);
36+
drained.reject(err);
37+
});
38+
39+
output.on('drain', () => {
40+
drained.resolve(continues);
41+
drained = defer();
42+
});
43+
44+
return {
45+
/**
46+
* @param {Uint8Array} [chunk]
47+
* @returns {Promise<IteratorResult<void>>}
48+
*/
49+
async next(chunk) {
50+
if (!chunk) {
51+
return continues;
52+
}
53+
if (!output.write(chunk)) {
54+
drained = defer();
55+
return drained.promise;
56+
}
57+
return continues;
58+
},
59+
async return() {
60+
output.end();
61+
return drained.promise;
62+
},
63+
async throw() {
64+
output.end();
65+
return drained.promise;
66+
},
67+
[Symbol.asyncIterator]() {
68+
return this;
69+
},
70+
};
71+
}

0 commit comments

Comments
 (0)