Skip to content

Commit 4b9549c

Browse files
yasumorishimaclaudenikosxenakis
authored
fix: prevent WASM double-initialization with concurrent plugin instances (#125)
## Summary - Fix race condition in `wasmInit()` that causes `buildStart` to hang when multiple `icpBindgen()` plugin instances run concurrently - Replace boolean `initialized` flag with a shared Promise pattern so concurrent callers await the same initialization - Reset the cached promise on failure so subsequent calls can retry ## Root Cause When multiple `icpBindgen()` plugin instances call `buildStart` in parallel, each triggers `wasmInit()`. The boolean `initialized` flag is only set **after** `await init()` completes, so concurrent callers all pass the `if (initialized)` check and each call `init()` independently, causing WASM double-initialization and a hang: ``` (vite-plugin-icp-bindgen) buildStart ← all hang (vite-plugin-icp-bindgen) buildStart (vite-plugin-icp-bindgen) buildStart ``` ## Fix Store the `init()` Promise so all concurrent callers share and await the same initialization: ```ts let initPromise: Promise<void> | undefined; export async function wasmInit(...args: Parameters<typeof init>) { if (!initPromise) { initPromise = init(...args).then( () => {}, (error: unknown) => { initPromise = undefined; throw error; }, ); } return initPromise; } ``` The error handler resets `initPromise` on failure so callers can retry if WASM loading fails. ## Test plan - [x] Added `tests/wasm-init.test.ts` — 5 concurrent `wasmInit` calls resolve without hanging - [x] All existing tests pass (20/21; 1 pre-existing CLI test requires `vite build` output) - [x] Biome lint clean - [x] CodeQL clean Closes #124 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Nikolaos Xenakis <25032940+nikosxenakis@users.noreply.github.com>
1 parent e0f87d1 commit 4b9549c

File tree

2 files changed

+34
-6
lines changed

2 files changed

+34
-6
lines changed

src/core/generate/rs.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import type {
55
} from './rs/dist/icp-js-bindgen.d.ts';
66
import init, { generate, start } from './rs/dist/icp-js-bindgen.js';
77

8-
let initialized = false;
8+
let initPromise: Promise<void> | undefined;
99

1010
export async function wasmInit(...args: Parameters<typeof init>) {
11-
if (initialized) {
12-
return;
11+
if (!initPromise) {
12+
initPromise = init(...args).then(
13+
() => {},
14+
(error: unknown) => {
15+
initPromise = undefined;
16+
throw error;
17+
},
18+
);
1319
}
14-
15-
await init(...args);
16-
initialized = true;
20+
return initPromise;
1721
}
1822

1923
export const wasmStart = start;

tests/wasm-init.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { wasmInit } from '../src/core/generate/rs.ts';
3+
import { getWasm } from './utils/wasm.ts';
4+
5+
describe('wasmInit', () => {
6+
it('should handle concurrent calls without hanging', async () => {
7+
const { wasm } = await getWasm();
8+
9+
// Simulate multiple concurrent wasmInit calls, as would happen when
10+
// multiple icpBindgen() plugin instances trigger buildStart in parallel.
11+
const results = await Promise.all([
12+
wasmInit({ module_or_path: wasm }),
13+
wasmInit({ module_or_path: wasm }),
14+
wasmInit({ module_or_path: wasm }),
15+
wasmInit({ module_or_path: wasm }),
16+
wasmInit({ module_or_path: wasm }),
17+
]);
18+
19+
// All calls should resolve (not hang) and return undefined.
20+
for (const result of results) {
21+
expect(result).toBeUndefined();
22+
}
23+
});
24+
});

0 commit comments

Comments
 (0)