diff --git a/bin/wasm-node/CHANGELOG.md b/bin/wasm-node/CHANGELOG.md
index da8f195b4f..015645f2ca 100644
--- a/bin/wasm-node/CHANGELOG.md
+++ b/bin/wasm-node/CHANGELOG.md
@@ -2,6 +2,10 @@
## Unreleased
+### Changed
+
+- No WebWorker/worker thread is spawned anymore by the JavaScript code. The WebAssembly virtual machine that runs smoldot is now directly instantiated by the `start` function. This should fix compatibility issues with various JavaScript bundlers. ([#2498](https://github.com/paritytech/smoldot/pull/2498))
+
## 0.6.23 - 2022-07-11
### Fixed
diff --git a/bin/wasm-node/javascript/README.md b/bin/wasm-node/javascript/README.md
index 3c211dd6f4..c9df517ff5 100644
--- a/bin/wasm-node/javascript/README.md
+++ b/bin/wasm-node/javascript/README.md
@@ -73,22 +73,3 @@ chains must be passed as parameter to `addChain` as well. In situations where th
specifications passed to `addChain` are not trusted, it is important for security reasons to not
establish a parachain-relay-chain link between two chains that aren't part of the same "trust
sandbox".
-
-# About the worker
-
-The code in this package uses a web worker (in browsers) or a worker thread (on NodeJS). The
-line of JavaScript that creates the worker is of the following form:
-
-``` js
-new Worker(new URL('./worker.js', import.meta.url), { type: "module" });
-```
-
-This format is compatible [with Webpack 5](https://webpack.js.org/guides/web-workers/), meaning
-that Webpack will be able to resolve the imports in `worker.js` and adjust this little snippet.
-
-This format also works in NodeJS without any issue.
-
-However, at the time of writing of this comment, this format doesn't work with Parcel (both 1 and
-2) due to various bugs.
-
-As a general warning, be aware of the fact that this line might cause issues if you use a bundler.
diff --git a/bin/wasm-node/javascript/package.json b/bin/wasm-node/javascript/package.json
index 8408cfc547..7c29e47b25 100644
--- a/bin/wasm-node/javascript/package.json
+++ b/bin/wasm-node/javascript/package.json
@@ -23,8 +23,7 @@
"test": "node prepare.js --debug && rimraf ./dist && tsc && ava --timeout=2m --concurrency 2 --no-worker-threads"
},
"browser": {
- "./dist/compat/index.js": "./dist/compat/index-browser-overwrite.js",
- "./dist/worker/spawn.js": "./dist/worker/spawn-browser-overwrite.js"
+ "./dist/compat/index.js": "./dist/compat/index-browser-overwrite.js"
},
"dependencies": {
"buffer": "^6.0.1",
diff --git a/bin/wasm-node/javascript/prepare.js b/bin/wasm-node/javascript/prepare.js
index f410aa2cf5..42acf56d49 100755
--- a/bin/wasm-node/javascript/prepare.js
+++ b/bin/wasm-node/javascript/prepare.js
@@ -69,13 +69,13 @@ child_process.execSync(
{ 'stdio': 'inherit' }
);
-// The code below will write a variable number of files to the `src/worker/autogen` directory.
+// The code below will write a variable number of files to the `src/instance/autogen` directory.
// Start by clearing all existing files from this directory in case there are some left from past
// builds.
-const filesToRemove = fs.readdirSync('./src/worker/autogen');
+const filesToRemove = fs.readdirSync('./src/instance/autogen');
for (const file of filesToRemove) {
if (!file.startsWith('.')) // Don't want to remove the `.gitignore` or `.npmignore` or similar
- fs.unlinkSync(path.join("./src/worker/autogen", file));
+ fs.unlinkSync(path.join("./src/instance/autogen", file));
}
// We then do an optimization pass on the Wasm file, using `wasm-opt`.
@@ -119,13 +119,13 @@ try {
const chunk = base64Data.slice(0, 1024 * 1024);
// We could simply export the chunk instead of a function that returns the chunk, but that
// would cause TypeScript to generate a definitions file containing a copy of the entire chunk.
- fs.writeFileSync('./src/worker/autogen/wasm' + fileNum + '.ts', 'export default function(): string { return "' + chunk + '"; }');
+ fs.writeFileSync('./src/instance/autogen/wasm' + fileNum + '.ts', 'export default function(): string { return "' + chunk + '"; }');
imports += 'import { default as wasm' + fileNum + ' } from \'./wasm' + fileNum + '.js\';\n';
chunksSum += ' + wasm' + fileNum + '()';
fileNum += 1;
base64Data = base64Data.slice(1024 * 1024);
}
- fs.writeFileSync('./src/worker/autogen/wasm.ts', imports + 'export default ' + chunksSum + ';');
+ fs.writeFileSync('./src/instance/autogen/wasm.ts', imports + 'export default ' + chunksSum + ';');
} finally {
fs.rmSync(tmpDir, { recursive: true });
diff --git a/bin/wasm-node/javascript/src/client.ts b/bin/wasm-node/javascript/src/client.ts
index 6da0617b8e..fdfa774c3d 100644
--- a/bin/wasm-node/javascript/src/client.ts
+++ b/bin/wasm-node/javascript/src/client.ts
@@ -15,9 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
-import { CompatWorker, workerOnMessage, workerOnError, workerTerminate } from './compat/index.js';
-import * as messages from './worker/messages.js';
-import spawnWorker from './worker/spawn.js';
+import { start as startInstance } from './instance/instance.js';
/**
* Thrown in case of a problem when initializing the chain.
@@ -365,207 +363,20 @@ export function start(options?: ClientOptions): Client {
}
});
- // The actual execution of Smoldot is performed in a worker thread.
- // Because this specific line of code is a bit sensitive, it is done in a separate file.
- const worker = spawnWorker();
- let workerError: null | Error = null;
-
- // Whenever an `addChain` or `removeChain` message is sent to the worker, a corresponding entry
- // is pushed to this array. The worker needs to send back a confirmation, which pops the first
- // element of this array. In the case of `addChain`, additional fields are stored in this array
- // to finish the initialization of the chain.
- let pendingConfirmations: PendingConfirmation[] = [];
-
- // Contains the information of each chain that is currently.
- // Entries are instantly removed when the user desires to remove a chain even before the worker
- // has confirmed the removal. Doing so avoids a race condition where the worker sends back a
- // database content or a JSON-RPC response/notification even though we've already sent a
- // `removeChain` message to it.
- //
- // This map is also used in general as a way to check whether a chain still exists.
- let chains: Map = new Map();
-
// For each chain object returned by `addChain`, the associated internal chain id.
//
// Immediately cleared when `remove()` is called on a chain.
let chainIds: WeakMap = new WeakMap();
- // The worker periodically reports the name of the task it is currently in. This makes it
- // possible, when the worker is frozen, to know which task it was in when frozen.
- const workerCurrentTask: { name: string | null } = { name: null };
-
- // The worker periodically sends a message of kind 'livenessPing' in order to notify that it is
- // still alive.
- // If this liveness ping isn't received for a long time, an error is reported in the logs.
- // The first check is delayed in order to account for the fact that the worker has to perform
- // an expensive initialization step when initializing the Wasm VM.
- let livenessTimeout: null | ReturnType = null;
- const resetLivenessTimeout = () => {
- if (livenessTimeout !== null)
- globalThis.clearTimeout(livenessTimeout);
- livenessTimeout = globalThis.setTimeout(() => {
- livenessTimeout = null;
- if (workerError)
- return; // The unresponsiveness is due to a crash. No need to print more warnings.
- console.warn(
- "Smoldot appears unresponsive" +
- (workerCurrentTask.name ? (" while executing task `" + workerCurrentTask.name + "`") : "") +
- ". Please open an issue at https://github.com/paritytech/smoldot/issues. If you have a " +
- "debugger available, please pause execution, generate a stack trace of the thread " +
- "that isn't the main execution thread, and paste it in the issue. Please also include " +
- "any other log found in the console or elsewhere."
- );
- }, 10000);
- };
- globalThis.setTimeout(() => resetLivenessTimeout(), 15000);
-
- // The worker can send us messages whose type is identified through a `kind` field.
- workerOnMessage(worker, (message: messages.FromWorker): void => {
- switch (message.kind) {
- case 'jsonrpc': {
- const cb = chains.get(message.chainId)?.jsonRpcCallback;
- if (cb) cb(message.data);
- break;
- }
-
- case 'chainAddedOk': {
- const expected = pendingConfirmations.shift()!;
- const chainId = message.chainId;
-
- if (chains.has(chainId)) // Sanity check.
- throw 'Unexpected reuse of a chain ID';
- chains.set(chainId, {
- jsonRpcCallback: expected.jsonRpcCallback,
- databasePromises: new Array()
- });
-
- // `expected` was pushed by the `addChain` method.
- // Resolve the promise that `addChain` returned to the user.
- const newChain: Chain = {
- sendJsonRpc: (request) => {
- if (workerError)
- throw workerError;
- if (!chains.has(chainId))
- throw new AlreadyDestroyedError();
- if (!(chains.get(chainId)?.jsonRpcCallback))
- throw new JsonRpcDisabledError();
- if (request.length >= 8 * 1024 * 1024)
- return;
- postMessage(worker, { ty: 'request', request, chainId });
- },
- databaseContent: (maxUtf8BytesSize) => {
- if (workerError)
- return Promise.reject(workerError);
-
- const databaseContentPromises = chains.get(chainId)?.databasePromises;
- if (!databaseContentPromises)
- return Promise.reject(new AlreadyDestroyedError());
-
- const promise: Promise = new Promise((resolve, reject) => {
- databaseContentPromises.push({ resolve, reject });
- });
-
- const twoPower32 = (1 << 30) * 4; // `1 << 31` and `1 << 32` in JavaScript don't give the value that you expect.
- const maxSize = maxUtf8BytesSize || (twoPower32 - 1);
- const cappedMaxSize = (maxSize >= twoPower32) ? (twoPower32 - 1) : maxSize;
-
- postMessage(worker, { ty: 'databaseContent', chainId, maxUtf8BytesSize: cappedMaxSize });
-
- return promise;
- },
- remove: () => {
- if (workerError)
- throw workerError;
- // Because the `removeChain` message is asynchronous, it is possible for a JSON-RPC
- // response or database content concerning that `chainId` to arrive after the `remove`
- // function has returned. We solve that by removing the information immediately.
- if (!chains.delete(chainId))
- throw new AlreadyDestroyedError();
- console.assert(chainIds.has(newChain));
- chainIds.delete(newChain);
- postMessage(worker, { ty: 'removeChain', chainId });
- },
- };
-
- chainIds.set(newChain, chainId);
- expected.resolve(newChain);
- break;
- }
-
- case 'chainAddedErr': {
- const expected = pendingConfirmations.shift()!;
- // `expected` was pushed by the `addChain` method.
- // Reject the promise that `addChain` returned to the user.
- expected.reject(new AddChainError(message.error));
- break;
- }
-
- case 'databaseContent': {
- const promises = chains.get(message.chainId)?.databasePromises;
- if (promises) (promises.shift() as DatabasePromise).resolve(message.data);
- break;
- }
+ // If `Client.terminate()̀ is called, this error is set to a value.
+ // All the functions of the public API check if this contains a value.
+ let alreadyDestroyedError: null | AlreadyDestroyedError = null;
- case 'log': {
- logCallback(message.level, message.target, message.message);
- break;
- }
-
- case 'livenessPing': {
- resetLivenessTimeout();
- break;
- }
-
- case 'currentTask': {
- workerCurrentTask.name = message.taskName;
- break;
- }
-
- default: {
- // Exhaustive check.
- const _exhaustiveCheck: never = message;
- return _exhaustiveCheck;
- }
- }
- });
-
- workerOnError(worker, (error) => {
- // A worker error should only happen in case of a critical error as the result of a bug
- // somewhere. Consequently, nothing is really in place to cleanly report the error.
- const errorToString = error.toString();
- console.error(
- "Smoldot has panicked" +
- (workerCurrentTask.name ? (" while executing task `" + workerCurrentTask.name + "`") : "") +
- ". This is a bug in smoldot. Please open an issue at " +
- "https://github.com/paritytech/smoldot/issues with the following message:\n" +
- errorToString
- );
- workerError = new CrashError(errorToString);
-
- // Reject all promises returned by `addChain`.
- for (var pending of pendingConfirmations) {
- if (pending.ty == 'chainAdded')
- pending.reject(workerError);
- }
- pendingConfirmations = [];
-
- // Reject all promises for database contents.
- for (const chain of chains) {
- for (const promise of chain[1].databasePromises) {
- promise.reject(workerError)
- }
- }
- chains.clear();
- });
-
- // The first message expected by the worker contains the configuration.
- postMessage(worker, {
+ const instance = startInstance({
// Maximum level of log entries sent by the client.
// 0 = Logging disabled, 1 = Error, 2 = Warn, 3 = Info, 4 = Debug, 5 = Trace
maxLogLevel: options.maxLogLevel || 3,
+ logCallback,
// `enableCurrentTask` adds a small performance hit, but adds some additional information to
// crash reports. Whether this should be enabled is very opiniated and not that important. At
// the moment, we enable it all the time, except if the user has logging disabled altogether.
@@ -578,9 +389,9 @@ export function start(options?: ClientOptions): Client {
});
return {
- addChain: (options: AddChainOptions): Promise => {
- if (workerError)
- throw workerError;
+ addChain: async (options: AddChainOptions): Promise => {
+ if (alreadyDestroyedError)
+ throw alreadyDestroyedError;
// Passing a JSON object for the chain spec is an easy mistake, so we provide a more
// readable error.
@@ -599,58 +410,55 @@ export function start(options?: ClientOptions): Client {
}
}
- // Build a promise that will be resolved or rejected after the chain has been added.
- // TODO: because of https://github.com/microsoft/TypeScript/issues/11498 we need to define the callbacks as possibly null, and go through `unknown`
- let chainAddedPromiseResolve;
- let chainAddedPromiseReject;
- const chainAddedPromise: Promise = new Promise((resolve, reject) => {
- chainAddedPromiseResolve = resolve;
- chainAddedPromiseReject = reject;
- });
-
- pendingConfirmations.push({
- ty: 'chainAdded',
- reject: chainAddedPromiseReject as unknown as (error: AddChainError) => void,
- resolve: chainAddedPromiseResolve as unknown as (c: Chain) => void,
- jsonRpcCallback: options.jsonRpcCallback,
- });
-
- postMessage(worker, {
- ty: 'addChain',
- chainSpec: options.chainSpec,
- databaseContent: typeof options.databaseContent === 'string' ? options.databaseContent : "",
- potentialRelayChains: potentialRelayChainsIds,
- jsonRpcRunning: !!options.jsonRpcCallback,
- });
-
- return chainAddedPromise;
+ const outcome = await instance.addChain(options.chainSpec, typeof options.databaseContent === 'string' ? options.databaseContent : "", potentialRelayChainsIds, options.jsonRpcCallback);
+
+ if (!outcome.success)
+ throw new AddChainError(outcome.error);
+
+ const chainId = outcome.chainId;
+ const wasDestroyed = { destroyed: false };
+
+ // `expected` was pushed by the `addChain` method.
+ // Resolve the promise that `addChain` returned to the user.
+ const newChain: Chain = {
+ sendJsonRpc: (request) => {
+ if (alreadyDestroyedError)
+ throw alreadyDestroyedError;
+ if (wasDestroyed.destroyed)
+ throw new AlreadyDestroyedError();
+ if (!options.jsonRpcCallback)
+ throw new JsonRpcDisabledError();
+ if (request.length >= 8 * 1024 * 1024)
+ return;
+ instance.request(request, chainId);
+ },
+ databaseContent: (maxUtf8BytesSize) => {
+ if (alreadyDestroyedError)
+ return Promise.reject(alreadyDestroyedError);
+ if (wasDestroyed.destroyed)
+ throw new AlreadyDestroyedError();
+ return instance.databaseContent(chainId, maxUtf8BytesSize);
+ },
+ remove: () => {
+ if (alreadyDestroyedError)
+ throw alreadyDestroyedError;
+ if (wasDestroyed.destroyed)
+ throw new AlreadyDestroyedError();
+ wasDestroyed.destroyed = true;
+ console.assert(chainIds.has(newChain));
+ chainIds.delete(newChain);
+ instance.removeChain(chainId);
+ },
+ };
+
+ chainIds.set(newChain, chainId);
+ return newChain;
},
- terminate: () => {
- if (workerError)
- return Promise.reject(workerError)
- workerError = new AlreadyDestroyedError();
-
- if (livenessTimeout !== null)
- globalThis.clearTimeout(livenessTimeout)
-
- return workerTerminate(worker)
+ terminate: async () => {
+ if (alreadyDestroyedError)
+ throw alreadyDestroyedError
+ alreadyDestroyedError = new AlreadyDestroyedError();
+ instance.startShutdown()
}
}
}
-
-// Separate function in order to enforce types.
-function postMessage(worker: CompatWorker, message: messages.ToWorker) {
- worker.postMessage(message)
-}
-
-interface PendingConfirmation {
- ty: 'chainAdded',
- resolve: (c: Chain) => void,
- reject: (error: AddChainError) => void,
- jsonRpcCallback?: JsonRpcCallback,
-}
-
-interface DatabasePromise {
- resolve: (data: string) => void,
- reject: (error: Error) => void,
-}
diff --git a/bin/wasm-node/javascript/src/compat/index-browser-overwrite.js b/bin/wasm-node/javascript/src/compat/index-browser-overwrite.js
index 96844eb5d3..962cd65fc1 100644
--- a/bin/wasm-node/javascript/src/compat/index-browser-overwrite.js
+++ b/bin/wasm-node/javascript/src/compat/index-browser-overwrite.js
@@ -17,38 +17,6 @@
// Overrides `index.js` when in a browser.
-export function workerOnMessage(worker, callback) {
- worker.onmessage = (event) => callback(event.data)
-}
-
-export function workerOnError(worker, callback) {
- worker.onerror = (event) => {
- // For reference:
- // https://html.spec.whatwg.org/multipage/indices.html#event-error
- // https://html.spec.whatwg.org/multipage/webappapis.html#errorevent
-
- // If `event.error` exists, then it will likely be an instance of `Error`.
- // However, that's not guaranteed by the spec and it could also be
- // something else. So, our second best try is the `event.message`
- // property. Finally, our last resort is to create an Error from the event.
- if (event.error instanceof Error) callback(event.error);
- else callback(new Error(event.message || event));
- }
-}
-
-export function workerTerminate(worker) {
- worker.terminate();
- return Promise.resolve();
-}
-
-export function postMessage(msg) {
- self.postMessage(msg);
-}
-
-export function setOnMessage(callback) {
- self.onmessage = (event) => callback(event.data);
-}
-
export function performanceNow() {
return performance.now()
}
diff --git a/bin/wasm-node/javascript/src/compat/index.d.ts b/bin/wasm-node/javascript/src/compat/index.d.ts
index f8abcd1a8e..5c752022c1 100644
--- a/bin/wasm-node/javascript/src/compat/index.d.ts
+++ b/bin/wasm-node/javascript/src/compat/index.d.ts
@@ -22,20 +22,6 @@ import type { Socket as TcpSocket, NetConnectOpts } from 'node:net';
export type WasmModuleImports = WebAssembly.ModuleImports;
-export class CompatWorker {
- postMessage(value: any, transferList?: ReadonlyArray): void;
-}
-
-export function workerOnMessage(worker: CompatWorker, callback: (value: any) => void): void;
-
-export function workerOnError(worker: CompatWorker, callback: (err: Error) => void): void;
-
-export function workerTerminate(worker: CompatWorker): Promise;
-
-export function postMessage(message: any): void;
-
-export function setOnMessage(callback: (message: any) => void): void;
-
export function performanceNow(): number;
export function isTcpAvailable(): boolean;
diff --git a/bin/wasm-node/javascript/src/compat/index.js b/bin/wasm-node/javascript/src/compat/index.js
index e3d7aa13da..bc86e6a468 100644
--- a/bin/wasm-node/javascript/src/compat/index.js
+++ b/bin/wasm-node/javascript/src/compat/index.js
@@ -19,31 +19,10 @@
//
// A rule in the `package.json` overrides it with `index-browser-override.js` when in a browser.
-import { parentPort } from 'node:worker_threads';
import { hrtime } from 'node:process';
import { createConnection as nodeCreateConnection } from 'node:net';
import { randomFillSync } from 'node:crypto';
-export function workerOnMessage(worker, callback) {
- worker.on('message', callback);
-}
-
-export function workerOnError(worker, callback) {
- worker.on('error', callback);
-}
-
-export function workerTerminate(worker) {
- return worker.terminate().then(() => { });
-}
-
-export function postMessage(msg) {
- parentPort.postMessage(msg);
-}
-
-export function setOnMessage(callback) {
- parentPort.on('message', callback);
-}
-
export function performanceNow() {
const time = hrtime();
return ((time[0] * 1e3) + (time[1] / 1e6));
diff --git a/bin/wasm-node/javascript/src/worker/autogen/.gitignore b/bin/wasm-node/javascript/src/instance/autogen/.gitignore
similarity index 100%
rename from bin/wasm-node/javascript/src/worker/autogen/.gitignore
rename to bin/wasm-node/javascript/src/instance/autogen/.gitignore
diff --git a/bin/wasm-node/javascript/src/worker/bindings-smoldot-light.ts b/bin/wasm-node/javascript/src/instance/bindings-smoldot-light.ts
similarity index 77%
rename from bin/wasm-node/javascript/src/worker/bindings-smoldot-light.ts
rename to bin/wasm-node/javascript/src/instance/bindings-smoldot-light.ts
index 5feabf0134..7c3fae321a 100644
--- a/bin/wasm-node/javascript/src/worker/bindings-smoldot-light.ts
+++ b/bin/wasm-node/javascript/src/instance/bindings-smoldot-light.ts
@@ -27,6 +27,14 @@ import type { SmoldotWasmInstance } from './bindings.js';
export interface Config {
instance?: SmoldotWasmInstance,
+
+ /**
+ * Closure to call when the Wasm instance calls `panic`.
+ *
+ * This callback will always be invoked from within a binding called the Wasm instance.
+ */
+ onPanic: (message: string) => never,
+
logCallback: (level: number, target: string, message: string) => void,
jsonRpcCallback: (response: string, chainId: number) => void,
databaseContentCallback: (data: string, chainId: number) => void,
@@ -37,12 +45,25 @@ export interface Config {
forbidWss: boolean,
}
-export default function (config: Config): compat.WasmModuleImports {
+export default function (config: Config): { imports: compat.WasmModuleImports, killAll: () => void } {
// Used below to store the list of all connections.
// The indices within this array are chosen by the Rust code.
let connections: Record = {};
- return {
+ // Object containing a boolean indicating whether the `killAll` function has been invoked by
+ // the user.
+ const killedTracked = { killed: false };
+
+ const killAll = () => {
+ killedTracked.killed = true;
+ // TODO: kill timers as well?
+ for (const connection in connections) {
+ connections[connection]!.close()
+ delete connections[connection]
+ }
+ };
+
+ const imports = {
// Must exit with an error. A human-readable message can be found in the WebAssembly
// memory in the given buffer.
panic: (ptr: number, len: number) => {
@@ -52,11 +73,13 @@ export default function (config: Config): compat.WasmModuleImports {
len >>>= 0;
const message = Buffer.from(instance.exports.memory.buffer).toString('utf8', ptr, ptr + len);
- throw new Error(message);
+ config.onPanic(message);
},
// Used by the Rust side to emit a JSON-RPC response or subscription notification.
json_rpc_respond: (ptr: number, len: number, chainId: number) => {
+ if (killedTracked.killed) return;
+
const instance = config.instance!;
ptr >>>= 0;
@@ -70,6 +93,8 @@ export default function (config: Config): compat.WasmModuleImports {
// Used by the Rust side in response to asking for the database content of a chain.
database_content_ready: (ptr: number, len: number, chainId: number) => {
+ if (killedTracked.killed) return;
+
const instance = config.instance!;
ptr >>>= 0;
@@ -84,6 +109,8 @@ export default function (config: Config): compat.WasmModuleImports {
// Used by the Rust side to emit a log entry.
// See also the `max_log_level` parameter in the configuration.
log: (level: number, targetPtr: number, targetLen: number, messagePtr: number, messageLen: number) => {
+ if (killedTracked.killed) return;
+
const instance = config.instance!;
targetPtr >>>= 0;
@@ -108,6 +135,8 @@ export default function (config: Config): compat.WasmModuleImports {
// Must call `timer_finished` after the given number of milliseconds has elapsed.
start_timer: (id: number, ms: number) => {
+ if (killedTracked.killed) return;
+
const instance = config.instance!;
// In both NodeJS and browsers, if `setTimeout` is called with a value larger than
@@ -122,11 +151,17 @@ export default function (config: Config): compat.WasmModuleImports {
// with `1`) and wants you to use `setImmediate` instead.
if (ms == 0 && typeof setImmediate === "function") {
setImmediate(() => {
- instance.exports.timer_finished(id);
+ if (killedTracked.killed) return;
+ try {
+ instance.exports.timer_finished(id);
+ } catch(_error) {}
})
} else {
setTimeout(() => {
- instance.exports.timer_finished(id);
+ if (killedTracked.killed) return;
+ try {
+ instance.exports.timer_finished(id);
+ } catch(_error) {}
}, ms)
}
},
@@ -145,6 +180,9 @@ export default function (config: Config): compat.WasmModuleImports {
}
try {
+ if (killedTracked.killed)
+ throw new Error("killAll invoked");
+
const address = Buffer.from(instance.exports.memory.buffer)
.toString('utf8', addrPtr, addrPtr + addrLen);
@@ -155,18 +193,27 @@ export default function (config: Config): compat.WasmModuleImports {
forbidNonLocalWs: config.forbidNonLocalWs,
forbidWss: config.forbidWss,
onOpen: () => {
- instance.exports.connection_open_single_stream(connectionId);
+ if (killedTracked.killed) return;
+ try {
+ instance.exports.connection_open_single_stream(connectionId);
+ } catch(_error) {}
},
onClose: (message: string) => {
- const len = Buffer.byteLength(message, 'utf8');
- const ptr = instance.exports.alloc(len) >>> 0;
- Buffer.from(instance.exports.memory.buffer).write(message, ptr);
- instance.exports.connection_closed(connectionId, ptr, len);
+ if (killedTracked.killed) return;
+ try {
+ const len = Buffer.byteLength(message, 'utf8');
+ const ptr = instance.exports.alloc(len) >>> 0;
+ Buffer.from(instance.exports.memory.buffer).write(message, ptr);
+ instance.exports.connection_closed(connectionId, ptr, len);
+ } catch(_error) {}
},
onMessage: (message: Buffer) => {
- const ptr = instance.exports.alloc(message.length) >>> 0;
- message.copy(Buffer.from(instance.exports.memory.buffer), ptr);
- instance.exports.stream_message(connectionId, 0, ptr, message.length);
+ if (killedTracked.killed) return;
+ try {
+ const ptr = instance.exports.alloc(message.length) >>> 0;
+ message.copy(Buffer.from(instance.exports.memory.buffer), ptr);
+ instance.exports.stream_message(connectionId, 0, ptr, message.length);
+ } catch(_error) {}
}
});
@@ -192,6 +239,7 @@ export default function (config: Config): compat.WasmModuleImports {
// Must close and destroy the connection object.
connection_close: (connectionId: number) => {
+ if (killedTracked.killed) return;
const connection = connections[connectionId]!;
connection.close();
delete connections[connectionId];
@@ -212,6 +260,8 @@ export default function (config: Config): compat.WasmModuleImports {
// Must queue the data found in the WebAssembly memory at the given pointer. It is assumed
// that this function is called only when the connection is in an open state.
stream_send: (connectionId: number, _streamId: number, ptr: number, len: number) => {
+ if (killedTracked.killed) return;
+
const instance = config.instance!;
ptr >>>= 0;
@@ -223,6 +273,8 @@ export default function (config: Config): compat.WasmModuleImports {
},
current_task_entered: (ptr: number, len: number) => {
+ if (killedTracked.killed) return;
+
const instance = config.instance!;
ptr >>>= 0;
@@ -234,8 +286,11 @@ export default function (config: Config): compat.WasmModuleImports {
},
current_task_exit: () => {
+ if (killedTracked.killed) return;
if (config.currentTaskCallback)
config.currentTaskCallback(null);
}
};
+
+ return { imports, killAll }
}
diff --git a/bin/wasm-node/javascript/src/worker/bindings-wasi.ts b/bin/wasm-node/javascript/src/instance/bindings-wasi.ts
similarity index 96%
rename from bin/wasm-node/javascript/src/worker/bindings-wasi.ts
rename to bin/wasm-node/javascript/src/instance/bindings-wasi.ts
index 915435f501..17b6c89c4b 100644
--- a/bin/wasm-node/javascript/src/worker/bindings-wasi.ts
+++ b/bin/wasm-node/javascript/src/instance/bindings-wasi.ts
@@ -37,6 +37,13 @@ export interface Config {
* Must never be modified after the bindings have been initialized.
*/
envVars: string[],
+
+ /**
+ * Closure to call when the Wasm instance calls `proc_exit`.
+ *
+ * This callback will always be invoked from within a binding called the Wasm instance.
+ */
+ onProcExit: (retCode: number) => never,
}
export default (config: Config): compat.WasmModuleImports => {
@@ -130,7 +137,7 @@ export default (config: Config): compat.WasmModuleImports => {
// Used by Rust in catastrophic situations, such as a double panic.
proc_exit: (retCode: number) => {
- throw new Error(`proc_exit called: ${retCode}`);
+ config.onProcExit(retCode)
},
// Return the number of environment variables and the total size of all environment
diff --git a/bin/wasm-node/javascript/src/worker/bindings.ts b/bin/wasm-node/javascript/src/instance/bindings.ts
similarity index 98%
rename from bin/wasm-node/javascript/src/worker/bindings.ts
rename to bin/wasm-node/javascript/src/instance/bindings.ts
index dfe54985e9..17b6b8435e 100644
--- a/bin/wasm-node/javascript/src/worker/bindings.ts
+++ b/bin/wasm-node/javascript/src/instance/bindings.ts
@@ -24,6 +24,7 @@
export interface SmoldotWasmExports extends WebAssembly.Exports {
memory: WebAssembly.Memory,
init: (maxLogLevel: number, enableCurrentTask: number, cpuRateLimit: number) => void,
+ start_shutdown: () => void,
alloc: (len: number) => number,
add_chain: (chainSpecPointer: number, chainSpecLen: number, databaseContentPointer: number, databaseContentLen: number, jsonRpcRunning: number, potentialRelayChainsPtr: number, potentialRelayChainsLen: number) => number;
remove_chain: (chainId: number) => void,
diff --git a/bin/wasm-node/javascript/src/worker/connection.ts b/bin/wasm-node/javascript/src/instance/connection.ts
similarity index 100%
rename from bin/wasm-node/javascript/src/worker/connection.ts
rename to bin/wasm-node/javascript/src/instance/connection.ts
diff --git a/bin/wasm-node/javascript/src/instance/instance.ts b/bin/wasm-node/javascript/src/instance/instance.ts
new file mode 100644
index 0000000000..b3d5d498ec
--- /dev/null
+++ b/bin/wasm-node/javascript/src/instance/instance.ts
@@ -0,0 +1,305 @@
+// Smoldot
+// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+import { Buffer } from 'buffer';
+import * as instance from './raw-instance.js';
+import { SmoldotWasmInstance } from './bindings.js';
+import { CrashError } from '../client.js';
+
+/**
+ * Contains the configuration of the instance.
+ */
+export interface Config {
+ logCallback: (level: number, target: string, message: string) => void
+ maxLogLevel: number;
+ enableCurrentTask: boolean;
+ cpuRateLimit: number,
+ forbidTcp: boolean;
+ forbidWs: boolean;
+ forbidNonLocalWs: boolean;
+ forbidWss: boolean;
+}
+
+export interface Instance {
+ request: (request: string, chainId: number) => void
+ addChain: (chainSpec: string, databaseContent: string, potentialRelayChains: number[], jsonRpcCallback?: (response: string) => void) => Promise<{ success: true, chainId: number } | { success: false, error: string }>
+ removeChain: (chainId: number) => void
+ databaseContent: (chainId: number, maxUtf8BytesSize?: number) => Promise
+ startShutdown: () => void
+}
+
+export function start(configMessage: Config): Instance {
+
+ // This variable represents the state of the instance, and serves two different purposes:
+ //
+ // - At initialization, it is a Promise containing the Wasm VM is still initializing.
+ // - After the Wasm VM has finished initialization, contains the `WebAssembly.Instance` object.
+ //
+ let state: { initialized: false, promise: Promise } | { initialized: true, instance: SmoldotWasmInstance };
+
+ const crashError: { error?: CrashError } = {};
+
+ const currentTask: { name: string | null } = { name: null };
+
+ const printError = { printError: true }
+
+ // Contains the information of each chain that is currently alive.
+ let chains: Map void,
+ databasePromises: DatabasePromise[],
+ }> = new Map();
+
+ // Start initialization of the Wasm VM.
+ const config: instance.Config = {
+ onWasmPanic: (message) => {
+ // TODO: consider obtaining a backtrace here
+ crashError.error = new CrashError(message);
+ if (!printError.printError)
+ return;
+ console.error(
+ "Smoldot has panicked" +
+ (currentTask.name ? (" while executing task `" + currentTask.name + "`") : "") +
+ ". This is a bug in smoldot. Please open an issue at " +
+ "https://github.com/paritytech/smoldot/issues with the following message:\n" +
+ message
+ );
+ },
+ logCallback: (level, target, message) => {
+ configMessage.logCallback(level, target, message)
+ },
+ jsonRpcCallback: (data, chainId) => {
+ const cb = chains.get(chainId)?.jsonRpcCallback;
+ if (cb) cb(data);
+ },
+ databaseContentCallback: (data, chainId) => {
+ const promises = chains.get(chainId)?.databasePromises!;
+ (promises.shift() as DatabasePromise).resolve(data);
+ },
+ currentTaskCallback: (taskName) => {
+ currentTask.name = taskName
+ },
+ cpuRateLimit: configMessage.cpuRateLimit,
+ forbidTcp: configMessage.forbidTcp,
+ forbidWs: configMessage.forbidWs,
+ forbidNonLocalWs: configMessage.forbidNonLocalWs,
+ forbidWss: configMessage.forbidWss,
+ };
+
+ state = {
+ initialized: false, promise: instance.startInstance(config).then((instance) => {
+ // `config.cpuRateLimit` is a floating point that should be between 0 and 1, while the value
+ // to pass as parameter must be between `0` and `2^32-1`.
+ // The few lines of code below should handle all possible values of `number`, including
+ // infinites and NaN.
+ let cpuRateLimit = Math.round(config.cpuRateLimit * 4294967295); // `2^32 - 1`
+ if (cpuRateLimit < 0) cpuRateLimit = 0;
+ if (cpuRateLimit > 4294967295) cpuRateLimit = 4294967295;
+ if (!Number.isFinite(cpuRateLimit)) cpuRateLimit = 4294967295; // User might have passed NaN
+
+ // Smoldot requires an initial call to the `init` function in order to do its internal
+ // configuration.
+ instance.exports.init(configMessage.maxLogLevel, configMessage.enableCurrentTask ? 1 : 0, cpuRateLimit);
+
+ state = { initialized: true, instance };
+ return instance;
+ })
+ };
+
+ async function queueOperation(operation: (instance: SmoldotWasmInstance) => T): Promise {
+ // What to do depends on the type of `state`.
+ // See the documentation of the `state` variable for information.
+ if (!state.initialized) {
+ // A message has been received while the Wasm VM is still initializing. Queue it for when
+ // initialization is over.
+ return state.promise.then((instance) => operation(instance))
+
+ } else {
+ // Everything is already initialized. Process the message synchronously.
+ return operation(state.instance)
+ }
+ }
+
+ return {
+ request: (request: string, chainId: number) => {
+ // Because `request` is passed as parameter an identifier returned by `addChain`, it is
+ // always the case that the Wasm instance is already initialized. The only possibility for
+ // it to not be the case is if the user completely invented the `chainId`.
+ if (!state.initialized)
+ throw new Error("Internal error");
+ if (crashError.error)
+ throw crashError.error;
+
+ try {
+ const len = Buffer.byteLength(request, 'utf8');
+ const ptr = state.instance.exports.alloc(len) >>> 0;
+ Buffer.from(state.instance.exports.memory.buffer).write(request, ptr);
+ state.instance.exports.json_rpc_send(ptr, len, chainId);
+ } catch (_error) {
+ console.assert(crashError.error);
+ throw crashError.error
+ }
+ },
+
+ addChain: (chainSpec: string, databaseContent: string, potentialRelayChains: number[], jsonRpcCallback?: (response: string) => void): Promise<{ success: true, chainId: number } | { success: false, error: string }> => {
+ return queueOperation((instance) => {
+ if (crashError.error)
+ throw crashError.error;
+
+ try {
+ // Write the chain specification into memory.
+ const chainSpecLen = Buffer.byteLength(chainSpec, 'utf8');
+ const chainSpecPtr = instance.exports.alloc(chainSpecLen) >>> 0;
+ Buffer.from(instance.exports.memory.buffer)
+ .write(chainSpec, chainSpecPtr);
+
+ // Write the database content into memory.
+ const databaseContentLen = Buffer.byteLength(databaseContent, 'utf8');
+ const databaseContentPtr = instance.exports.alloc(databaseContentLen) >>> 0;
+ Buffer.from(instance.exports.memory.buffer)
+ .write(databaseContent, databaseContentPtr);
+
+ // Write the potential relay chains into memory.
+ const potentialRelayChainsLen = potentialRelayChains.length;
+ const potentialRelayChainsPtr = instance.exports.alloc(potentialRelayChainsLen * 4) >>> 0;
+ for (let idx = 0; idx < potentialRelayChains.length; ++idx) {
+ Buffer.from(instance.exports.memory.buffer)
+ .writeUInt32LE(potentialRelayChains[idx]!, potentialRelayChainsPtr + idx * 4);
+ }
+
+ // `add_chain` unconditionally allocates a chain id. If an error occurs, however, this chain
+ // id will refer to an *erroneous* chain. `chain_is_ok` is used below to determine whether it
+ // has succeeeded or not.
+ // Note that `add_chain` properly de-allocates buffers even if it failed.
+ const chainId = instance.exports.add_chain(
+ chainSpecPtr, chainSpecLen,
+ databaseContentPtr, databaseContentLen,
+ !!jsonRpcCallback ? 1 : 0,
+ potentialRelayChainsPtr, potentialRelayChainsLen
+ );
+
+ if (instance.exports.chain_is_ok(chainId) != 0) {
+ console.assert(!chains.has(chainId));
+ chains.set(chainId, {
+ jsonRpcCallback,
+ databasePromises: new Array()
+ });
+ return { success: true, chainId };
+ } else {
+ const errorMsgLen = instance.exports.chain_error_len(chainId) >>> 0;
+ const errorMsgPtr = instance.exports.chain_error_ptr(chainId) >>> 0;
+ const errorMsg = Buffer.from(instance.exports.memory.buffer)
+ .toString('utf8', errorMsgPtr, errorMsgPtr + errorMsgLen);
+ instance.exports.remove_chain(chainId);
+ return { success: false, error: errorMsg };
+ }
+ } catch (_error) {
+ console.assert(crashError.error);
+ throw crashError.error
+ }
+ })
+ },
+
+ removeChain: (chainId: number) => {
+ // Because `removeChain` is passed as parameter an identifier returned by `addChain`, it is
+ // always the case that the Wasm instance is already initialized. The only possibility for
+ // it to not be the case is if the user completely invented the `chainId`.
+ if (!state.initialized)
+ throw new Error("Internal error");
+ if (crashError.error)
+ throw crashError.error;
+
+ // Removing the chain synchronously avoids having to deal with race conditions such as a
+ // JSON-RPC response corresponding to a chain that is going to be deleted but hasn't been yet.
+ // These kind of race conditions are already delt with within smoldot.
+ console.assert(chains.has(chainId));
+ chains.delete(chainId);
+ try {
+ state.instance.exports.remove_chain(chainId);
+ } catch (_error) {
+ console.assert(crashError.error);
+ throw crashError.error
+ }
+ },
+
+ databaseContent: (chainId: number, maxUtf8BytesSize?: number): Promise => {
+ // Because `databaseContent` is passed as parameter an identifier returned by `addChain`, it
+ // is always the case that the Wasm instance is already initialized. The only possibility for
+ // it to not be the case is if the user completely invented the `chainId`.
+ if (!state.initialized)
+ throw new Error("Internal error");
+
+ if (crashError.error)
+ throw crashError.error;
+
+ console.assert(chains.has(chainId));
+ const databaseContentPromises = chains.get(chainId)?.databasePromises!;
+ const promise: Promise = new Promise((resolve, reject) => {
+ databaseContentPromises.push({ resolve, reject });
+ });
+
+ // Cap `maxUtf8BytesSize` and set a default value.
+ const twoPower32 = (1 << 30) * 4; // `1 << 31` and `1 << 32` in JavaScript don't give the value that you expect.
+ const maxSize = maxUtf8BytesSize || (twoPower32 - 1);
+ const cappedMaxSize = (maxSize >= twoPower32) ? (twoPower32 - 1) : maxSize;
+
+ // The value of `maxUtf8BytesSize` is guaranteed to always fit in 32 bits, in
+ // other words, that `maxUtf8BytesSize < (1 << 32)`.
+ // We need to perform a conversion in such a way that the the bits of the output of
+ // `ToInt32(converted)`, when interpreted as u32, is equal to `maxUtf8BytesSize`.
+ // See ToInt32 here: https://tc39.es/ecma262/#sec-toint32
+ // Note that the code below has been tested against example values. Please be very careful
+ // if you decide to touch it. Ideally it would be unit-tested, but since it concerns the FFI
+ // layer between JS and Rust, writing unit tests would be extremely complicated.
+ const twoPower31 = (1 << 30) * 2; // `1 << 31` in JavaScript doesn't give the value that you expect.
+ const converted = (cappedMaxSize >= twoPower31) ?
+ (cappedMaxSize - twoPower32) : cappedMaxSize;
+
+ try {
+ state.instance.exports.database_content(chainId, converted);
+ return promise;
+ } catch (_error) {
+ console.assert(crashError.error);
+ throw crashError.error
+ }
+ },
+
+ startShutdown: () => {
+ return queueOperation((instance) => {
+ // `startShutdown` is a bit special in its handling of crashes.
+ // Shutting down will lead to `onWasmPanic` being called at some point, possibly during
+ // the call to `start_shutdown` itself. As such, we move into "don't print errors anymore"
+ // mode even before calling `start_shutdown`.
+ //
+ // Furthermore, if a crash happened in the past, there is no point in throwing an
+ // exception when the user wants the shutdown to happen.
+ if (crashError.error)
+ return;
+ try {
+ printError.printError = false
+ instance.exports.start_shutdown()
+ } catch (_error) {
+ }
+ })
+ }
+ }
+
+}
+
+interface DatabasePromise {
+ resolve: (data: string) => void,
+ reject: (error: Error) => void,
+}
diff --git a/bin/wasm-node/javascript/src/worker/instance.ts b/bin/wasm-node/javascript/src/instance/raw-instance.ts
similarity index 73%
rename from bin/wasm-node/javascript/src/worker/instance.ts
rename to bin/wasm-node/javascript/src/instance/raw-instance.ts
index 36579a9107..a21487e1b0 100644
--- a/bin/wasm-node/javascript/src/worker/instance.ts
+++ b/bin/wasm-node/javascript/src/instance/raw-instance.ts
@@ -19,7 +19,7 @@ import pako from 'pako';
import { Buffer } from 'buffer';
-import { Config as SmoldotBindingsConfig, default as smolditLightBindingsBuilder } from './bindings-smoldot-light.js';
+import { Config as SmoldotBindingsConfig, default as smoldotLightBindingsBuilder } from './bindings-smoldot-light.js';
import { Config as WasiConfig, default as wasiBindingsBuilder } from './bindings-wasi.js';
import { default as wasmBase64 } from './autogen/wasm.js';
@@ -27,6 +27,18 @@ import { default as wasmBase64 } from './autogen/wasm.js';
import { SmoldotWasmInstance } from './bindings.js';
export interface Config {
+ /**
+ * Closure to call when the Wasm instance panics.
+ *
+ * This callback will always be invoked from within a binding called the Wasm instance.
+ *
+ * After this callback has been called, it is forbidden to invoke any function from the Wasm
+ * VM.
+ *
+ * If this callback is called while invoking a function from the Wasm VM, this function will
+ * throw a dummy exception.
+ */
+ onWasmPanic: (message: string) => void,
logCallback: (level: number, target: string, message: string) => void,
jsonRpcCallback: (response: string, chainId: number) => void,
databaseContentCallback: (data: string, chainId: number) => void,
@@ -45,22 +57,39 @@ export async function startInstance(config: Config): Promise void;
+
// Used to bind with the smoldot-light bindings. See the `bindings-smoldot-light.js` file.
const smoldotJsConfig: SmoldotBindingsConfig = {
+ onPanic: (message) => {
+ killAll();
+ config.onWasmPanic(message);
+ throw new Error();
+ },
...config
};
// Used to bind with the Wasi bindings. See the `bindings-wasi.js` file.
const wasiConfig: WasiConfig = {
envVars: [],
+ onProcExit: (retCode) => {
+ killAll();
+ config.onWasmPanic(`proc_exit called: ${retCode}`)
+ throw new Error();
+ }
};
+ const { imports: smoldotBindings, killAll: smoldotBindingsKillAll } =
+ smoldotLightBindingsBuilder(smoldotJsConfig);
+
+ killAll = smoldotBindingsKillAll;
+
// Start the Wasm virtual machine.
// The Rust code defines a list of imports that must be fulfilled by the environment. The second
// parameter provides their implementations.
const result = await WebAssembly.instantiate(wasmBytecode, {
// The functions with the "smoldot" prefix are specific to smoldot.
- "smoldot": smolditLightBindingsBuilder(smoldotJsConfig),
+ "smoldot": smoldotBindings,
// As the Rust code is compiled for wasi, some more wasi-specific imports exist.
"wasi_snapshot_preview1": wasiBindingsBuilder(wasiConfig),
});
diff --git a/bin/wasm-node/javascript/src/worker/messages.ts b/bin/wasm-node/javascript/src/worker/messages.ts
deleted file mode 100644
index 5d895f3eb0..0000000000
--- a/bin/wasm-node/javascript/src/worker/messages.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-// Smoldot
-// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
-// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-/**
- * Message to the worker.
- *
- * The first ever message sent to the worker must be a `ToWorkerConfig`, then all subsequent
- * messages must be `ToWorkerNonConfig`s.
- */
-export type ToWorker = ToWorkerConfig | ToWorkerNonConfig;
-export type ToWorkerNonConfig = ToWorkerRpcRequest | ToWorkerAddChain | ToWorkerRemoveChain | ToWorkerDatabaseContent;
-
-/**
- * Message that the worker can send to the outside.
- */
-export type FromWorker = FromWorkerChainAddedOk | FromWorkerChainAddedError | FromWorkerLog | FromWorkerJsonRpc | FromWorkerDatabaseContent | FromWorkerLivenessPing | FromWorkerCurrentTask;
-
-/**
- * Contains the initial configuration of the worker.
- *
- * This message is only ever sent once, and it is always the first ever message sent to the
- * worker.
- */
-export interface ToWorkerConfig {
- maxLogLevel: number;
- enableCurrentTask: boolean;
- cpuRateLimit: number,
- forbidTcp: boolean;
- forbidWs: boolean;
- forbidNonLocalWs: boolean;
- forbidWss: boolean;
-}
-
-/**
- * Start a JSON-RPC request.
- */
-export interface ToWorkerRpcRequest {
- ty: 'request',
- request: string,
- chainId: number,
-}
-
-/**
- * Add a new chain.
- *
- * The worker must reply with either a `FromWorkerChainAddedOk` or a `FromWorkerChainAddedError`.
- */
-export interface ToWorkerAddChain {
- ty: 'addChain',
- chainSpec: string,
- databaseContent: string,
- potentialRelayChains: number[],
- jsonRpcRunning: boolean,
-}
-
-/**
- * Remove a chain.
- *
- * The worker must reply with a `FromWorkerChainRemoved`.
- */
-export interface ToWorkerRemoveChain {
- ty: 'removeChain',
- chainId: number,
-}
-
-/**
- * Get the database content of a chain.
- *
- * The worker must reply with a `FromWorkerDatabaseContent`.
- */
-export interface ToWorkerDatabaseContent {
- ty: 'databaseContent',
- chainId: number,
- maxUtf8BytesSize: number,
-}
-
-export interface FromWorkerChainAddedOk {
- kind: 'chainAddedOk',
- chainId: number,
-}
-
-export interface FromWorkerChainAddedError {
- kind: 'chainAddedErr',
- error: string,
-}
-
-export interface FromWorkerLog {
- kind: 'log',
- level: number,
- target: string,
- message: string,
-}
-
-export interface FromWorkerJsonRpc {
- kind: 'jsonrpc',
- data: string,
- chainId: number,
-}
-
-export interface FromWorkerDatabaseContent {
- kind: 'databaseContent',
- data: string,
- chainId: number,
-}
-
-export interface FromWorkerLivenessPing {
- kind: 'livenessPing',
-}
-
-export interface FromWorkerCurrentTask {
- kind: 'currentTask',
- taskName: string | null,
-}
diff --git a/bin/wasm-node/javascript/src/worker/spawn-browser-overwrite.js b/bin/wasm-node/javascript/src/worker/spawn-browser-overwrite.js
deleted file mode 100644
index 8020c208fb..0000000000
--- a/bin/wasm-node/javascript/src/worker/spawn-browser-overwrite.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// Smoldot
-// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
-// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-export default function () {
- if (!window.Worker)
- throw new Error("Workers not available");
-
- // The line of code below (`new Worker(...)`) is designed to hopefully work across all
- // platforms and bundlers.
- // Because this line is precisely recognized by bundlers, we extract it to a separate
- // JavaScript file.
- // See also the README.md for more context.
-
- // Note that, at the time of writing, Firefox doesn't support the `type: "module"` option.
- // Because browsers don't fully support modules yet, this code is expected to be run through
- // a bundler (e.g. WebPack) before being given to a browser, which will remove all usage of
- // modules in the worker code. It is thus also the role of this bundler to tweak or remove
- // the value of this `type` property to indicate to the browser that modules aren't in use.
- //
- // WebPack in particular does this, but it is unclear whether *all* bundlers do it.
- // Whether bundlers actually do this or not, it is nonetheless more correct to indicate
- // `type: "module"` and doing so doesn't have any drawback.
- const worker = new Worker(new URL('./worker.js', import.meta.url), { name: "smoldot", type: "module" });
- return worker;
-}
diff --git a/bin/wasm-node/javascript/src/worker/spawn.d.ts b/bin/wasm-node/javascript/src/worker/spawn.d.ts
deleted file mode 100644
index 670e46b531..0000000000
--- a/bin/wasm-node/javascript/src/worker/spawn.d.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-// Smoldot
-// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
-// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-import type { CompatWorker } from '../compat/index.js';
-
-export default function (): CompatWorker;
diff --git a/bin/wasm-node/javascript/src/worker/spawn.js b/bin/wasm-node/javascript/src/worker/spawn.js
deleted file mode 100644
index 6b7fa06533..0000000000
--- a/bin/wasm-node/javascript/src/worker/spawn.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// Smoldot
-// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
-// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-import { Worker } from 'node:worker_threads';
-
-export default function () {
- // The line of code below (`new Worker(...)`) is designed to hopefully work across all
- // platforms and bundlers.
- // Because this line is precisely recognized by bundlers, we extract it to a separate
- // JavaScript file.
- // See also the README.md for more context.
-
- // Note that at the time of writing of this comment, NodeJS doesn't support the
- // `type: "module"` option, as modules "just work". But we put it anyway because Deno throws
- // an exception if this option isn't present.
- const worker = new Worker(new URL('./worker.js', import.meta.url), { type: "module" });
- return worker;
-}
diff --git a/bin/wasm-node/javascript/src/worker/worker.ts b/bin/wasm-node/javascript/src/worker/worker.ts
deleted file mode 100644
index 2bd9243df8..0000000000
--- a/bin/wasm-node/javascript/src/worker/worker.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-// Smoldot
-// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd.
-// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
-
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU General Public License for more details.
-
-// You should have received a copy of the GNU General Public License
-// along with this program. If not, see .
-
-import { Buffer } from 'buffer';
-import * as compat from './../compat/index.js';
-import * as instance from './instance.js';
-import * as messages from './messages.js';
-import { SmoldotWasmInstance } from './bindings.js';
-
-// This variable represents the state of the worker, and serves three different purposes:
-//
-// - At initialization, it is set to `null`.
-// - Once the first message, containing the configuration, has been received from the parent, it
-// becomes an array filled with the messages that are received while the Wasm VM is still
-// initializing.
-// - After the Wasm VM has finished initialization, contains the `WebAssembly.Instance` object.
-//
-let state: null | messages.ToWorkerNonConfig[] | SmoldotWasmInstance = null;
-
-// Inject a message coming from `index.js` to a running Wasm VM.
-function injectMessage(instance: SmoldotWasmInstance, message: messages.ToWorkerNonConfig): void {
- switch (message.ty) {
- case 'request': {
- const len = Buffer.byteLength(message.request, 'utf8');
- const ptr = instance.exports.alloc(len) >>> 0;
- Buffer.from(instance.exports.memory.buffer).write(message.request, ptr);
- instance.exports.json_rpc_send(ptr, len, message.chainId);
- break;
- }
-
- case 'addChain': {
- // Write the chain specification into memory.
- const chainSpecLen = Buffer.byteLength(message.chainSpec, 'utf8');
- const chainSpecPtr = instance.exports.alloc(chainSpecLen) >>> 0;
- Buffer.from(instance.exports.memory.buffer)
- .write(message.chainSpec, chainSpecPtr);
-
- // Write the database content into memory.
- const databaseContentLen = Buffer.byteLength(message.databaseContent, 'utf8');
- const databaseContentPtr = instance.exports.alloc(databaseContentLen) >>> 0;
- Buffer.from(instance.exports.memory.buffer)
- .write(message.databaseContent, databaseContentPtr);
-
- // Write the potential relay chains into memory.
- const potentialRelayChainsLen = message.potentialRelayChains.length;
- const potentialRelayChainsPtr = instance.exports.alloc(potentialRelayChainsLen * 4) >>> 0;
- for (let idx = 0; idx < message.potentialRelayChains.length; ++idx) {
- Buffer.from(instance.exports.memory.buffer)
- .writeUInt32LE(message.potentialRelayChains[idx]!, potentialRelayChainsPtr + idx * 4);
- }
-
- // `add_chain` unconditionally allocates a chain id. If an error occurs, however, this chain
- // id will refer to an *erroneous* chain. `chain_is_ok` is used below to determine whether it
- // has succeeeded or not.
- // Note that `add_chain` properly de-allocates buffers even if it failed.
- const chainId = instance.exports.add_chain(
- chainSpecPtr, chainSpecLen,
- databaseContentPtr, databaseContentLen,
- message.jsonRpcRunning ? 1 : 0,
- potentialRelayChainsPtr, potentialRelayChainsLen
- );
-
- if (instance.exports.chain_is_ok(chainId) != 0) {
- postMessage({ kind: 'chainAddedOk', chainId });
- } else {
- const errorMsgLen = instance.exports.chain_error_len(chainId) >>> 0;
- const errorMsgPtr = instance.exports.chain_error_ptr(chainId) >>> 0;
- const errorMsg = Buffer.from(instance.exports.memory.buffer)
- .toString('utf8', errorMsgPtr, errorMsgPtr + errorMsgLen);
- instance.exports.remove_chain(chainId);
- postMessage({ kind: 'chainAddedErr', error: errorMsg });
- }
-
- break;
- }
-
- case 'removeChain': {
- instance.exports.remove_chain(message.chainId);
- break;
- }
-
- case 'databaseContent': {
- // The value of `maxUtf8BytesSize` is guaranteed (by `index.js`) to always fit in 32 bits, in
- // other words, that `maxUtf8BytesSize < (1 << 32)`.
- // We need to perform a conversion in such a way that the the bits of the output of
- // `ToInt32(converted)`, when interpreted as u32, is equal to `maxUtf8BytesSize`.
- // See ToInt32 here: https://tc39.es/ecma262/#sec-toint32
- // Note that the code below has been tested against example values. Please be very careful
- // if you decide to touch it. Ideally it would be unit-tested, but since it concerns the FFI
- // layer between JS and Rust, writing unit tests would be extremely complicated.
- const twoPower31 = (1 << 30) * 2; // `1 << 31` in JavaScript doesn't give the value that you expect.
- const converted = (message.maxUtf8BytesSize >= twoPower31) ?
- (message.maxUtf8BytesSize - (twoPower31 * 2)) : message.maxUtf8BytesSize;
- instance.exports.database_content(message.chainId, converted);
- break;
- }
-
- default: {
- // Exhaustive check.
- const _exhaustiveCheck: never = message;
- return _exhaustiveCheck;
- }
- }
-};
-
-function postMessage(message: messages.FromWorker) {
- // `compat.postMessage` is the same as `postMessage`, but works across environments.
- compat.postMessage(message)
-}
-
-// `compat.setOnMessage` is the same as `onmessage = ...`, but works across environments.
-compat.setOnMessage((message: messages.ToWorker) => {
- // What to do depends on the type of `state`.
- // See the documentation of the `state` variable for information.
- if (state == null) {
- // First ever message received by the worker. Always contains the initial configuration.
- const configMessage = message as messages.ToWorkerConfig;
-
- // Transition to the next phase: an array during which messages are stored while the
- // initialization is in progress.
- state = [];
-
- // Start initialization of the Wasm VM.
- const config: instance.Config = {
- logCallback: (level, target, message) => {
- postMessage({ kind: 'log', level, target, message });
- },
- jsonRpcCallback: (data, chainId) => {
- postMessage({ kind: 'jsonrpc', data, chainId });
- },
- databaseContentCallback: (data, chainId) => {
- postMessage({ kind: 'databaseContent', data, chainId });
- },
- currentTaskCallback: (taskName) => {
- postMessage({ kind: 'currentTask', taskName });
- },
- cpuRateLimit: configMessage.cpuRateLimit,
- forbidTcp: configMessage.forbidTcp,
- forbidWs: configMessage.forbidWs,
- forbidNonLocalWs: configMessage.forbidNonLocalWs,
- forbidWss: configMessage.forbidWss,
- };
-
- instance.startInstance(config).then((instance) => {
- // `config.cpuRateLimit` is a floating point that should be between 0 and 1, while the value
- // to pass as parameter must be between `0` and `2^32-1`.
- // The few lines of code below should handle all possible values of `number`, including
- // infinites and NaN.
- let cpuRateLimit = Math.round(config.cpuRateLimit * 4294967295); // `2^32 - 1`
- if (cpuRateLimit < 0) cpuRateLimit = 0;
- if (cpuRateLimit > 4294967295) cpuRateLimit = 4294967295;
- if (!Number.isFinite(cpuRateLimit)) cpuRateLimit = 4294967295; // User might have passed NaN
-
- // Smoldot requires an initial call to the `init` function in order to do its internal
- // configuration.
- instance.exports.init(configMessage.maxLogLevel, configMessage.enableCurrentTask ? 1 : 0, cpuRateLimit);
-
- // Smoldot has finished initializing.
- // Since this function is an asynchronous function, it is possible that messages have been
- // received from the parent while it was executing. These messages are now handled.
- (state as messages.ToWorkerNonConfig[]).forEach((message) => {
- injectMessage(instance, message);
- });
-
- state = instance;
- });
-
- } else if (Array.isArray(state)) {
- // A message has been received while the Wasm VM is still initializing. Queue it for when
- // initialization is over.
- state.push(message as messages.ToWorkerNonConfig);
-
- } else {
- // Everything is already initialized. Process the message synchronously.
- injectMessage(state, message as messages.ToWorkerNonConfig);
- }
-});
-
-// Periodically send a ping message to the outside, as a way to report liveness.
-setInterval(() => {
- postMessage({ kind: 'livenessPing' });
-}, 2500);
diff --git a/bin/wasm-node/rust/src/bindings.rs b/bin/wasm-node/rust/src/bindings.rs
index d40a50a5d8..5be2385223 100644
--- a/bin/wasm-node/rust/src/bindings.rs
+++ b/bin/wasm-node/rust/src/bindings.rs
@@ -280,6 +280,18 @@ pub extern "C" fn init(max_log_level: u32, enable_current_task: u32, cpu_rate_li
crate::init(max_log_level, enable_current_task, cpu_rate_limit)
}
+/// Instructs the client to start shutting down.
+///
+/// Later, the client will use `exit` to stop.
+///
+/// It is still legal to call all the other functions of these bindings. The client continues to
+/// operate normally until the call to `exit`, which happens at some point in the future.
+// TODO: can this be called multiple times?
+#[no_mangle]
+pub extern "C" fn start_shutdown() {
+ crate::start_shutdown()
+}
+
/// Allocates a buffer of the given length, with an alignment of 1.
///
/// This must be used in the context of [`add_chain`] and other functions that similarly require
diff --git a/bin/wasm-node/rust/src/lib.rs b/bin/wasm-node/rust/src/lib.rs
index 0d513b1e66..2ded7e32c1 100644
--- a/bin/wasm-node/rust/src/lib.rs
+++ b/bin/wasm-node/rust/src/lib.rs
@@ -125,6 +125,11 @@ fn init(max_log_level: u32, enable_current_task: u32, cpu_rate_limit: u32) {
*client_lock = Some(init_out);
}
+fn start_shutdown() {
+ // TODO: do this in a clean way
+ std::process::exit(0)
+}
+
fn add_chain(
chain_spec_pointer: u32,
chain_spec_len: u32,