smoldot 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ export interface Config {
22
22
  onPanic: (message: string) => never;
23
23
  logCallback: (level: number, target: string, message: string) => void;
24
24
  jsonRpcResponsesNonEmptyCallback: (chainId: number) => void;
25
+ advanceExecutionReadyCallback: () => void;
25
26
  currentTaskCallback?: (taskName: string | null) => void;
26
27
  }
27
28
  /**
@@ -61,6 +61,9 @@ export default function (config) {
61
61
  const buf = config.bufferIndices[bufferIndex];
62
62
  new Uint8Array(instance.exports.memory.buffer).set(buf, targetPtr);
63
63
  },
64
+ advance_execution_ready: () => {
65
+ config.advanceExecutionReadyCallback();
66
+ },
64
67
  // Used by the Rust side to notify that a JSON-RPC response or subscription notification
65
68
  // is available in the queue of JSON-RPC responses.
66
69
  json_rpc_responses_non_empty: (chainId) => {
@@ -86,7 +89,7 @@ export default function (config) {
86
89
  }
87
90
  },
88
91
  // Must call `timer_finished` after the given number of milliseconds has elapsed.
89
- start_timer: (id, ms) => {
92
+ start_timer: (ms) => {
90
93
  if (killedTracked.killed)
91
94
  return;
92
95
  const instance = config.instance;
@@ -104,7 +107,7 @@ export default function (config) {
104
107
  if (killedTracked.killed)
105
108
  return;
106
109
  try {
107
- instance.exports.timer_finished(id);
110
+ instance.exports.timer_finished();
108
111
  }
109
112
  catch (_error) { }
110
113
  });
@@ -114,7 +117,7 @@ export default function (config) {
114
117
  if (killedTracked.killed)
115
118
  return;
116
119
  try {
117
- instance.exports.timer_finished(id);
120
+ instance.exports.timer_finished();
118
121
  }
119
122
  catch (_error) { }
120
123
  }, ms);
@@ -6,8 +6,8 @@
6
6
  */
