Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion LOADING_WASM.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,16 @@ Bundlers don’t emit `.wasm` files by default — they stay inside `node_module

* **Next.js + Webpack:** Add a `.wasm` asset rule or use the copy method.
* **Turbopack:** Only the copy method works for now.
* **Dev mode:** Optionally use a watcher script to auto-copy on rebuilds.
* **Dev mode:** Optionally use a watcher script to auto-copy on rebuilds.

#### 📦 Hosts that can't fetch the file

If the runtime can't fetch or read the `.wasm` file at all (e.g. a custom protocol without fetch support), load the bytes yourself and pass them in:

```ts
import { loadModule } from 'libpg-query';

await loadModule({ wasm: bytes }); // ArrayBuffer, typed array, or a compiled WebAssembly.Module
```

Pass only the `libpg-query.wasm` shipped with the package (or bytes you trust as much): whatever you pass runs as the parser.
44 changes: 42 additions & 2 deletions templates/full/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,25 @@ let failures = 0;
// about 45, a stack overflow can make the next one spin forever.
const MAX_STALE_FAILURES = 10;
let staleFailures = 0;
// Set by loadModule({ wasm }); instances are then created from it instead of the file.
let provided: WasmCompiledModule | undefined;

function createModule(): Promise<any> {
const module = provided;
if (!module) return PgQueryModule();
return new Promise((resolve, reject) => {
PgQueryModule({
instantiateWasm(imports: any, receive: (instance: any) => void) {
WebAssembly.instantiate(module, imports).then(receive, reject);
return {};
},
}).then(resolve, reject);
});
}

function instantiate(): Promise<void> {
if (loading) return loading;
const promise = PgQueryModule().then(
const promise = createModule().then(
(module: WasmModule) => {
wasmModule = module;
stale = false;
Expand Down Expand Up @@ -200,7 +215,32 @@ function ensureLoaded() {
if (!wasmModule) throw new Error("WASM module not initialized. Call `loadModule()` first.");
}

export async function loadModule(): Promise<void> {
type WasmCompiledModule = typeof globalThis extends {
WebAssembly: { Module: new (...args: any[]) => infer M }
} ? M : never;

export interface LoadModuleOptions {
/**
* The contents of `libpg-query.wasm`, or the module compiled from them, for
* hosts where the wrapper cannot read the file itself (a custom protocol
* without fetch support, a bundle that inlines assets). Every later instance,
* including replacements after a failed call, is created from it.
*/
wasm?: ArrayBuffer | ArrayBufferView | WasmCompiledModule;
}

// A rest parameter keeps loadModule.length at 0, so `before(loadModule)` is not
// mistaken for a callback-style hook by test runners.
export async function loadModule(...args: [options?: LoadModuleOptions]): Promise<void> {
const wasm = args[0]?.wasm;
if (wasm !== undefined) {
provided = wasm instanceof WebAssembly.Module
? wasm
: await WebAssembly.compile(wasm as ArrayBuffer | ArrayBufferView);
// An instance loaded from the file, or still loading, is replaced by one from `wasm`.
await loading?.catch(() => {});
stale = true;
}
while (!wasmModule || stale) await instantiate();
}

Expand Down
44 changes: 42 additions & 2 deletions templates/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,25 @@ let failures = 0;
// about 45, a stack overflow can make the next one spin forever.
const MAX_STALE_FAILURES = 10;
let staleFailures = 0;
// Set by loadModule({ wasm }); instances are then created from it instead of the file.
let provided: WasmCompiledModule | undefined;

function createModule(): Promise<any> {
const module = provided;
if (!module) return PgQueryModule();
return new Promise((resolve, reject) => {
PgQueryModule({
instantiateWasm(imports: any, receive: (instance: any) => void) {
WebAssembly.instantiate(module, imports).then(receive, reject);
return {};
},
}).then(resolve, reject);
});
}

function instantiate(): Promise<void> {
if (loading) return loading;
const promise = PgQueryModule().then(
const promise = createModule().then(
(module: any) => {
wasmModule = module;
stale = false;
Expand Down Expand Up @@ -188,7 +203,32 @@ function ensureLoaded() {
if (!wasmModule) throw new Error("WASM module not initialized. Call `loadModule()` first.");
}

export async function loadModule() {
type WasmCompiledModule = typeof globalThis extends {
WebAssembly: { Module: new (...args: any[]) => infer M }
} ? M : never;

export interface LoadModuleOptions {
/**
* The contents of `libpg-query.wasm`, or the module compiled from them, for
* hosts where the wrapper cannot read the file itself (a custom protocol
* without fetch support, a bundle that inlines assets). Every later instance,
* including replacements after a failed call, is created from it.
*/
wasm?: ArrayBuffer | ArrayBufferView | WasmCompiledModule;
}

// A rest parameter keeps loadModule.length at 0, so `before(loadModule)` is not
// mistaken for a callback-style hook by test runners.
export async function loadModule(...args: [options?: LoadModuleOptions]): Promise<void> {
const wasm = args[0]?.wasm;
if (wasm !== undefined) {
provided = wasm instanceof WebAssembly.Module
? wasm
: await WebAssembly.compile(wasm as ArrayBuffer | ArrayBufferView);
// An instance loaded from the file, or still loading, is replaced by one from `wasm`.
await loading?.catch(() => {});
stale = true;
}
while (!wasmModule || stale) await instantiate();
}

Expand Down
12 changes: 12 additions & 0 deletions versions/13/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ const result = parseSync('SELECT * FROM users');

Note: We recommend using async methods as they handle initialization automatically. Use sync methods only when necessary, and always call `loadModule()` first.

### `loadModule({ wasm }): Promise<void>`

For hosts where the wrapper cannot read `libpg-query.wasm` itself (for example a custom protocol without fetch support), pass the file's bytes or a compiled `WebAssembly.Module`. It is compiled once, and every later instance is created from it. Pass only the `libpg-query.wasm` shipped with this package (or bytes you trust as much): whatever you pass runs as the parser.

```typescript
import { loadModule, parseSync } from 'libpg-query';
import wasm from 'libpg-query/wasm/libpg-query.wasm'; // bytes, via your bundler's binary loader

