Skip to content

Commit 4f5abd8

Browse files
committed
Improve fend-web worker/wasm integration
1 parent 0299e07 commit 4f5abd8

File tree

7 files changed

+112
-46
lines changed

7 files changed

+112
-46
lines changed

telegram-bot/build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ cd "$(dirname "$0")"
55
rm -rfv ../wasm/pkg
66
(cd ../wasm && wasm-pack build)
77

8-
npm install
8+
npm ci
99
npm exec tsc
1010
node --experimental-strip-types esbuild.ts
1111

web/build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ if ! type magick &>/dev/null; then
88
alias magick=convert
99
fi
1010

11-
rm -rf ../wasm/pkg-fend-web
12-
(cd ../wasm && wasm-pack build --target web --out-dir pkg-fend-web)
11+
rm -rf ../wasm/pkg
12+
(cd ../wasm && wasm-pack build)
1313

1414
npm ci
1515
npm run lint

web/package-lock.json

Lines changed: 16 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"lint": "eslint ."
1313
},
1414
"dependencies": {
15-
"fend-wasm": "file:../wasm/pkg-fend-web",
15+
"fend-wasm": "file:../wasm/pkg",
1616
"react": "^19.0.0",
1717
"react-dom": "^19.0.0",
1818
"react-svg-spinners": "^0.3.1"
@@ -32,12 +32,16 @@
3232
"prettier": "^3.4.2",
3333
"typescript": "^5.7.2",
3434
"typescript-eslint": "^8.17.0",
35-
"vite": "^6.0.3"
35+
"vite": "^6.0.3",
36+
"vite-plugin-wasm": "^3.3.0"
3637
},
3738
"overrides": {
3839
"react-svg-spinners": {
3940
"react": "$react",
4041
"react-dom": "$react-dom"
42+
},
43+
"vite-plugin-wasm": {
44+
"vite": "$vite"
4145
}
4246
},
4347
"repository": {

web/src/lib/fend.ts

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,39 +3,76 @@ import type { FendArgs, FendResult } from './worker';
33
import MyWorker from './worker?worker';
44

55
let exchangeRateCache: Map<string, number> | null = null;
6-
type WorkerCache = { worker: Worker; active: boolean; resolve: (fr: FendResult) => void };
7-
let workerCache: WorkerCache | null = null;
6+
type State = 'new' | 'ready' | 'busy';
7+
class WorkerCache {
8+
worker!: Worker;
9+
state!: State;
10+
initialisedPromise!: Promise<void>;
11+
resolveDone?: (r: FendResult) => void;
12+
rejectError?: (e: Error) => void;
13+
14+
init() {
15+
this.state = 'new';
16+
this.worker = new MyWorker({
17+
name: 'fend worker',
18+
});
19+
let resolveInitialised: () => void;
20+
this.initialisedPromise = new Promise<void>(resolve => {
21+
resolveInitialised = resolve;
22+
});
23+
this.worker.onmessage = (e: MessageEvent<FendResult | 'ready'>) => {
24+
this.state = 'ready';
25+
if (e.data === 'ready') {
26+
resolveInitialised();
27+
} else {
28+
this.resolveDone?.(e.data);
29+
}
30+
};
31+
this.worker.onerror = e => {
32+
this.state = 'ready';
33+
this.rejectError?.(new Error(e.message, { cause: e }));
34+
};
35+
this.worker.onmessageerror = e => {
36+
this.state = 'ready';
37+
this.rejectError?.(new Error('received messageerror event', { cause: e }));
38+
};
39+
}
40+
41+
constructor() {
42+
this.init();
43+
}
44+
45+
async query(args: FendArgs) {
46+
if (this.state === 'new') {
47+
await this.initialisedPromise;
48+
}
49+
if (this.state === 'busy') {
50+
console.log('terminating existing worker');
51+
this.worker.terminate();
52+
this.resolveDone?.({ ok: false, message: 'cancelled' });
53+
this.init();
54+
await this.initialisedPromise;
55+
}
56+
if (this.state !== 'ready') {
57+
throw new Error('unexpected worker state: ' + this.state);
58+
}
59+
const p = new Promise<FendResult>((resolve, reject) => {
60+
this.resolveDone = resolve;
61+
this.rejectError = reject;
62+
});
63+
this.state = 'busy';
64+
this.worker.postMessage(args);
65+
return await p;
66+
}
67+
}
68+
const workerCache = new WorkerCache();
869

970
export async function fend(input: string, timeout: number, variables: string): Promise<FendResult> {
1071
try {
1172
const currencyData = exchangeRateCache || (await getExchangeRates());
1273
exchangeRateCache = currencyData;
13-
return await new Promise<FendResult>((resolve, reject) => {
14-
const args: FendArgs = { input, timeout, variables, currencyData };
15-
const w = workerCache || { worker: new MyWorker(), active: false, resolve };
16-
if (w.active) {
17-
w.worker.terminate();
18-
w.resolve({ ok: false, message: 'cancelled' });
19-
w.worker = new MyWorker();
20-
w.active = false;
21-
}
22-
w.resolve = resolve;
23-
w.worker.onmessage = (e: MessageEvent<FendResult>) => {
24-
if (workerCache) {
25-
workerCache.active = false;
26-
}
27-
resolve(e.data);
28-
};
29-
w.worker.onerror = e => {
30-
if (workerCache) {
31-
workerCache.active = false;
32-
}
33-
reject(new Error(e.message));
34-
};
35-
workerCache = w;
36-
w.active = true;
37-
w.worker.postMessage(args);
38-
});
74+
const args: FendArgs = { input, timeout, variables, currencyData };
75+
return await workerCache.query(args);
3976
} catch (e) {
4077
console.error(e);
4178
alert('Failed to initialise WebAssembly');

web/src/lib/worker.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { evaluateFendWithVariablesJson, default as initWasm, initialiseWithHandlers } from 'fend-wasm';
1+
import { evaluateFendWithVariablesJson, initialiseWithHandlers } from 'fend-wasm';
22

33
export type FendArgs = {
44
input: string;
@@ -9,10 +9,17 @@ export type FendArgs = {
99

1010
export type FendResult = { ok: true; result: string; variables: string } | { ok: false; message: string };
1111

12-
const eventListener = async ({ data }: MessageEvent<FendArgs>) => {
13-
await initWasm();
14-
initialiseWithHandlers(data.currencyData);
15-
const result = JSON.parse(evaluateFendWithVariablesJson(data.input, data.timeout, data.variables)) as FendResult;
16-
postMessage(result);
17-
};
18-
self.addEventListener('message', (ev: MessageEvent<FendArgs>) => void eventListener(ev));
12+
function eventListener({ data }: MessageEvent<FendArgs>) {
13+
try {
14+
initialiseWithHandlers(data.currencyData);
15+
const result = JSON.parse(evaluateFendWithVariablesJson(data.input, data.timeout, data.variables)) as FendResult;
16+
postMessage(result);
17+
} catch (e: unknown) {
18+
console.error(e);
19+
throw e;
20+
}
21+
}
22+
self.addEventListener('message', (ev: MessageEvent<FendArgs>) => {
23+
eventListener(ev);
24+
});
25+
postMessage('ready');

web/vite.config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolve } from 'node:path';
22
import react from '@vitejs/plugin-react';
3+
import wasm from 'vite-plugin-wasm';
34
import { defineConfig, searchForWorkspaceRoot } from 'vite';
45

56
const ReactCompilerConfig = {};
@@ -17,7 +18,13 @@ export default defineConfig({
1718
sourcemap: true,
1819
target: 'esnext',
1920
},
21+
worker: {
22+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
23+
plugins: () => [wasm()],
24+
format: 'es',
25+
},
2026
plugins: [
27+
wasm(),
2128
react({
2229
babel: {
2330
plugins: [['babel-plugin-react-compiler', ReactCompilerConfig]],

0 commit comments

Comments
 (0)