7
7
  export interface SmoldotWasmExports extends WebAssembly.Exports {
8
8
  memory: WebAssembly.Memory;
9
- init: (maxLogLevel: number, enableCurrentTask: number, cpuRateLimit: number, periodicallyYield: number) => void;
10
- set_periodically_yield: (periodicallyYield: number) => void;
9
+ init: (maxLogLevel: number) => void;
10
+ advance_execution: () => number;
11
11
  start_shutdown: () => void;
12
12
  add_chain: (chainSpecBufferIndex: number, databaseContentBufferIndex: number, jsonRpcRunning: number, potentialRelayChainsBufferIndex: number) => number;
13
13
  remove_chain: (chainId: number) => void;
@@ -17,7 +17,7 @@ export interface SmoldotWasmExports extends WebAssembly.Exports {
17
17
  json_rpc_send: (textBufferIndex: number, chainId: number) => number;
18
18
  json_rpc_responses_peek: (chainId: number) => number;
19
19
  json_rpc_responses_pop: (chainId: number) => void;
20
- timer_finished: (timerId: number) => void;
20
+ timer_finished: () => void;
21
21
  connection_open_single_stream: (connectionId: number, handshakeTy: number, initialWritableBytes: number, writeClosable: number) => void;
22
22
  connection_open_multi_stream: (connectionId: number, handshakeTyBufferIndex: number) => void;
23
23
  stream_writable_bytes: (connectionId: number, streamId: number, numBytes: number) => void;
@@ -24,9 +24,9 @@ export declare class QueueFullError extends Error {
24
24
  * Contains the configuration of the instance.
25
25
  */
26
26
  export interface Config {
27
+ wasmModule: Promise<WebAssembly.Module>;
27
28
  logCallback: (level: number, target: string, message: string) => void;
28
29
  maxLogLevel: number;
29
- enableCurrentTask: boolean;
30
30
  cpuRateLimit: number;
31
31
  }
32
32
  export interface Instance {
@@ -62,65 +62,49 @@ export function start(configMessage, platformBindings) {
62
62
  const printError = { printError: true };
63
63
  // Contains the information of each chain that is currently alive.
64
64
  let chains = new Map();
65
- // Start initialization of the Wasm VM.
66
- const config = {
67
- onWasmPanic: (message) => {
68
- // TODO: consider obtaining a backtrace here
69
- crashError.error = new CrashError(message);
70
- if (!printError.printError)
71
- return;
72
- console.error("Smoldot has panicked" +
73
- (currentTask.name ? (" while executing task `" + currentTask.name + "`") : "") +
74
- ". This is a bug in smoldot. Please open an issue at " +
75
- "https://github.com/smol-dot/smoldot/issues with the following message:\n" +
76
- message);
77
- for (const chain of Array.from(chains.values())) {
78
- for (const promise of chain.jsonRpcResponsesPromises) {
79
- promise.reject(crashError.error);
80
- }
81
- chain.jsonRpcResponsesPromises = [];
82
- }
83
- },
84
- logCallback: (level, target, message) => {
85
- configMessage.logCallback(level, target, message);
86
- },
87
- jsonRpcResponsesNonEmptyCallback: (chainId) => {
88
- // Notify every single promise found in `jsonRpcResponsesPromises`.
89
- const promises = chains.get(chainId).jsonRpcResponsesPromises;
90
- while (promises.length !== 0) {
91
- promises.shift().resolve();
92
- }
93
- },
94
- currentTaskCallback: (taskName) => {
95
- currentTask.name = taskName;
96
- },
97
- cpuRateLimit: configMessage.cpuRateLimit,
98
- };
99
- state = {
100
- initialized: false, promise: instance.startInstance(config, platformBindings).then(([instance, bufferIndices]) => {
101
- // `config.cpuRateLimit` is a floating point that should be between 0 and 1, while the value
102
- // to pass as parameter must be between `0` and `2^32-1`.
103
- // The few lines of code below should handle all possible values of `number`, including
104
- // infinites and NaN.
105
- let cpuRateLimit = Math.round(config.cpuRateLimit * 4294967295); // `2^32 - 1`
106
- if (cpuRateLimit < 0)
107
- cpuRateLimit = 0;
108
- if (cpuRateLimit > 4294967295)
109
- cpuRateLimit = 4294967295;
110
- if (!Number.isFinite(cpuRateLimit))
111
- cpuRateLimit = 4294967295; // User might have passed NaN
112
- // Smoldot requires an initial call to the `init` function in order to do its internal
113
- // configuration.
114
- const [periodicallyYield, unregisterCallback] = platformBindings.registerShouldPeriodicallyYield((newValue) => {
115
- if (state.initialized && !crashError.error) {
116
- try {
117
- state.instance.exports.set_periodically_yield(newValue ? 1 : 0);
65
+ const initPromise = (() => __awaiter(this, void 0, void 0, function* () {
66
+ const module = yield configMessage.wasmModule;
67
+ // Start initialization of the Wasm VM.
68
+ const config = {
69
+ onWasmPanic: (message) => {
70
+ // TODO: consider obtaining a backtrace here
71
+ crashError.error = new CrashError(message);
72
+ if (!printError.printError)
73
+ return;
74
+ console.error("Smoldot has panicked" +
75
+ (currentTask.name ? (" while executing task `" + currentTask.name + "`") : "") +
76
+ ". This is a bug in smoldot. Please open an issue at " +
77
+ "https://github.com/smol-dot/smoldot/issues with the following message:\n" +
78
+ message);
79
+ for (const chain of Array.from(chains.values())) {
80
+ for (const promise of chain.jsonRpcResponsesPromises) {
81
+ promise.reject(crashError.error);
118
82
  }
119
- catch (_error) { }
83
+ chain.jsonRpcResponsesPromises = [];
120
84
  }
121
- });
122
- instance.exports.init(configMessage.maxLogLevel, configMessage.enableCurrentTask ? 1 : 0, cpuRateLimit, periodicallyYield ? 1 : 0);
123
- state = { initialized: true, instance, bufferIndices, unregisterCallback };
85
+ },
86
+ logCallback: (level, target, message) => {
87
+ configMessage.logCallback(level, target, message);
88
+ },
89
+ wasmModule: module,
90
+ jsonRpcResponsesNonEmptyCallback: (chainId) => {
91
+ // Notify every single promise found in `jsonRpcResponsesPromises`.
92
+ const promises = chains.get(chainId).jsonRpcResponsesPromises;
93
+ while (promises.length !== 0) {
94
+ promises.shift().resolve();
95
+ }
96
+ },
97
+ currentTaskCallback: (taskName) => {
98
+ currentTask.name = taskName;
99
+ },
100
+ cpuRateLimit: configMessage.cpuRateLimit,
101
+ maxLogLevel: configMessage.maxLogLevel,
102
+ };
103
+ return yield instance.startInstance(config, platformBindings);
104
+ }))();
105
+ state = {
106
+ initialized: false, promise: initPromise.then(([instance, bufferIndices]) => {
107
+ state = { initialized: true, instance, bufferIndices };
124
108
  return [instance, bufferIndices];
125
109
  })
126
110
  };
@@ -273,8 +257,6 @@ export function start(configMessage, platformBindings) {
273
257
  // exception when the user wants the shutdown to happen.
274
258
  if (crashError.error)
275
259
  return;
276
- if (state.initialized)
277
- state.unregisterCallback();
278
260
  try {
279
261
  printError.printError = false;
280
262
  instance.exports.start_shutdown();
@@ -15,25 +15,16 @@ export interface Config {
15
15
  */
16
16
  onWasmPanic: (message: string) => void;
17
17
  logCallback: (level: number, target: string, message: string) => void;
18
+ wasmModule: WebAssembly.Module;
18
19
  jsonRpcResponsesNonEmptyCallback: (chainId: number) => void;
19
20
  currentTaskCallback?: (taskName: string | null) => void;
21
+ maxLogLevel: number;
20
22
  cpuRateLimit: number;
21
23
  }
22
24
  /**
23
25
  * Contains functions that the client will use when it needs to leverage the platform.
24
26
  */
25
27
  export interface PlatformBindings {
26
- /**
27
- * Base64-decode the given buffer then decompress its content using the inflate algorithm
28
- * with zlib header.
29
- *
30
- * The input is considered trusted. In other words, the implementation doesn't have to
31
- * resist malicious input.
32
- *
33
- * This function is asynchronous because implementations might use the compression streams
34
- * Web API, which for whatever reason is asynchronous.
35
- */
36
- trustedBase64DecodeAndZlibInflate: (input: string) => Promise<Uint8Array>;
37
28
  /**
38
29
  * Returns the number of milliseconds since an arbitrary epoch.
39
30
  */
@@ -22,22 +22,22 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
22
22
  // along with this program. If not, see <http://www.gnu.org/licenses/>.
23
23
  import { default as smoldotLightBindingsBuilder } from './bindings-smoldot-light.js';
24
24
  import { default as wasiBindingsBuilder } from './bindings-wasi.js';
25
- import { default as wasmBase64 } from './autogen/wasm.js';
26
25
  export { ConnectionError } from './bindings-smoldot-light.js';
27
26
  export function startInstance(config, platformBindings) {
28
27
  return __awaiter(this, void 0, void 0, function* () {
29
- // The actual Wasm bytecode is base64-decoded then deflate-decoded from a constant found in a
30
- // different file.
31
- // This is suboptimal compared to using `instantiateStreaming`, but it is the most
32
- // cross-platform cross-bundler approach.
33
- const wasmBytecode = yield platformBindings.trustedBase64DecodeAndZlibInflate(wasmBase64);
34
28
  let killAll;
35
29
  const bufferIndices = new Array;
30
+ // Callback called when `advance_execution_ready` is called by the Rust code, if any.
31
+ const advanceExecutionPromise = { value: null };
36
32
  // Used to bind with the smoldot-light bindings. See the `bindings-smoldot-light.js` file.
37
33
  const smoldotJsConfig = Object.assign({ bufferIndices, connect: platformBindings.connect, onPanic: (message) => {
38
34
  killAll();
39
35
  config.onWasmPanic(message);
40
36
  throw new Error();
37
+ }, advanceExecutionReadyCallback: () => {
38
+ if (advanceExecutionPromise.value)
39
+ advanceExecutionPromise.value();
40
+ advanceExecutionPromise.value = null;
41
41
  } }, config);
42
42
  // Used to bind with the Wasi bindings. See the `bindings-wasi.js` file.
43
43
  const wasiConfig = {
@@ -55,15 +55,70 @@ export function startInstance(config, platformBindings) {
55
55
  // Start the Wasm virtual machine.
56
56
  // The Rust code defines a list of imports that must be fulfilled by the environment. The second
57
57
  // parameter provides their implementations.
58
- const result = yield WebAssembly.instantiate(wasmBytecode, {
58
+ const result = yield WebAssembly.instantiate(config.wasmModule, {
59
59
  // The functions with the "smoldot" prefix are specific to smoldot.
60
60
  "smoldot": smoldotBindings,
61
61
  // As the Rust code is compiled for wasi, some more wasi-specific imports exist.
62
62
  "wasi_snapshot_preview1": wasiBindingsBuilder(wasiConfig),
63
63
  });
64
- const instance = result.instance;
64
+ const instance = result;
65
65
  smoldotJsConfig.instance = instance;
66
66
  wasiConfig.instance = instance;
67
+ // Smoldot requires an initial call to the `init` function in order to do its internal
68
+ // configuration.
69
+ instance.exports.init(config.maxLogLevel);
70
+ (() => __awaiter(this, void 0, void 0, function* () {
71
+ // In order to avoid calling `setTimeout` too often, we accumulate sleep up until
72
+ // a certain threshold.
73
+ let missingSleep = 0;
74
+ // Extract (to make sure the value doesn't change) and sanitize `cpuRateLimit`.
75
+ let cpuRateLimit = config.cpuRateLimit;
76
+ if (isNaN(cpuRateLimit))
77
+ cpuRateLimit = 1.0;
78
+ if (cpuRateLimit > 1.0)
79
+ cpuRateLimit = 1.0;
80
+ if (cpuRateLimit < 0.0)
81
+ cpuRateLimit = 0.0;
82
+ const periodicallyYield = { value: false };
83
+ const [periodicallyYieldInit, unregisterCallback] = platformBindings.registerShouldPeriodicallyYield((newValue) => {
84
+ periodicallyYield.value = newValue;
85
+ });
86
+ periodicallyYield.value = periodicallyYieldInit;
87
+ let now = platformBindings.performanceNow();
88
+ while (true) {
89
+ const whenReadyAgain = new Promise((resolve) => advanceExecutionPromise.value = resolve);
90
+ const outcome = instance.exports.advance_execution();
91
+ if (outcome === 0) {
92
+ unregisterCallback();
93
+ break;
94
+ }
95
+ const afterExec = platformBindings.performanceNow();
96
+ const elapsed = afterExec - now;
97
+ now = afterExec;
98
+ // In order to enforce the rate limiting, we stop executing for a certain
99
+ // amount of time.
100
+ // The base equation here is: `(sleep + elapsed) * rateLimit == elapsed`,
101
+ // from which the calculation below is derived.
102
+ const sleep = elapsed * (1.0 / cpuRateLimit - 1.0);
103
+ missingSleep += sleep;
104
+ if (missingSleep > (periodicallyYield ? 5 : 1000)) {
105
+ // `setTimeout` has a maximum value, after which it will overflow. 🤦
106
+ // See <https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#maximum_delay_value>
107
+ // While adding a cap technically skews the CPU rate limiting algorithm, we don't
108
+ // really care for such extreme values.
109
+ if (missingSleep > 2147483646) // Doc says `> 2147483647`, but I don't really trust their pedanticism so let's be safe
110
+ missingSleep = 2147483646;
111
+ yield new Promise((resolve) => setTimeout(resolve, missingSleep));
112
+ missingSleep = 0;
113
+ }
114
+ yield whenReadyAgain;
115
+ const afterWait = platformBindings.performanceNow();
116
+ missingSleep -= (afterWait - now);
117
+ if (missingSleep < 0)
118
+ missingSleep = 0;
119
+ now = afterWait;
120
+ }
121
+ }))();
67
122
  return [instance, bufferIndices];
68
123
  });
69
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoldot",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Light client that connects to Polkadot and Substrate-based blockchains",
5
5
  "author": "Parity Technologies <admin@parity.io>",
6
6
  "license": "GPL-3.0-or-later WITH Classpath-exception-2.0",