await loadModule({ wasm });
const result = parseSync('SELECT * FROM users');
```

### Type Definitions

```typescript
Expand Down
2 changes: 1 addition & 1 deletion versions/13/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"wasm:rebuild": "pnpm wasm:make rebuild",
"wasm:clean": "pnpm wasm:make clean",
"wasm:clean-cache": "pnpm wasm:make clean-cache",
"test": "node --test test/parsing.test.js test/errors.test.js test/input-encoding.test.js test/wasm-recovery.test.js"
"test": "node --test test/parsing.test.js test/errors.test.js test/input-encoding.test.js test/wasm-recovery.test.js test/load-module.test.js"
},
"author": "Constructive <developers@constructive.io>",
"license": "MIT",
Expand Down
44 changes: 42 additions & 2 deletions versions/13/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,25 @@ let failures = 0;
// about 45, a stack overflow can make the next one spin forever.
const MAX_STALE_FAILURES = 10;
let staleFailures = 0;
// Set by loadModule({ wasm }); instances are then created from it instead of the file.
let provided: WasmCompiledModule | undefined;

function createModule(): Promise<any> {
const module = provided;
if (!module) return PgQueryModule();
return new Promise((resolve, reject) => {
PgQueryModule({
instantiateWasm(imports: any, receive: (instance: any) => void) {
WebAssembly.instantiate(module, imports).then(receive, reject);
return {};
},
}).then(resolve, reject);
});
}

