Repository navigation
Conversation
Hosts that cannot fetch their own files (an Electron custom protocol registered without supportFetchAPI, as for Beekeeper Studio plugins) had no way to hand libpg-query its WASM: the module started loading at import and loadModule() took no options. - Nothing loads at import; the first loadModule() or async call starts the load, so its options apply. - loadModule(options?) accepts instantiateWasm, adapted to Emscripten's callback form and raced against the hook's failure, and locateFile, forwarded unchanged. A call that sets a hook after the first load started rejects with "initialization already started". - The options are reused for every load, including retries and the fresh instance loaded after a WASM exception; an instance the hook already returned is rejected. - loadModule keeps arity 0 and a zero-argument overload, so it can still be passed as a callback; the hook types are structural, so the declarations need no dom lib. - Docs in the v18 README, LOADING_WASM.md and the @pgsql/parser README; tests in versions/18/test/load-module.test.js.
Gldywn
force-pushed
the
feat/load-module-options
branch
from
October 29, 2026 09:15
dec180f to
265abd2
Compare
This was referenced Oct 29, 2026
|
Thanks @Gldywn, and thanks for the clear Beekeeper write-up. We reproduced the fetch-less await loadModule({ wasm }); // ArrayBuffer, typed array, or WebAssembly.ModuleEvery later instance is built from it, including the one created after a failed parse, so nothing fetches the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
libpg-querycan only get its WASM through Emscripten's default loader, which fetcheslibpg-query.wasm(or reads it from disk under Node). A host that cannot fetch its own files has no way in: the wrapper starts that load at import time, andloadModule()takes no options.We hit this in Gatekeeper, a Beekeeper Studio plugin that parses SQL with libpg-query. Plugin pages are served from Electron's custom
plugin://scheme, which Beekeeper registers withoutsupportFetchAPI, sofetchon the plugin's own files fails (beekeeper-studio/beekeeper-studio#4864, item 5). The generated factory already readsModule.instantiateWasmandModule.locateFile(it does not readModule.wasmBinary), and instantiating the module from bytes bundled in the page's JS works in Beekeeper. The only thing missing is a way to give the factory that hook.This PR adds it, for v18 (the full-API template):
loadModule(options?)acceptsinstantiateWasm(imports) => Promise<instance>andlocateFile(path, scriptDirectory).locateFileis forwarded to Emscripten unchanged. An unknown key next to a hook, or a hook that is not a function, rejects with aTypeError.loadModule()or async call (parse,scan, ...) starts the load, so its options apply to it.initialization already started, and every other call joins. Late options are never silently ignored and never restart the load.importsit receives each time. An instance it already returned is rejected. A cached instance would bring back the one the exception corrupted, with its imports still bound to the previous module; the result looks recovered but returns empty results.instantiateWasmpath has no failure callback, so the wrapper races the factory against the hook. A hook that rejects, throws, or resolves withoutexportsrejectsloadModule()instead of leaving it pending forever.loadModulekeeps arity 0 and a zero-argument overload, sobefore(loadModule),beforeAll(loadModule)andpromise.then(loadModule)work and type-check as before. An argument that sets neither hook (a test runner's context, a promise value,{}) counts as no options.wasm/index.d.tsstill compiles without thedomlib.Behaviour change. Because nothing loads at import, a sync call (
parseSync,scanSync, ...) made without ever awaitingloadModule()or an async method now always throwsWASM module not initialized. Call loadModule() first.Before, such a call worked once the import-time load happened to finish.parseSyncexample and the@pgsql/parserdocs for its version exports (v18,@pgsql/parser/v18) both used that pattern. Both are fixed here.Parserclass, which awaitsloadModule().Versions 13 to 17 (slim template) are unchanged.
Evidence
Node 24.22.1, TypeScript 5.8.3, run against the published 18.1.5
libpg-query.{js,wasm}. The WASM was not rebuilt here; CI rebuilds it.The 162 tests are the 92 existing ones,
input-encoding(11),wasm-recovery(40, including itsnode -ecases) and the newload-module(19).main's wrapper: 14 of the 19 new tests fail. The 5 that pass are the compatibility cases: concurrent calls,parse()withoutloadModule(), arguments without a hook, andbefore(loadModule)in two suites.parse()throws on deeply nested SQL, the rebuild goes through the sameinstantiateWasmhook (2 hook calls, 2 factory calls). A hook that returns its cached instance is refused, instead of leaving a module that returns""fromfingerprintSync('SELECT 1').locateFiletest fail.wasm-recoverycases fail, because they require a failing sync call to start its fresh instance at once.tsc --noEmit,strictandskipLibCheck: false, on small consumers ofwasm/index.d.ts.lib: ["es2022"]andtypes: [], it compiles.dom, hooks typed with the realWebAssembly.ImportsandWebAssembly.Instancecompile, and so does the README example; the{ instance, module }pair is a type error.@types/node,before(loadModule)and.then(loadModule)compile.pnpm test:mutations. By reading, all of its patterns still match the compiledindex.cjs.