@sdeverywhere/runtime-async 0.1.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020-2022 Climate Interactive / New Venture Fund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @sdeverywhere/runtime-async
2
+
3
+ This package provides an implementation of the `ModelRunner` interface from
4
+ the `@sdeverywhere/runtime` package that uses a Web Worker (in the browser)
5
+ or a worker thread (in a Node.js app) to run the model asynchronously off
6
+ the main JavaScript thread.
7
+
8
+ ## Usage
9
+
10
+ ### 1. Initialize the `WasmModel` in a Web Worker
11
+
12
+ In your app project, define a separate JavaScript file, called
13
+ `worker.js` for example, that initializes the model worker in the
14
+ context of the Web Worker:
15
+
16
+ ```js
17
+ import { initWasmModelAndBuffers } from '@sdeverywhere/runtime'
18
+ import { exposeModelWorker } from '@sdeverywhere/runtime-async/worker'
19
+
20
+ async function initWasmModel() {
21
+ const wasmModules = loadWasm()
22
+ return initWasmModelAndBuffers(...)
23
+ }
24
+
25
+ exposeModelWorker(initWasmModel)
26
+ ```
27
+
28
+ ### 2. Spawn the worker
29
+
30
+ In your web app, call the `spawnAsyncModelRunner` function, which will
31
+ spawn the Web Worker and initialize the `ModelRunner` that communicates
32
+ with the worker:
33
+
34
+ ```js
35
+ import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async/runner'
36
+
37
+ async function initApp() {
38
+ // ...
39
+ const runner = await spawnAsyncModelRunner({ path: './worker.js' })
40
+ // ...
41
+ }
42
+ ```
43
+
44
+ ## Documentation
45
+
46
+ API documentation is available in the [`docs`](./docs/index.md) directory.
47
+
48
+ ## License
49
+
50
+ SDEverywhere is distributed under the MIT license. See `LICENSE` for more details.
package/dist/index.cjs ADDED
@@ -0,0 +1,138 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var src_exports = {};
21
+ __export(src_exports, {
22
+ exposeModelWorker: () => exposeModelWorker,
23
+ spawnAsyncModelRunner: () => spawnAsyncModelRunner
24
+ });
25
+ module.exports = __toCommonJS(src_exports);
26
+
27
+ // src/runner.ts
28
+ var import_threads = require("threads");
29
+ async function spawnAsyncModelRunner(workerSpec) {
30
+ if (workerSpec["path"]) {
31
+ return spawnAsyncModelRunnerWithWorker(new import_threads.Worker(workerSpec["path"]));
32
+ } else {
33
+ return spawnAsyncModelRunnerWithWorker(import_threads.BlobWorker.fromText(workerSpec["source"]));
34
+ }
35
+ }
36
+ async function spawnAsyncModelRunnerWithWorker(worker) {
37
+ const modelWorker2 = await (0, import_threads.spawn)(worker);
38
+ const initResult = await modelWorker2.initModel();
39
+ const rowLength = initResult.rowLength;
40
+ let ioBuffer = initResult.ioBuffer;
41
+ let running = false;
42
+ let terminated = false;
43
+ return {
44
+ runModel: async (inputs, outputs) => {
45
+ if (terminated) {
46
+ throw new Error("Async model runner has already been terminated");
47
+ } else if (running) {
48
+ throw new Error("Async model runner only supports one `runModel` call at a time");
49
+ } else {
50
+ running = true;
51
+ }
52
+ const runTimeLengthInElements = 1;
53
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
54
+ const inputsLengthInElements = inputs.length;
55
+ const inputsLengthInBytes = inputsLengthInElements * 8;
56
+ const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
57
+ for (let i = 0; i < inputs.length; i++) {
58
+ inputsArray[i] = inputs[i].get();
59
+ }
60
+ try {
61
+ ioBuffer = await modelWorker2.runModel((0, import_threads.Transfer)(ioBuffer));
62
+ } finally {
63
+ running = false;
64
+ }
65
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
66
+ outputs.runTimeInMillis = runTimeBufferArray[0];
67
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
68
+ const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
69
+ outputs.updateFromBuffer(outputsArray, rowLength);
70
+ return outputs;
71
+ },
72
+ terminate: () => {
73
+ if (terminated) {
74
+ return Promise.resolve();
75
+ } else {
76
+ terminated = true;
77
+ return import_threads.Thread.terminate(modelWorker2);
78
+ }
79
+ }
80
+ };
81
+ }
82
+
83
+ // src/worker.ts
84
+ var import_worker = require("threads/worker");
85
+ var import_runtime = require("@sdeverywhere/runtime");
86
+ var initWasmModel;
87
+ var wasmModel;
88
+ var inputsWasmBuffer;
89
+ var outputsWasmBuffer;
90
+ var modelWorker = {
91
+ async initModel() {
92
+ if (wasmModel) {
93
+ throw new Error("WasmModel was already initialized");
94
+ }
95
+ const result = await initWasmModel();
96
+ wasmModel = result.model;
97
+ inputsWasmBuffer = result.inputsBuffer;
98
+ outputsWasmBuffer = result.outputsBuffer;
99
+ const runTimeLength = 8;
100
+ const inputsLength = inputsWasmBuffer.getArrayView().length;
101
+ const outputsLength = outputsWasmBuffer.getArrayView().length;
102
+ const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength);
103
+ const rowLength = result.endTime - result.startTime + 1;
104
+ const ioBuffer = ioArray.buffer;
105
+ return (0, import_worker.Transfer)({ rowLength, ioBuffer }, [ioBuffer]);
106
+ },
107
+ runModel(ioBuffer) {
108
+ if (!wasmModel) {
109
+ throw new Error("WasmModel must be initialized before running the model in worker");
110
+ }
111
+ const runTimeLengthInElements = 1;
112
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
113
+ const inputsWasmArray = inputsWasmBuffer.getArrayView();
114
+ const inputsLengthInElements = inputsWasmArray.length;
115
+ const inputsLengthInBytes = inputsLengthInElements * 8;
116
+ const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
117
+ inputsWasmArray.set(inputsBufferArray);
118
+ const t0 = (0, import_runtime.perfNow)();
119
+ wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer);
120
+ const elapsed = (0, import_runtime.perfElapsed)(t0);
121
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
122
+ runTimeBufferArray[0] = elapsed;
123
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
124
+ const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
125
+ outputsBufferArray.set(outputsWasmBuffer.getArrayView());
126
+ return (0, import_worker.Transfer)(ioBuffer);
127
+ }
128
+ };
129
+ function exposeModelWorker(init) {
130
+ initWasmModel = init;
131
+ (0, import_worker.expose)(modelWorker);
132
+ }
133
+ // Annotate the CommonJS export names for ESM import in node:
134
+ 0 && (module.exports = {
135
+ exposeModelWorker,
136
+ spawnAsyncModelRunner
137
+ });
138
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/runner.ts","../src/worker.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nexport { spawnAsyncModelRunner } from './runner'\n\nexport { exposeModelWorker } from './worker'\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport { BlobWorker, spawn, Thread, Transfer, Worker } from 'threads'\n\nimport type { ModelRunner } from '@sdeverywhere/runtime'\n\n/**\n * Initialize a `ModelRunner` that runs the model asynchronously in a worker thread.\n *\n * In your app project, define a JavaScript file, called `worker.js` for example, that\n * initializes the model worker in the context of the Web Worker:\n *\n * ```js\n * import { initWasmModelAndBuffers } from '@sdeverywhere/runtime'\n * import { exposeModelWorker } from '@sdeverywhere/runtime-async/worker'\n *\n * async function initWasmModel() {\n * const wasmModules = loadWasm()\n * return initWasmModelAndBuffers(...)\n * }\n *\n * exposeModelWorker(initWasmModel)\n * ```\n *\n * Then, in your web app, call the `spawnAsyncModelRunner` function, which\n * will spawn the Web Worker and initialize the `ModelRunner` that communicates\n * with the worker:\n *\n * ```js\n * import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async/runner'\n *\n * async function initApp() {\n * // ...\n * const runner = await spawnAsyncModelRunner({ path: './worker.js' })\n * // ...\n * }\n * ```\n *\n * @param workerSpec Either a `path` to the worker JavaScript file, or the `source`\n * containing the full JavaScript source of the worker.\n */\nexport async function spawnAsyncModelRunner(workerSpec: { path: string } | { source: string }): Promise<ModelRunner> {\n if (workerSpec['path']) {\n return spawnAsyncModelRunnerWithWorker(new Worker(workerSpec['path']))\n } else {\n return spawnAsyncModelRunnerWithWorker(BlobWorker.fromText(workerSpec['source']))\n }\n}\n\n/**\n * @hidden For internal use only\n */\nasync function spawnAsyncModelRunnerWithWorker(worker: Worker): Promise<ModelRunner> {\n // Spawn the given Worker that contains the `ModelWorker`\n const modelWorker = await spawn(worker)\n\n // Wait for the worker to initialize the wasm model (in the worker thread)\n const initResult = await modelWorker.initModel()\n const rowLength: number = initResult.rowLength\n let ioBuffer: ArrayBuffer = initResult.ioBuffer\n\n // Use a flag to ensure that only one request is made at a time\n let running = false\n\n // Disallow `runModel` after the runner has been terminated\n let terminated = false\n\n return {\n runModel: async (inputs, outputs) => {\n if (terminated) {\n throw new Error('Async model runner has already been terminated')\n } else if (running) {\n throw new Error('Async model runner only supports one `runModel` call at a time')\n } else {\n running = true\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Capture the current set of input values into the reusable buffer\n const inputsLengthInElements = inputs.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n for (let i = 0; i < inputs.length; i++) {\n inputsArray[i] = inputs[i].get()\n }\n\n // Run the model in the worker. We pass the underlying `ArrayBuffer`\n // instance back to the worker wrapped in a `Transfer` to make it\n // no-copy transferable, and then the worker will return it back\n // to us.\n try {\n ioBuffer = await modelWorker.runModel(Transfer(ioBuffer))\n } finally {\n running = false\n }\n\n // Save the model run time\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n outputs.runTimeInMillis = runTimeBufferArray[0]\n\n // Capture the outputs array by copying the data into the given `Outputs`\n // data structure\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputs.updateFromBuffer(outputsArray, rowLength)\n\n return outputs\n },\n\n terminate: () => {\n if (terminated) {\n return Promise.resolve()\n } else {\n terminated = true\n return Thread.terminate(modelWorker)\n }\n }\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { TransferDescriptor } from 'threads'\nimport { expose, Transfer } from 'threads/worker'\n\nimport type { WasmBuffer, WasmModel, WasmModelInitResult } from '@sdeverywhere/runtime'\nimport { perfElapsed, perfNow } from '@sdeverywhere/runtime'\n\n/** @hidden */\nlet initWasmModel: () => Promise<WasmModelInitResult>\n/** @hidden */\nlet wasmModel: WasmModel\n/** @hidden */\nlet inputsWasmBuffer: WasmBuffer\n/** @hidden */\nlet outputsWasmBuffer: WasmBuffer\n\ninterface InitResult {\n rowLength: number\n ioBuffer: ArrayBuffer\n}\n\n/** @hidden */\nconst modelWorker = {\n async initModel(): Promise<TransferDescriptor<InitResult>> {\n if (wasmModel) {\n throw new Error('WasmModel was already initialized')\n }\n\n // Initialize the wasm model and associated buffers\n const result = await initWasmModel()\n\n // Capture the `WasmModel` instance and `WasmBuffer` instances\n wasmModel = result.model\n inputsWasmBuffer = result.inputsBuffer\n outputsWasmBuffer = result.outputsBuffer\n\n // Create a combined array that will hold a copy of the inputs and outputs\n // wasm buffers; this buffer is no-copy transferable, whereas the wasm ones\n // are not allowed to be transferred\n const runTimeLength = 8\n const inputsLength = inputsWasmBuffer.getArrayView().length\n const outputsLength = outputsWasmBuffer.getArrayView().length\n const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength)\n\n // The row length is the number of elements in each row of the outputs buffer\n const rowLength = result.endTime - result.startTime + 1\n\n // Transfer the underlying buffer to the runner\n const ioBuffer = ioArray.buffer\n return Transfer({ rowLength, ioBuffer }, [ioBuffer])\n },\n\n runModel(ioBuffer: ArrayBuffer): TransferDescriptor<ArrayBuffer> {\n if (!wasmModel) {\n throw new Error('WasmModel must be initialized before running the model in worker')\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Copy the inputs into the wasm inputs buffer\n const inputsWasmArray = inputsWasmBuffer.getArrayView()\n const inputsLengthInElements = inputsWasmArray.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n inputsWasmArray.set(inputsBufferArray)\n\n // Run the model using the wasm buffers\n const t0 = perfNow()\n wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer)\n const elapsed = perfElapsed(t0)\n\n // Write the model run time to the buffer\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n runTimeBufferArray[0] = elapsed\n\n // Copy the outputs from the wasm outputs buffer\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputsBufferArray.set(outputsWasmBuffer.getArrayView())\n\n // Transfer the buffer back to the runner\n return Transfer(ioBuffer)\n }\n}\n\n/**\n * Expose an object in the current worker thread that communicates with the\n * `ModelRunner` instance running in the main thread. The exposed worker\n * object will take care of running the `WasmModel` on the worker thread\n * and sending the outputs back to the main process.\n *\n * @param init The function that initializes the `WasmModel` instance that\n * is used in the worker thread.\n */\nexport function exposeModelWorker(init: () => Promise<WasmModelInitResult>): void {\n // Save the initializer, which will be used when the runner calls `initModel`\n // on the worker\n initWasmModel = init\n\n // Expose the worker implementation to `threads.js`\n expose(modelWorker)\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,3 @@
1
+ export { spawnAsyncModelRunner } from './runner.js';
2
+ export { exposeModelWorker } from './worker.js';
3
+ import '@sdeverywhere/runtime';
package/dist/index.js ADDED
@@ -0,0 +1,111 @@
1
+ // src/runner.ts
2
+ import { BlobWorker, spawn, Thread, Transfer, Worker } from "threads";
3
+ async function spawnAsyncModelRunner(workerSpec) {
4
+ if (workerSpec["path"]) {
5
+ return spawnAsyncModelRunnerWithWorker(new Worker(workerSpec["path"]));
6
+ } else {
7
+ return spawnAsyncModelRunnerWithWorker(BlobWorker.fromText(workerSpec["source"]));
8
+ }
9
+ }
10
+ async function spawnAsyncModelRunnerWithWorker(worker) {
11
+ const modelWorker2 = await spawn(worker);
12
+ const initResult = await modelWorker2.initModel();
13
+ const rowLength = initResult.rowLength;
14
+ let ioBuffer = initResult.ioBuffer;
15
+ let running = false;
16
+ let terminated = false;
17
+ return {
18
+ runModel: async (inputs, outputs) => {
19
+ if (terminated) {
20
+ throw new Error("Async model runner has already been terminated");
21
+ } else if (running) {
22
+ throw new Error("Async model runner only supports one `runModel` call at a time");
23
+ } else {
24
+ running = true;
25
+ }
26
+ const runTimeLengthInElements = 1;
27
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
28
+ const inputsLengthInElements = inputs.length;
29
+ const inputsLengthInBytes = inputsLengthInElements * 8;
30
+ const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
31
+ for (let i = 0; i < inputs.length; i++) {
32
+ inputsArray[i] = inputs[i].get();
33
+ }
34
+ try {
35
+ ioBuffer = await modelWorker2.runModel(Transfer(ioBuffer));
36
+ } finally {
37
+ running = false;
38
+ }
39
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
40
+ outputs.runTimeInMillis = runTimeBufferArray[0];
41
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
42
+ const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
43
+ outputs.updateFromBuffer(outputsArray, rowLength);
44
+ return outputs;
45
+ },
46
+ terminate: () => {
47
+ if (terminated) {
48
+ return Promise.resolve();
49
+ } else {
50
+ terminated = true;
51
+ return Thread.terminate(modelWorker2);
52
+ }
53
+ }
54
+ };
55
+ }
56
+
57
+ // src/worker.ts
58
+ import { expose, Transfer as Transfer2 } from "threads/worker";
59
+ import { perfElapsed, perfNow } from "@sdeverywhere/runtime";
60
+ var initWasmModel;
61
+ var wasmModel;
62
+ var inputsWasmBuffer;
63
+ var outputsWasmBuffer;
64
+ var modelWorker = {
65
+ async initModel() {
66
+ if (wasmModel) {
67
+ throw new Error("WasmModel was already initialized");
68
+ }
69
+ const result = await initWasmModel();
70
+ wasmModel = result.model;
71
+ inputsWasmBuffer = result.inputsBuffer;
72
+ outputsWasmBuffer = result.outputsBuffer;
73
+ const runTimeLength = 8;
74
+ const inputsLength = inputsWasmBuffer.getArrayView().length;
75
+ const outputsLength = outputsWasmBuffer.getArrayView().length;
76
+ const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength);
77
+ const rowLength = result.endTime - result.startTime + 1;
78
+ const ioBuffer = ioArray.buffer;
79
+ return Transfer2({ rowLength, ioBuffer }, [ioBuffer]);
80
+ },
81
+ runModel(ioBuffer) {
82
+ if (!wasmModel) {
83
+ throw new Error("WasmModel must be initialized before running the model in worker");
84
+ }
85
+ const runTimeLengthInElements = 1;
86
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
87
+ const inputsWasmArray = inputsWasmBuffer.getArrayView();
88
+ const inputsLengthInElements = inputsWasmArray.length;
89
+ const inputsLengthInBytes = inputsLengthInElements * 8;
90
+ const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
91
+ inputsWasmArray.set(inputsBufferArray);
92
+ const t0 = perfNow();
93
+ wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer);
94
+ const elapsed = perfElapsed(t0);
95
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
96
+ runTimeBufferArray[0] = elapsed;
97
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
98
+ const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
99
+ outputsBufferArray.set(outputsWasmBuffer.getArrayView());
100
+ return Transfer2(ioBuffer);
101
+ }
102
+ };
103
+ function exposeModelWorker(init) {
104
+ initWasmModel = init;
105
+ expose(modelWorker);
106
+ }
107
+ export {
108
+ exposeModelWorker,
109
+ spawnAsyncModelRunner
110
+ };
111
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runner.ts","../src/worker.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport { BlobWorker, spawn, Thread, Transfer, Worker } from 'threads'\n\nimport type { ModelRunner } from '@sdeverywhere/runtime'\n\n/**\n * Initialize a `ModelRunner` that runs the model asynchronously in a worker thread.\n *\n * In your app project, define a JavaScript file, called `worker.js` for example, that\n * initializes the model worker in the context of the Web Worker:\n *\n * ```js\n * import { initWasmModelAndBuffers } from '@sdeverywhere/runtime'\n * import { exposeModelWorker } from '@sdeverywhere/runtime-async/worker'\n *\n * async function initWasmModel() {\n * const wasmModules = loadWasm()\n * return initWasmModelAndBuffers(...)\n * }\n *\n * exposeModelWorker(initWasmModel)\n * ```\n *\n * Then, in your web app, call the `spawnAsyncModelRunner` function, which\n * will spawn the Web Worker and initialize the `ModelRunner` that communicates\n * with the worker:\n *\n * ```js\n * import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async/runner'\n *\n * async function initApp() {\n * // ...\n * const runner = await spawnAsyncModelRunner({ path: './worker.js' })\n * // ...\n * }\n * ```\n *\n * @param workerSpec Either a `path` to the worker JavaScript file, or the `source`\n * containing the full JavaScript source of the worker.\n */\nexport async function spawnAsyncModelRunner(workerSpec: { path: string } | { source: string }): Promise<ModelRunner> {\n if (workerSpec['path']) {\n return spawnAsyncModelRunnerWithWorker(new Worker(workerSpec['path']))\n } else {\n return spawnAsyncModelRunnerWithWorker(BlobWorker.fromText(workerSpec['source']))\n }\n}\n\n/**\n * @hidden For internal use only\n */\nasync function spawnAsyncModelRunnerWithWorker(worker: Worker): Promise<ModelRunner> {\n // Spawn the given Worker that contains the `ModelWorker`\n const modelWorker = await spawn(worker)\n\n // Wait for the worker to initialize the wasm model (in the worker thread)\n const initResult = await modelWorker.initModel()\n const rowLength: number = initResult.rowLength\n let ioBuffer: ArrayBuffer = initResult.ioBuffer\n\n // Use a flag to ensure that only one request is made at a time\n let running = false\n\n // Disallow `runModel` after the runner has been terminated\n let terminated = false\n\n return {\n runModel: async (inputs, outputs) => {\n if (terminated) {\n throw new Error('Async model runner has already been terminated')\n } else if (running) {\n throw new Error('Async model runner only supports one `runModel` call at a time')\n } else {\n running = true\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Capture the current set of input values into the reusable buffer\n const inputsLengthInElements = inputs.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n for (let i = 0; i < inputs.length; i++) {\n inputsArray[i] = inputs[i].get()\n }\n\n // Run the model in the worker. We pass the underlying `ArrayBuffer`\n // instance back to the worker wrapped in a `Transfer` to make it\n // no-copy transferable, and then the worker will return it back\n // to us.\n try {\n ioBuffer = await modelWorker.runModel(Transfer(ioBuffer))\n } finally {\n running = false\n }\n\n // Save the model run time\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n outputs.runTimeInMillis = runTimeBufferArray[0]\n\n // Capture the outputs array by copying the data into the given `Outputs`\n // data structure\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputs.updateFromBuffer(outputsArray, rowLength)\n\n return outputs\n },\n\n terminate: () => {\n if (terminated) {\n return Promise.resolve()\n } else {\n terminated = true\n return Thread.terminate(modelWorker)\n }\n }\n }\n}\n","// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { TransferDescriptor } from 'threads'\nimport { expose, Transfer } from 'threads/worker'\n\nimport type { WasmBuffer, WasmModel, WasmModelInitResult } from '@sdeverywhere/runtime'\nimport { perfElapsed, perfNow } from '@sdeverywhere/runtime'\n\n/** @hidden */\nlet initWasmModel: () => Promise<WasmModelInitResult>\n/** @hidden */\nlet wasmModel: WasmModel\n/** @hidden */\nlet inputsWasmBuffer: WasmBuffer\n/** @hidden */\nlet outputsWasmBuffer: WasmBuffer\n\ninterface InitResult {\n rowLength: number\n ioBuffer: ArrayBuffer\n}\n\n/** @hidden */\nconst modelWorker = {\n async initModel(): Promise<TransferDescriptor<InitResult>> {\n if (wasmModel) {\n throw new Error('WasmModel was already initialized')\n }\n\n // Initialize the wasm model and associated buffers\n const result = await initWasmModel()\n\n // Capture the `WasmModel` instance and `WasmBuffer` instances\n wasmModel = result.model\n inputsWasmBuffer = result.inputsBuffer\n outputsWasmBuffer = result.outputsBuffer\n\n // Create a combined array that will hold a copy of the inputs and outputs\n // wasm buffers; this buffer is no-copy transferable, whereas the wasm ones\n // are not allowed to be transferred\n const runTimeLength = 8\n const inputsLength = inputsWasmBuffer.getArrayView().length\n const outputsLength = outputsWasmBuffer.getArrayView().length\n const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength)\n\n // The row length is the number of elements in each row of the outputs buffer\n const rowLength = result.endTime - result.startTime + 1\n\n // Transfer the underlying buffer to the runner\n const ioBuffer = ioArray.buffer\n return Transfer({ rowLength, ioBuffer }, [ioBuffer])\n },\n\n runModel(ioBuffer: ArrayBuffer): TransferDescriptor<ArrayBuffer> {\n if (!wasmModel) {\n throw new Error('WasmModel must be initialized before running the model in worker')\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Copy the inputs into the wasm inputs buffer\n const inputsWasmArray = inputsWasmBuffer.getArrayView()\n const inputsLengthInElements = inputsWasmArray.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n inputsWasmArray.set(inputsBufferArray)\n\n // Run the model using the wasm buffers\n const t0 = perfNow()\n wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer)\n const elapsed = perfElapsed(t0)\n\n // Write the model run time to the buffer\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n runTimeBufferArray[0] = elapsed\n\n // Copy the outputs from the wasm outputs buffer\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputsBufferArray.set(outputsWasmBuffer.getArrayView())\n\n // Transfer the buffer back to the runner\n return Transfer(ioBuffer)\n }\n}\n\n/**\n * Expose an object in the current worker thread that communicates with the\n * `ModelRunner` instance running in the main thread. The exposed worker\n * object will take care of running the `WasmModel` on the worker thread\n * and sending the outputs back to the main process.\n *\n * @param init The function that initializes the `WasmModel` instance that\n * is used in the worker thread.\n */\nexport function exposeModelWorker(init: () => Promise<WasmModelInitResult>): void {\n // Save the initializer, which will be used when the runner calls `initModel`\n // on the worker\n initWasmModel = init\n\n // Expose the worker implementation to `threads.js`\n expose(modelWorker)\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,83 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/runner.ts
20
+ var runner_exports = {};
21
+ __export(runner_exports, {
22
+ spawnAsyncModelRunner: () => spawnAsyncModelRunner
23
+ });
24
+ module.exports = __toCommonJS(runner_exports);
25
+ var import_threads = require("threads");
26
+ async function spawnAsyncModelRunner(workerSpec) {
27
+ if (workerSpec["path"]) {
28
+ return spawnAsyncModelRunnerWithWorker(new import_threads.Worker(workerSpec["path"]));
29
+ } else {
30
+ return spawnAsyncModelRunnerWithWorker(import_threads.BlobWorker.fromText(workerSpec["source"]));
31
+ }
32
+ }
33
+ async function spawnAsyncModelRunnerWithWorker(worker) {
34
+ const modelWorker = await (0, import_threads.spawn)(worker);
35
+ const initResult = await modelWorker.initModel();
36
+ const rowLength = initResult.rowLength;
37
+ let ioBuffer = initResult.ioBuffer;
38
+ let running = false;
39
+ let terminated = false;
40
+ return {
41
+ runModel: async (inputs, outputs) => {
42
+ if (terminated) {
43
+ throw new Error("Async model runner has already been terminated");
44
+ } else if (running) {
45
+ throw new Error("Async model runner only supports one `runModel` call at a time");
46
+ } else {
47
+ running = true;
48
+ }
49
+ const runTimeLengthInElements = 1;
50
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
51
+ const inputsLengthInElements = inputs.length;
52
+ const inputsLengthInBytes = inputsLengthInElements * 8;
53
+ const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
54
+ for (let i = 0; i < inputs.length; i++) {
55
+ inputsArray[i] = inputs[i].get();
56
+ }
57
+ try {
58
+ ioBuffer = await modelWorker.runModel((0, import_threads.Transfer)(ioBuffer));
59
+ } finally {
60
+ running = false;
61
+ }
62
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
63
+ outputs.runTimeInMillis = runTimeBufferArray[0];
64
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
65
+ const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
66
+ outputs.updateFromBuffer(outputsArray, rowLength);
67
+ return outputs;
68
+ },
69
+ terminate: () => {
70
+ if (terminated) {
71
+ return Promise.resolve();
72
+ } else {
73
+ terminated = true;
74
+ return import_threads.Thread.terminate(modelWorker);
75
+ }
76
+ }
77
+ };
78
+ }
79
+ // Annotate the CommonJS export names for ESM import in node:
80
+ 0 && (module.exports = {
81
+ spawnAsyncModelRunner
82
+ });
83
+ //# sourceMappingURL=runner.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runner.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport { BlobWorker, spawn, Thread, Transfer, Worker } from 'threads'\n\nimport type { ModelRunner } from '@sdeverywhere/runtime'\n\n/**\n * Initialize a `ModelRunner` that runs the model asynchronously in a worker thread.\n *\n * In your app project, define a JavaScript file, called `worker.js` for example, that\n * initializes the model worker in the context of the Web Worker:\n *\n * ```js\n * import { initWasmModelAndBuffers } from '@sdeverywhere/runtime'\n * import { exposeModelWorker } from '@sdeverywhere/runtime-async/worker'\n *\n * async function initWasmModel() {\n * const wasmModules = loadWasm()\n * return initWasmModelAndBuffers(...)\n * }\n *\n * exposeModelWorker(initWasmModel)\n * ```\n *\n * Then, in your web app, call the `spawnAsyncModelRunner` function, which\n * will spawn the Web Worker and initialize the `ModelRunner` that communicates\n * with the worker:\n *\n * ```js\n * import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async/runner'\n *\n * async function initApp() {\n * // ...\n * const runner = await spawnAsyncModelRunner({ path: './worker.js' })\n * // ...\n * }\n * ```\n *\n * @param workerSpec Either a `path` to the worker JavaScript file, or the `source`\n * containing the full JavaScript source of the worker.\n */\nexport async function spawnAsyncModelRunner(workerSpec: { path: string } | { source: string }): Promise<ModelRunner> {\n if (workerSpec['path']) {\n return spawnAsyncModelRunnerWithWorker(new Worker(workerSpec['path']))\n } else {\n return spawnAsyncModelRunnerWithWorker(BlobWorker.fromText(workerSpec['source']))\n }\n}\n\n/**\n * @hidden For internal use only\n */\nasync function spawnAsyncModelRunnerWithWorker(worker: Worker): Promise<ModelRunner> {\n // Spawn the given Worker that contains the `ModelWorker`\n const modelWorker = await spawn(worker)\n\n // Wait for the worker to initialize the wasm model (in the worker thread)\n const initResult = await modelWorker.initModel()\n const rowLength: number = initResult.rowLength\n let ioBuffer: ArrayBuffer = initResult.ioBuffer\n\n // Use a flag to ensure that only one request is made at a time\n let running = false\n\n // Disallow `runModel` after the runner has been terminated\n let terminated = false\n\n return {\n runModel: async (inputs, outputs) => {\n if (terminated) {\n throw new Error('Async model runner has already been terminated')\n } else if (running) {\n throw new Error('Async model runner only supports one `runModel` call at a time')\n } else {\n running = true\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Capture the current set of input values into the reusable buffer\n const inputsLengthInElements = inputs.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n for (let i = 0; i < inputs.length; i++) {\n inputsArray[i] = inputs[i].get()\n }\n\n // Run the model in the worker. We pass the underlying `ArrayBuffer`\n // instance back to the worker wrapped in a `Transfer` to make it\n // no-copy transferable, and then the worker will return it back\n // to us.\n try {\n ioBuffer = await modelWorker.runModel(Transfer(ioBuffer))\n } finally {\n running = false\n }\n\n // Save the model run time\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n outputs.runTimeInMillis = runTimeBufferArray[0]\n\n // Capture the outputs array by copying the data into the given `Outputs`\n // data structure\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputs.updateFromBuffer(outputsArray, rowLength)\n\n return outputs\n },\n\n terminate: () => {\n if (terminated) {\n return Promise.resolve()\n } else {\n terminated = true\n return Thread.terminate(modelWorker)\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,qBAA4D;AAuC5D,qCAA4C,YAAyE;AACnH,MAAI,WAAW,SAAS;AACtB,WAAO,gCAAgC,IAAI,sBAAO,WAAW,OAAO,CAAC;AAAA,EACvE,OAAO;AACL,WAAO,gCAAgC,0BAAW,SAAS,WAAW,SAAS,CAAC;AAAA,EAClF;AACF;AAKA,+CAA+C,QAAsC;AAEnF,QAAM,cAAc,MAAM,0BAAM,MAAM;AAGtC,QAAM,aAAa,MAAM,YAAY,UAAU;AAC/C,QAAM,YAAoB,WAAW;AACrC,MAAI,WAAwB,WAAW;AAGvC,MAAI,UAAU;AAGd,MAAI,aAAa;AAEjB,SAAO;AAAA,IACL,UAAU,OAAO,QAAQ,YAAY;AACnC,UAAI,YAAY;AACd,cAAM,IAAI,MAAM,gDAAgD;AAAA,MAClE,WAAW,SAAS;AAClB,cAAM,IAAI,MAAM,gEAAgE;AAAA,MAClF,OAAO;AACL,kBAAU;AAAA,MACZ;AAGA,YAAM,0BAA0B;AAChC,YAAM,uBAAuB,0BAA0B;AAGvD,YAAM,yBAAyB,OAAO;AACtC,YAAM,sBAAsB,yBAAyB;AACrD,YAAM,cAAc,IAAI,aAAa,UAAU,sBAAsB,sBAAsB;AAC3F,eAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,oBAAY,KAAK,OAAO,GAAG,IAAI;AAAA,MACjC;AAMA,UAAI;AACF,mBAAW,MAAM,YAAY,SAAS,6BAAS,QAAQ,CAAC;AAAA,MAC1D,UAAE;AACA,kBAAU;AAAA,MACZ;AAGA,YAAM,qBAAqB,IAAI,aAAa,UAAU,GAAG,uBAAuB;AAChF,cAAQ,kBAAkB,mBAAmB;AAI7C,YAAM,uBAAuB,uBAAuB;AACpD,YAAM,eAAe,IAAI,aAAa,UAAU,oBAAoB;AACpE,cAAQ,iBAAiB,cAAc,SAAS;AAEhD,aAAO;AAAA,IACT;AAAA,IAEA,WAAW,MAAM;AACf,UAAI,YAAY;AACd,eAAO,QAAQ,QAAQ;AAAA,MACzB,OAAO;AACL,qBAAa;AACb,eAAO,sBAAO,UAAU,WAAW;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,44 @@
1
+ import { ModelRunner } from '@sdeverywhere/runtime';
2
+
3
+ /**
4
+ * Initialize a `ModelRunner` that runs the model asynchronously in a worker thread.
5
+ *
6
+ * In your app project, define a JavaScript file, called `worker.js` for example, that
7
+ * initializes the model worker in the context of the Web Worker:
8
+ *
9
+ * ```js
10
+ * import { initWasmModelAndBuffers } from '@sdeverywhere/runtime'
11
+ * import { exposeModelWorker } from '@sdeverywhere/runtime-async/worker'
12
+ *
13
+ * async function initWasmModel() {
14
+ * const wasmModules = loadWasm()
15
+ * return initWasmModelAndBuffers(...)
16
+ * }
17
+ *
18
+ * exposeModelWorker(initWasmModel)
19
+ * ```
20
+ *
21
+ * Then, in your web app, call the `spawnAsyncModelRunner` function, which
22
+ * will spawn the Web Worker and initialize the `ModelRunner` that communicates
23
+ * with the worker:
24
+ *
25
+ * ```js
26
+ * import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async/runner'
27
+ *
28
+ * async function initApp() {
29
+ * // ...
30
+ * const runner = await spawnAsyncModelRunner({ path: './worker.js' })
31
+ * // ...
32
+ * }
33
+ * ```
34
+ *
35
+ * @param workerSpec Either a `path` to the worker JavaScript file, or the `source`
36
+ * containing the full JavaScript source of the worker.
37
+ */
38
+ declare function spawnAsyncModelRunner(workerSpec: {
39
+ path: string;
40
+ } | {
41
+ source: string;
42
+ }): Promise<ModelRunner>;
43
+
44
+ export { spawnAsyncModelRunner };
package/dist/runner.js ADDED
@@ -0,0 +1,59 @@
1
+ // src/runner.ts
2
+ import { BlobWorker, spawn, Thread, Transfer, Worker } from "threads";
3
+ async function spawnAsyncModelRunner(workerSpec) {
4
+ if (workerSpec["path"]) {
5
+ return spawnAsyncModelRunnerWithWorker(new Worker(workerSpec["path"]));
6
+ } else {
7
+ return spawnAsyncModelRunnerWithWorker(BlobWorker.fromText(workerSpec["source"]));
8
+ }
9
+ }
10
+ async function spawnAsyncModelRunnerWithWorker(worker) {
11
+ const modelWorker = await spawn(worker);
12
+ const initResult = await modelWorker.initModel();
13
+ const rowLength = initResult.rowLength;
14
+ let ioBuffer = initResult.ioBuffer;
15
+ let running = false;
16
+ let terminated = false;
17
+ return {
18
+ runModel: async (inputs, outputs) => {
19
+ if (terminated) {
20
+ throw new Error("Async model runner has already been terminated");
21
+ } else if (running) {
22
+ throw new Error("Async model runner only supports one `runModel` call at a time");
23
+ } else {
24
+ running = true;
25
+ }
26
+ const runTimeLengthInElements = 1;
27
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
28
+ const inputsLengthInElements = inputs.length;
29
+ const inputsLengthInBytes = inputsLengthInElements * 8;
30
+ const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
31
+ for (let i = 0; i < inputs.length; i++) {
32
+ inputsArray[i] = inputs[i].get();
33
+ }
34
+ try {
35
+ ioBuffer = await modelWorker.runModel(Transfer(ioBuffer));
36
+ } finally {
37
+ running = false;
38
+ }
39
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
40
+ outputs.runTimeInMillis = runTimeBufferArray[0];
41
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
42
+ const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
43
+ outputs.updateFromBuffer(outputsArray, rowLength);
44
+ return outputs;
45
+ },
46
+ terminate: () => {
47
+ if (terminated) {
48
+ return Promise.resolve();
49
+ } else {
50
+ terminated = true;
51
+ return Thread.terminate(modelWorker);
52
+ }
53
+ }
54
+ };
55
+ }
56
+ export {
57
+ spawnAsyncModelRunner
58
+ };
59
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/runner.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport { BlobWorker, spawn, Thread, Transfer, Worker } from 'threads'\n\nimport type { ModelRunner } from '@sdeverywhere/runtime'\n\n/**\n * Initialize a `ModelRunner` that runs the model asynchronously in a worker thread.\n *\n * In your app project, define a JavaScript file, called `worker.js` for example, that\n * initializes the model worker in the context of the Web Worker:\n *\n * ```js\n * import { initWasmModelAndBuffers } from '@sdeverywhere/runtime'\n * import { exposeModelWorker } from '@sdeverywhere/runtime-async/worker'\n *\n * async function initWasmModel() {\n * const wasmModules = loadWasm()\n * return initWasmModelAndBuffers(...)\n * }\n *\n * exposeModelWorker(initWasmModel)\n * ```\n *\n * Then, in your web app, call the `spawnAsyncModelRunner` function, which\n * will spawn the Web Worker and initialize the `ModelRunner` that communicates\n * with the worker:\n *\n * ```js\n * import { spawnAsyncModelRunner } from '@sdeverywhere/runtime-async/runner'\n *\n * async function initApp() {\n * // ...\n * const runner = await spawnAsyncModelRunner({ path: './worker.js' })\n * // ...\n * }\n * ```\n *\n * @param workerSpec Either a `path` to the worker JavaScript file, or the `source`\n * containing the full JavaScript source of the worker.\n */\nexport async function spawnAsyncModelRunner(workerSpec: { path: string } | { source: string }): Promise<ModelRunner> {\n if (workerSpec['path']) {\n return spawnAsyncModelRunnerWithWorker(new Worker(workerSpec['path']))\n } else {\n return spawnAsyncModelRunnerWithWorker(BlobWorker.fromText(workerSpec['source']))\n }\n}\n\n/**\n * @hidden For internal use only\n */\nasync function spawnAsyncModelRunnerWithWorker(worker: Worker): Promise<ModelRunner> {\n // Spawn the given Worker that contains the `ModelWorker`\n const modelWorker = await spawn(worker)\n\n // Wait for the worker to initialize the wasm model (in the worker thread)\n const initResult = await modelWorker.initModel()\n const rowLength: number = initResult.rowLength\n let ioBuffer: ArrayBuffer = initResult.ioBuffer\n\n // Use a flag to ensure that only one request is made at a time\n let running = false\n\n // Disallow `runModel` after the runner has been terminated\n let terminated = false\n\n return {\n runModel: async (inputs, outputs) => {\n if (terminated) {\n throw new Error('Async model runner has already been terminated')\n } else if (running) {\n throw new Error('Async model runner only supports one `runModel` call at a time')\n } else {\n running = true\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Capture the current set of input values into the reusable buffer\n const inputsLengthInElements = inputs.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n for (let i = 0; i < inputs.length; i++) {\n inputsArray[i] = inputs[i].get()\n }\n\n // Run the model in the worker. We pass the underlying `ArrayBuffer`\n // instance back to the worker wrapped in a `Transfer` to make it\n // no-copy transferable, and then the worker will return it back\n // to us.\n try {\n ioBuffer = await modelWorker.runModel(Transfer(ioBuffer))\n } finally {\n running = false\n }\n\n // Save the model run time\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n outputs.runTimeInMillis = runTimeBufferArray[0]\n\n // Capture the outputs array by copying the data into the given `Outputs`\n // data structure\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputs.updateFromBuffer(outputsArray, rowLength)\n\n return outputs\n },\n\n terminate: () => {\n if (terminated) {\n return Promise.resolve()\n } else {\n terminated = true\n return Thread.terminate(modelWorker)\n }\n }\n }\n}\n"],"mappings":";AAEA;AAuCA,qCAA4C,YAAyE;AACnH,MAAI,WAAW,SAAS;AACtB,WAAO,gCAAgC,IAAI,OAAO,WAAW,OAAO,CAAC;AAAA,EACvE,OAAO;AACL,WAAO,gCAAgC,WAAW,SAAS,WAAW,SAAS,CAAC;AAAA,EAClF;AACF;AAKA,+CAA+C,QAAsC;AAEnF,QAAM,cAAc,MAAM,MAAM,MAAM;AAGtC,QAAM,aAAa,MAAM,YAAY,UAAU;AAC/C,QAAM,YAAoB,WAAW;AACrC,MAAI,WAAwB,WAAW;AAGvC,MAAI,UAAU;AAGd,MAAI,aAAa;AAEjB,SAAO;AAAA,IACL,UAAU,OAAO,QAAQ,YAAY;AACnC,UAAI,YAAY;AACd,cAAM,IAAI,MAAM,gDAAgD;AAAA,MAClE,WAAW,SAAS;AAClB,cAAM,IAAI,MAAM,gEAAgE;AAAA,MAClF,OAAO;AACL,kBAAU;AAAA,MACZ;AAGA,YAAM,0BAA0B;AAChC,YAAM,uBAAuB,0BAA0B;AAGvD,YAAM,yBAAyB,OAAO;AACtC,YAAM,sBAAsB,yBAAyB;AACrD,YAAM,cAAc,IAAI,aAAa,UAAU,sBAAsB,sBAAsB;AAC3F,eAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK;AACtC,oBAAY,KAAK,OAAO,GAAG,IAAI;AAAA,MACjC;AAMA,UAAI;AACF,mBAAW,MAAM,YAAY,SAAS,SAAS,QAAQ,CAAC;AAAA,MAC1D,UAAE;AACA,kBAAU;AAAA,MACZ;AAGA,YAAM,qBAAqB,IAAI,aAAa,UAAU,GAAG,uBAAuB;AAChF,cAAQ,kBAAkB,mBAAmB;AAI7C,YAAM,uBAAuB,uBAAuB;AACpD,YAAM,eAAe,IAAI,aAAa,UAAU,oBAAoB;AACpE,cAAQ,iBAAiB,cAAc,SAAS;AAEhD,aAAO;AAAA,IACT;AAAA,IAEA,WAAW,MAAM;AACf,UAAI,YAAY;AACd,eAAO,QAAQ,QAAQ;AAAA,MACzB,OAAO;AACL,qBAAa;AACb,eAAO,OAAO,UAAU,WAAW;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,78 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/worker.ts
20
+ var worker_exports = {};
21
+ __export(worker_exports, {
22
+ exposeModelWorker: () => exposeModelWorker
23
+ });
24
+ module.exports = __toCommonJS(worker_exports);
25
+ var import_worker = require("threads/worker");
26
+ var import_runtime = require("@sdeverywhere/runtime");
27
+ var initWasmModel;
28
+ var wasmModel;
29
+ var inputsWasmBuffer;
30
+ var outputsWasmBuffer;
31
+ var modelWorker = {
32
+ async initModel() {
33
+ if (wasmModel) {
34
+ throw new Error("WasmModel was already initialized");
35
+ }
36
+ const result = await initWasmModel();
37
+ wasmModel = result.model;
38
+ inputsWasmBuffer = result.inputsBuffer;
39
+ outputsWasmBuffer = result.outputsBuffer;
40
+ const runTimeLength = 8;
41
+ const inputsLength = inputsWasmBuffer.getArrayView().length;
42
+ const outputsLength = outputsWasmBuffer.getArrayView().length;
43
+ const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength);
44
+ const rowLength = result.endTime - result.startTime + 1;
45
+ const ioBuffer = ioArray.buffer;
46
+ return (0, import_worker.Transfer)({ rowLength, ioBuffer }, [ioBuffer]);
47
+ },
48
+ runModel(ioBuffer) {
49
+ if (!wasmModel) {
50
+ throw new Error("WasmModel must be initialized before running the model in worker");
51
+ }
52
+ const runTimeLengthInElements = 1;
53
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
54
+ const inputsWasmArray = inputsWasmBuffer.getArrayView();
55
+ const inputsLengthInElements = inputsWasmArray.length;
56
+ const inputsLengthInBytes = inputsLengthInElements * 8;
57
+ const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
58
+ inputsWasmArray.set(inputsBufferArray);
59
+ const t0 = (0, import_runtime.perfNow)();
60
+ wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer);
61
+ const elapsed = (0, import_runtime.perfElapsed)(t0);
62
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
63
+ runTimeBufferArray[0] = elapsed;
64
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
65
+ const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
66
+ outputsBufferArray.set(outputsWasmBuffer.getArrayView());
67
+ return (0, import_worker.Transfer)(ioBuffer);
68
+ }
69
+ };
70
+ function exposeModelWorker(init) {
71
+ initWasmModel = init;
72
+ (0, import_worker.expose)(modelWorker);
73
+ }
74
+ // Annotate the CommonJS export names for ESM import in node:
75
+ 0 && (module.exports = {
76
+ exposeModelWorker
77
+ });
78
+ //# sourceMappingURL=worker.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { TransferDescriptor } from 'threads'\nimport { expose, Transfer } from 'threads/worker'\n\nimport type { WasmBuffer, WasmModel, WasmModelInitResult } from '@sdeverywhere/runtime'\nimport { perfElapsed, perfNow } from '@sdeverywhere/runtime'\n\n/** @hidden */\nlet initWasmModel: () => Promise<WasmModelInitResult>\n/** @hidden */\nlet wasmModel: WasmModel\n/** @hidden */\nlet inputsWasmBuffer: WasmBuffer\n/** @hidden */\nlet outputsWasmBuffer: WasmBuffer\n\ninterface InitResult {\n rowLength: number\n ioBuffer: ArrayBuffer\n}\n\n/** @hidden */\nconst modelWorker = {\n async initModel(): Promise<TransferDescriptor<InitResult>> {\n if (wasmModel) {\n throw new Error('WasmModel was already initialized')\n }\n\n // Initialize the wasm model and associated buffers\n const result = await initWasmModel()\n\n // Capture the `WasmModel` instance and `WasmBuffer` instances\n wasmModel = result.model\n inputsWasmBuffer = result.inputsBuffer\n outputsWasmBuffer = result.outputsBuffer\n\n // Create a combined array that will hold a copy of the inputs and outputs\n // wasm buffers; this buffer is no-copy transferable, whereas the wasm ones\n // are not allowed to be transferred\n const runTimeLength = 8\n const inputsLength = inputsWasmBuffer.getArrayView().length\n const outputsLength = outputsWasmBuffer.getArrayView().length\n const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength)\n\n // The row length is the number of elements in each row of the outputs buffer\n const rowLength = result.endTime - result.startTime + 1\n\n // Transfer the underlying buffer to the runner\n const ioBuffer = ioArray.buffer\n return Transfer({ rowLength, ioBuffer }, [ioBuffer])\n },\n\n runModel(ioBuffer: ArrayBuffer): TransferDescriptor<ArrayBuffer> {\n if (!wasmModel) {\n throw new Error('WasmModel must be initialized before running the model in worker')\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Copy the inputs into the wasm inputs buffer\n const inputsWasmArray = inputsWasmBuffer.getArrayView()\n const inputsLengthInElements = inputsWasmArray.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n inputsWasmArray.set(inputsBufferArray)\n\n // Run the model using the wasm buffers\n const t0 = perfNow()\n wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer)\n const elapsed = perfElapsed(t0)\n\n // Write the model run time to the buffer\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n runTimeBufferArray[0] = elapsed\n\n // Copy the outputs from the wasm outputs buffer\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputsBufferArray.set(outputsWasmBuffer.getArrayView())\n\n // Transfer the buffer back to the runner\n return Transfer(ioBuffer)\n }\n}\n\n/**\n * Expose an object in the current worker thread that communicates with the\n * `ModelRunner` instance running in the main thread. The exposed worker\n * object will take care of running the `WasmModel` on the worker thread\n * and sending the outputs back to the main process.\n *\n * @param init The function that initializes the `WasmModel` instance that\n * is used in the worker thread.\n */\nexport function exposeModelWorker(init: () => Promise<WasmModelInitResult>): void {\n // Save the initializer, which will be used when the runner calls `initModel`\n // on the worker\n initWasmModel = init\n\n // Expose the worker implementation to `threads.js`\n expose(modelWorker)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,oBAAiC;AAGjC,qBAAqC;AAGrC,IAAI;AAEJ,IAAI;AAEJ,IAAI;AAEJ,IAAI;AAQJ,IAAM,cAAc;AAAA,EAClB,MAAM,YAAqD;AACzD,QAAI,WAAW;AACb,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AAGA,UAAM,SAAS,MAAM,cAAc;AAGnC,gBAAY,OAAO;AACnB,uBAAmB,OAAO;AAC1B,wBAAoB,OAAO;AAK3B,UAAM,gBAAgB;AACtB,UAAM,eAAe,iBAAiB,aAAa,EAAE;AACrD,UAAM,gBAAgB,kBAAkB,aAAa,EAAE;AACvD,UAAM,UAAU,IAAI,aAAa,gBAAgB,eAAe,aAAa;AAG7E,UAAM,YAAY,OAAO,UAAU,OAAO,YAAY;AAGtD,UAAM,WAAW,QAAQ;AACzB,WAAO,4BAAS,EAAE,WAAW,SAAS,GAAG,CAAC,QAAQ,CAAC;AAAA,EACrD;AAAA,EAEA,SAAS,UAAwD;AAC/D,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,kEAAkE;AAAA,IACpF;AAGA,UAAM,0BAA0B;AAChC,UAAM,uBAAuB,0BAA0B;AAGvD,UAAM,kBAAkB,iBAAiB,aAAa;AACtD,UAAM,yBAAyB,gBAAgB;AAC/C,UAAM,sBAAsB,yBAAyB;AACrD,UAAM,oBAAoB,IAAI,aAAa,UAAU,sBAAsB,sBAAsB;AACjG,oBAAgB,IAAI,iBAAiB;AAGrC,UAAM,KAAK,4BAAQ;AACnB,cAAU,SAAS,kBAAkB,iBAAiB;AACtD,UAAM,UAAU,gCAAY,EAAE;AAG9B,UAAM,qBAAqB,IAAI,aAAa,UAAU,GAAG,uBAAuB;AAChF,uBAAmB,KAAK;AAGxB,UAAM,uBAAuB,uBAAuB;AACpD,UAAM,qBAAqB,IAAI,aAAa,UAAU,oBAAoB;AAC1E,uBAAmB,IAAI,kBAAkB,aAAa,CAAC;AAGvD,WAAO,4BAAS,QAAQ;AAAA,EAC1B;AACF;AAWO,2BAA2B,MAAgD;AAGhF,kBAAgB;AAGhB,4BAAO,WAAW;AACpB;","names":[]}
@@ -0,0 +1,14 @@
1
+ import { WasmModelInitResult } from '@sdeverywhere/runtime';
2
+
3
+ /**
4
+ * Expose an object in the current worker thread that communicates with the
5
+ * `ModelRunner` instance running in the main thread. The exposed worker
6
+ * object will take care of running the `WasmModel` on the worker thread
7
+ * and sending the outputs back to the main process.
8
+ *
9
+ * @param init The function that initializes the `WasmModel` instance that
10
+ * is used in the worker thread.
11
+ */
12
+ declare function exposeModelWorker(init: () => Promise<WasmModelInitResult>): void;
13
+
14
+ export { exposeModelWorker };
package/dist/worker.js ADDED
@@ -0,0 +1,54 @@
1
+ // src/worker.ts
2
+ import { expose, Transfer } from "threads/worker";
3
+ import { perfElapsed, perfNow } from "@sdeverywhere/runtime";
4
+ var initWasmModel;
5
+ var wasmModel;
6
+ var inputsWasmBuffer;
7
+ var outputsWasmBuffer;
8
+ var modelWorker = {
9
+ async initModel() {
10
+ if (wasmModel) {
11
+ throw new Error("WasmModel was already initialized");
12
+ }
13
+ const result = await initWasmModel();
14
+ wasmModel = result.model;
15
+ inputsWasmBuffer = result.inputsBuffer;
16
+ outputsWasmBuffer = result.outputsBuffer;
17
+ const runTimeLength = 8;
18
+ const inputsLength = inputsWasmBuffer.getArrayView().length;
19
+ const outputsLength = outputsWasmBuffer.getArrayView().length;
20
+ const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength);
21
+ const rowLength = result.endTime - result.startTime + 1;
22
+ const ioBuffer = ioArray.buffer;
23
+ return Transfer({ rowLength, ioBuffer }, [ioBuffer]);
24
+ },
25
+ runModel(ioBuffer) {
26
+ if (!wasmModel) {
27
+ throw new Error("WasmModel must be initialized before running the model in worker");
28
+ }
29
+ const runTimeLengthInElements = 1;
30
+ const runTimeLengthInBytes = runTimeLengthInElements * 8;
31
+ const inputsWasmArray = inputsWasmBuffer.getArrayView();
32
+ const inputsLengthInElements = inputsWasmArray.length;
33
+ const inputsLengthInBytes = inputsLengthInElements * 8;
34
+ const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements);
35
+ inputsWasmArray.set(inputsBufferArray);
36
+ const t0 = perfNow();
37
+ wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer);
38
+ const elapsed = perfElapsed(t0);
39
+ const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements);
40
+ runTimeBufferArray[0] = elapsed;
41
+ const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes;
42
+ const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes);
43
+ outputsBufferArray.set(outputsWasmBuffer.getArrayView());
44
+ return Transfer(ioBuffer);
45
+ }
46
+ };
47
+ function exposeModelWorker(init) {
48
+ initWasmModel = init;
49
+ expose(modelWorker);
50
+ }
51
+ export {
52
+ exposeModelWorker
53
+ };
54
+ //# sourceMappingURL=worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/worker.ts"],"sourcesContent":["// Copyright (c) 2020-2022 Climate Interactive / New Venture Fund\n\nimport type { TransferDescriptor } from 'threads'\nimport { expose, Transfer } from 'threads/worker'\n\nimport type { WasmBuffer, WasmModel, WasmModelInitResult } from '@sdeverywhere/runtime'\nimport { perfElapsed, perfNow } from '@sdeverywhere/runtime'\n\n/** @hidden */\nlet initWasmModel: () => Promise<WasmModelInitResult>\n/** @hidden */\nlet wasmModel: WasmModel\n/** @hidden */\nlet inputsWasmBuffer: WasmBuffer\n/** @hidden */\nlet outputsWasmBuffer: WasmBuffer\n\ninterface InitResult {\n rowLength: number\n ioBuffer: ArrayBuffer\n}\n\n/** @hidden */\nconst modelWorker = {\n async initModel(): Promise<TransferDescriptor<InitResult>> {\n if (wasmModel) {\n throw new Error('WasmModel was already initialized')\n }\n\n // Initialize the wasm model and associated buffers\n const result = await initWasmModel()\n\n // Capture the `WasmModel` instance and `WasmBuffer` instances\n wasmModel = result.model\n inputsWasmBuffer = result.inputsBuffer\n outputsWasmBuffer = result.outputsBuffer\n\n // Create a combined array that will hold a copy of the inputs and outputs\n // wasm buffers; this buffer is no-copy transferable, whereas the wasm ones\n // are not allowed to be transferred\n const runTimeLength = 8\n const inputsLength = inputsWasmBuffer.getArrayView().length\n const outputsLength = outputsWasmBuffer.getArrayView().length\n const ioArray = new Float64Array(runTimeLength + inputsLength + outputsLength)\n\n // The row length is the number of elements in each row of the outputs buffer\n const rowLength = result.endTime - result.startTime + 1\n\n // Transfer the underlying buffer to the runner\n const ioBuffer = ioArray.buffer\n return Transfer({ rowLength, ioBuffer }, [ioBuffer])\n },\n\n runModel(ioBuffer: ArrayBuffer): TransferDescriptor<ArrayBuffer> {\n if (!wasmModel) {\n throw new Error('WasmModel must be initialized before running the model in worker')\n }\n\n // The run time is stored in the first 8 bytes\n const runTimeLengthInElements = 1\n const runTimeLengthInBytes = runTimeLengthInElements * 8\n\n // Copy the inputs into the wasm inputs buffer\n const inputsWasmArray = inputsWasmBuffer.getArrayView()\n const inputsLengthInElements = inputsWasmArray.length\n const inputsLengthInBytes = inputsLengthInElements * 8\n const inputsBufferArray = new Float64Array(ioBuffer, runTimeLengthInBytes, inputsLengthInElements)\n inputsWasmArray.set(inputsBufferArray)\n\n // Run the model using the wasm buffers\n const t0 = perfNow()\n wasmModel.runModel(inputsWasmBuffer, outputsWasmBuffer)\n const elapsed = perfElapsed(t0)\n\n // Write the model run time to the buffer\n const runTimeBufferArray = new Float64Array(ioBuffer, 0, runTimeLengthInElements)\n runTimeBufferArray[0] = elapsed\n\n // Copy the outputs from the wasm outputs buffer\n const outputsOffsetInBytes = runTimeLengthInBytes + inputsLengthInBytes\n const outputsBufferArray = new Float64Array(ioBuffer, outputsOffsetInBytes)\n outputsBufferArray.set(outputsWasmBuffer.getArrayView())\n\n // Transfer the buffer back to the runner\n return Transfer(ioBuffer)\n }\n}\n\n/**\n * Expose an object in the current worker thread that communicates with the\n * `ModelRunner` instance running in the main thread. The exposed worker\n * object will take care of running the `WasmModel` on the worker thread\n * and sending the outputs back to the main process.\n *\n * @param init The function that initializes the `WasmModel` instance that\n * is used in the worker thread.\n */\nexport function exposeModelWorker(init: () => Promise<WasmModelInitResult>): void {\n // Save the initializer, which will be used when the runner calls `initModel`\n // on the worker\n initWasmModel = init\n\n // Expose the worker implementation to `threads.js`\n expose(modelWorker)\n}\n"],"mappings":";AAGA;AAGA;AAGA,IAAI;AAEJ,IAAI;AAEJ,IAAI;AAEJ,IAAI;AAQJ,IAAM,cAAc;AAAA,EAClB,MAAM,YAAqD;AACzD,QAAI,WAAW;AACb,YAAM,IAAI,MAAM,mCAAmC;AAAA,IACrD;AAGA,UAAM,SAAS,MAAM,cAAc;AAGnC,gBAAY,OAAO;AACnB,uBAAmB,OAAO;AAC1B,wBAAoB,OAAO;AAK3B,UAAM,gBAAgB;AACtB,UAAM,eAAe,iBAAiB,aAAa,EAAE;AACrD,UAAM,gBAAgB,kBAAkB,aAAa,EAAE;AACvD,UAAM,UAAU,IAAI,aAAa,gBAAgB,eAAe,aAAa;AAG7E,UAAM,YAAY,OAAO,UAAU,OAAO,YAAY;AAGtD,UAAM,WAAW,QAAQ;AACzB,WAAO,SAAS,EAAE,WAAW,SAAS,GAAG,CAAC,QAAQ,CAAC;AAAA,EACrD;AAAA,EAEA,SAAS,UAAwD;AAC/D,QAAI,CAAC,WAAW;AACd,YAAM,IAAI,MAAM,kEAAkE;AAAA,IACpF;AAGA,UAAM,0BAA0B;AAChC,UAAM,uBAAuB,0BAA0B;AAGvD,UAAM,kBAAkB,iBAAiB,aAAa;AACtD,UAAM,yBAAyB,gBAAgB;AAC/C,UAAM,sBAAsB,yBAAyB;AACrD,UAAM,oBAAoB,IAAI,aAAa,UAAU,sBAAsB,sBAAsB;AACjG,oBAAgB,IAAI,iBAAiB;AAGrC,UAAM,KAAK,QAAQ;AACnB,cAAU,SAAS,kBAAkB,iBAAiB;AACtD,UAAM,UAAU,YAAY,EAAE;AAG9B,UAAM,qBAAqB,IAAI,aAAa,UAAU,GAAG,uBAAuB;AAChF,uBAAmB,KAAK;AAGxB,UAAM,uBAAuB,uBAAuB;AACpD,UAAM,qBAAqB,IAAI,aAAa,UAAU,oBAAoB;AAC1E,uBAAmB,IAAI,kBAAkB,aAAa,CAAC;AAGvD,WAAO,SAAS,QAAQ;AAAA,EAC1B;AACF;AAWO,2BAA2B,MAAgD;AAGhF,kBAAgB;AAGhB,SAAO,WAAW;AACpB;","names":[]}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@sdeverywhere/runtime-async",
3
+ "version": "0.1.0",
4
+ "files": [
5
+ "dist/**"
6
+ ],
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./runner": {
18
+ "types": "./dist/runner.d.ts",
19
+ "import": "./dist/runner.js",
20
+ "require": "./dist/runner.cjs"
21
+ },
22
+ "./worker": {
23
+ "types": "./dist/worker.d.ts",
24
+ "import": "./dist/worker.js",
25
+ "require": "./dist/worker.cjs"
26
+ }
27
+ },
28
+ "dependencies": {
29
+ "@sdeverywhere/runtime": "^0.1.0",
30
+ "threads": "1.7.0"
31
+ },
32
+ "author": "Climate Interactive",
33
+ "license": "MIT",
34
+ "homepage": "https://sdeverywhere.org",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/climateinteractive/SDEverywhere.git",
38
+ "directory": "packages/runtime-async"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/climateinteractive/SDEverywhere/issues"
42
+ },
43
+ "scripts": {
44
+ "clean": "rm -rf dist",
45
+ "lint": "eslint src --ext .ts --max-warnings 0",
46
+ "prettier:check": "prettier --check .",
47
+ "prettier:fix": "prettier --write .",
48
+ "precommit": "../../scripts/precommit",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "test:ci": "vitest run",
52
+ "type-check": "tsc --noEmit -p tsconfig-build.json",
53
+ "build": "tsup",
54
+ "docs": "../../scripts/gen-docs.js",
55
+ "ci:build": "run-s clean lint prettier:check type-check build test:ci docs"
56
+ }
57
+ }