function instantiate(): Promise<void> {
if (loading) return loading;
const promise = PgQueryModule().then(
const promise = createModule().then(
(module: any) => {
wasmModule = module;
stale = false;
Expand Down Expand Up @@ -195,7 +210,32 @@ function ensureLoaded() {
if (!wasmModule) throw new Error("WASM module not initialized. Call `loadModule()` first.");
}

export async function loadModule() {
type WasmCompiledModule = typeof globalThis extends {
WebAssembly: { Module: new (...args: any[]) => infer M }
} ? M : never;

export interface LoadModuleOptions {
/**
* The contents of `libpg-query.wasm`, or the module compiled from them, for
* hosts where the wrapper cannot read the file itself (a custom protocol
* without fetch support, a bundle that inlines assets). Every later instance,
* including replacements after a failed call, is created from it.
*/
wasm?: ArrayBuffer | ArrayBufferView | WasmCompiledModule;
}

// A rest parameter keeps loadModule.length at 0, so `before(loadModule)` is not
// mistaken for a callback-style hook by test runners.
export async function loadModule(...args: [options?: LoadModuleOptions]): Promise<void> {
const wasm = args[0]?.wasm;
if (wasm !== undefined) {
provided = wasm instanceof WebAssembly.Module
? wasm
: await WebAssembly.compile(wasm as ArrayBuffer | ArrayBufferView);
// An instance loaded from the file, or still loading, is replaced by one from `wasm`.
await loading?.catch(() => {});
stale = true;
}
while (!wasmModule || stale) await instantiate();
}

Expand Down
99 changes: 99 additions & 0 deletions versions/13/test/load-module.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');

const WASM = path.join(__dirname, '../wasm/libpg-query.wasm');
const RESULT = 'RESULT ';

// Runs `body` in a fresh process whose copy of the wrapper has no
// libpg-query.wasm next to it, like a host that cannot read its own files.
function withoutWasmFile(body) {
const dir = fs.mkdtempSync(path.join(__dirname, '.no-wasm-'));
try {
for (const file of ['index.cjs', 'libpg-query.js']) {
fs.copyFileSync(path.join(__dirname, '../wasm', file), path.join(dir, file));
}
const script = `
const fs = require('node:fs');
const lib = require(${JSON.stringify(path.join(dir, 'index.cjs'))});
const bytes = fs.readFileSync(${JSON.stringify(WASM)});
const DEEP = 'SELECT 1' + '+1'.repeat(100000);
(async () => { ${body} })().then(
(result) => console.log(${JSON.stringify(RESULT)} + JSON.stringify(result)),
(error) => { console.log(${JSON.stringify(RESULT)} + JSON.stringify({ error: String(error && error.message || error) })); }
);
`;
const run = spawnSync(process.execPath, ['--max-old-space-size=4096', '-e', script], {
encoding: 'utf8',
maxBuffer: 64 * 1024 * 1024,
timeout: 120000,
});
const line = (run.stdout || '').split('\n').reverse().find((l) => l.startsWith(RESULT));
if (!line) {
const how = run.error ? run.error.message : run.signal ? `signal ${run.signal}` : `exit ${run.status}`;
throw new Error(`child process gave no result (${how}):\n${(run.stderr || '').slice(-4000)}`);
}
return JSON.parse(line.slice(RESULT.length));
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}

describe('loadModule({ wasm }) when the wrapper cannot read libpg-query.wasm', { timeout: 300000 }, () => {
it('fails to load without it', () => {
const result = withoutWasmFile(`await lib.loadModule(); return { ok: true };`);
assert.ok(result.error, 'expected loadModule() to reject without the .wasm file');
});

it('loads from the bytes and recovers after a failed call without the file', () => {
const result = withoutWasmFile(`
await lib.loadModule({ wasm: bytes });
const first = lib.parseSync('SELECT 1').stmts.length;
let threw = false;
try { lib.parseSync(DEEP); } catch { threw = true; }
const afterSync = lib.parseSync('SELECT 2').stmts.length;
let threwAsync = false;
try { await lib.parse(DEEP); } catch { threwAsync = true; }
const afterAsync = (await lib.parse('SELECT 3')).stmts.length;
return { first, threw, afterSync, threwAsync, afterAsync };
`);
assert.deepEqual(result, { first: 1, threw: true, afterSync: 1, threwAsync: true, afterAsync: 1 });
});

it('accepts a compiled WebAssembly.Module and a typed array view', () => {
const result = withoutWasmFile(`
await lib.loadModule({ wasm: new WebAssembly.Module(bytes) });
const fromModule = lib.parseSync('SELECT 1').stmts.length;
await lib.loadModule({ wasm: new Uint8Array(bytes) });
return { fromModule, fromView: (await lib.parse('SELECT 1; SELECT 2')).stmts.length };
`);
assert.deepEqual(result, { fromModule: 1, fromView: 2 });
});

it('creates the replacement from the bytes after out-of-memory failures', () => {
const result = withoutWasmFile(`
await lib.loadModule({ wasm: bytes });
const huge = 'SELECT 1;\\n'.repeat(5000000);
const messages = [];
for (let i = 0; i < 3; i++) {
try { lib.parseSync(huge); } catch (e) { messages.push(e.message); }
}
return { messages, after: (await lib.parse('SELECT 1')).stmts.length };
`);
assert.equal(result.messages.length, 3);
assert.equal(result.after, 1);
});

it('rejects input that is not WebAssembly', () => {
const result = withoutWasmFile(`await lib.loadModule({ wasm: new Uint8Array([1, 2, 3]) }); return { ok: true };`);
assert.ok(result.error);
});
});

describe('loadModule() arity', () => {
it('stays 0 so test runners can pass it directly as a hook', () => {
assert.equal(require('../wasm/index.cjs').loadModule.length, 0);
});
});
12 changes: 12 additions & 0 deletions versions/14/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ const result = parseSync('SELECT * FROM users');

Note: We recommend using async methods as they handle initialization automatically. Use sync methods only when necessary, and always call `loadModule()` first.

### `loadModule({ wasm }): Promise<void>`

For hosts where the wrapper cannot read `libpg-query.wasm` itself (for example a custom protocol without fetch support), pass the file's bytes or a compiled `WebAssembly.Module`. It is compiled once, and every later instance is created from it. Pass only the `libpg-query.wasm` shipped with this package (or bytes you trust as much): whatever you pass runs as the parser.

```typescript
import { loadModule, parseSync } from 'libpg-query';
import wasm from 'libpg-query/wasm/libpg-query.wasm'; // bytes, via your bundler's binary loader

await loadModule({ wasm });
const result = parseSync('SELECT * FROM users');
```

### Type Definitions

```typescript
Expand Down
2 changes: 1 addition & 1 deletion versions/14/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"wasm:rebuild": "pnpm wasm:make rebuild",
"wasm:clean": "pnpm wasm:make clean",
"wasm:clean-cache": "pnpm wasm:make clean-cache",
"test": "node --test test/parsing.test.js test/errors.test.js test/input-encoding.test.js test/wasm-recovery.test.js"
"test": "node --test test/parsing.test.js test/errors.test.js test/input-encoding.test.js test/wasm-recovery.test.js test/load-module.test.js"
},
"author": "Constructive <developers@constructive.io>",
"license": "MIT",
Expand Down
Loading
Loading