@sqldoc/atlas 0.0.1

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.
@@ -0,0 +1,175 @@
1
+ /**
2
+ * WASI module instantiation with custom atlas_sql host function import.
3
+ *
4
+ * Includes workarounds for Bun's WASI bugs:
5
+ * 1. FD_MAP ignores stdin/stdout/stderr constructor options
6
+ * 2. random_get returns byte count instead of errno 0
7
+ * 3. proc_exit calls process.exit() instead of throwing
8
+ */
9
+
10
+ import * as crypto from 'node:crypto'
11
+ import * as fs from 'node:fs'
12
+ import * as os from 'node:os'
13
+ import * as path from 'node:path'
14
+ import { WASI } from 'node:wasi'
15
+
16
+ const isBun = typeof (globalThis as any).Bun !== 'undefined'
17
+
18
+ /** Sentinel thrown by Bun proc_exit workaround */
19
+ class WASIExitError {
20
+ code: number
21
+ constructor(code: number) {
22
+ this.code = code
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Get WASI imports compatible with both Node.js and Bun.
28
+ */
29
+ export function getWasiImports(wasi: WASI): Record<string, any> {
30
+ if (typeof (wasi as any).getImportObject === 'function') {
31
+ return (wasi as any).getImportObject()
32
+ }
33
+ if ((wasi as any).wasiImport) {
34
+ return { wasi_snapshot_preview1: (wasi as any).wasiImport }
35
+ }
36
+ throw new Error('Cannot get WASI imports: neither getImportObject() nor wasiImport available')
37
+ }
38
+
39
+ export interface WasiRunOptions {
40
+ stdinData: string
41
+ wasmPath: string
42
+ compiledModule?: WebAssembly.Module
43
+ atlasSqlFn: (reqPtr: number, reqLen: number, respPtr: number, respCap: number) => bigint | number
44
+ onInstance?: (instance: WebAssembly.Instance) => void
45
+ }
46
+
47
+ export interface WasiRunResult {
48
+ stdout: string
49
+ module: WebAssembly.Module
50
+ }
51
+
52
+ export async function runWasi(options: WasiRunOptions): Promise<WasiRunResult> {
53
+ const { stdinData, wasmPath, atlasSqlFn, onInstance } = options
54
+ const uid = crypto.randomUUID()
55
+ const tmpDir = os.tmpdir()
56
+ const stdinPath = path.join(tmpDir, `atlas-stdin-${uid}.json`)
57
+ const stdoutPath = path.join(tmpDir, `atlas-stdout-${uid}.json`)
58
+
59
+ let stdinFd: number | undefined
60
+ let stdoutFd: number | undefined
61
+
62
+ try {
63
+ fs.writeFileSync(stdinPath, stdinData)
64
+ fs.writeFileSync(stdoutPath, '')
65
+
66
+ stdinFd = fs.openSync(stdinPath, 'r')
67
+ stdoutFd = fs.openSync(stdoutPath, 'w')
68
+
69
+ const wasi = new WASI({
70
+ version: 'preview1',
71
+ stdin: stdinFd,
72
+ stdout: stdoutFd,
73
+ returnOnExit: true,
74
+ })
75
+
76
+ // Bun bug #1: FD_MAP ignores constructor options
77
+ if (isBun) {
78
+ const fdMap: Map<number, any> = (wasi as any).FD_MAP
79
+ if (fdMap) {
80
+ const fd0 = fdMap.get(0)
81
+ if (fd0) fd0.real = stdinFd
82
+ const fd1 = fdMap.get(1)
83
+ if (fd1) fd1.real = stdoutFd
84
+ const fd2 = fdMap.get(2)
85
+ if (fd2) fd2.real = 2
86
+ }
87
+ }
88
+
89
+ // Compile module (or reuse cached)
90
+ let wasmModule = options.compiledModule
91
+ if (!wasmModule) {
92
+ const wasmBytes = fs.readFileSync(wasmPath)
93
+ wasmModule = await WebAssembly.compile(wasmBytes)
94
+ }
95
+
96
+ // Build imports
97
+ const wasiImports = getWasiImports(wasi)
98
+ let instanceRef: WebAssembly.Instance | undefined
99
+
100
+ if (isBun && wasiImports.wasi_snapshot_preview1) {
101
+ // Bun bug #2: random_get returns byte count instead of errno 0
102
+ wasiImports.wasi_snapshot_preview1.random_get = (bufPtr: number, bufLen: number) => {
103
+ if (instanceRef) {
104
+ const mem = instanceRef.exports.memory as WebAssembly.Memory
105
+ const view = new Uint8Array(mem.buffer, bufPtr, bufLen)
106
+ crypto.getRandomValues(view)
107
+ }
108
+ return 0
109
+ }
110
+
111
+ // Bun bug #3: proc_exit calls process.exit()
112
+ wasiImports.wasi_snapshot_preview1.proc_exit = (code: number) => {
113
+ throw new WASIExitError(code)
114
+ }
115
+ }
116
+
117
+ const imports = {
118
+ ...wasiImports,
119
+ env: { atlas_sql: atlasSqlFn },
120
+ }
121
+
122
+ // Instantiate
123
+ const instance = await WebAssembly.instantiate(wasmModule, imports)
124
+ instanceRef = instance
125
+
126
+ if (isBun && typeof (wasi as any).setMemory === 'function') {
127
+ ;(wasi as any).setMemory(instance.exports.memory)
128
+ }
129
+
130
+ if (onInstance) {
131
+ onInstance(instance)
132
+ }
133
+
134
+ // Run
135
+ try {
136
+ if (isBun) {
137
+ ;(instance.exports._start as Function)()
138
+ } else {
139
+ wasi.start(instance)
140
+ }
141
+ } catch (err: unknown) {
142
+ if (err instanceof WASIExitError) {
143
+ // Bun sentinel — non-zero exit is fine, we read stdout for error JSON
144
+ } else {
145
+ // Node: returnOnExit throws on non-zero exit code
146
+ const isExitError = err instanceof Error && ('code' in err || err.message.includes('exit'))
147
+ if (!isExitError) throw err
148
+ }
149
+ }
150
+
151
+ // Close fds before reading stdout
152
+ fs.closeSync(stdinFd)
153
+ stdinFd = undefined
154
+ fs.closeSync(stdoutFd)
155
+ stdoutFd = undefined
156
+
157
+ const stdout = fs.readFileSync(stdoutPath, 'utf-8')
158
+ return { stdout, module: wasmModule }
159
+ } finally {
160
+ if (stdinFd !== undefined)
161
+ try {
162
+ fs.closeSync(stdinFd)
163
+ } catch {}
164
+ if (stdoutFd !== undefined)
165
+ try {
166
+ fs.closeSync(stdoutFd)
167
+ } catch {}
168
+ try {
169
+ fs.unlinkSync(stdinPath)
170
+ } catch {}
171
+ try {
172
+ fs.unlinkSync(stdoutPath)
173
+ } catch {}
174
+ }
175
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Worker thread entry point for Atlas WASI execution.
3
+ *
4
+ * This file runs inside a node:worker_threads Worker. It:
5
+ * 1. Receives initialization data via workerData
6
+ * 2. Creates the atlas_sql callback using SharedArrayBuffer + Atomics
7
+ * 3. Runs the WASI module with the stdin data
8
+ * 4. Posts the stdout result back to the parent
9
+ *
10
+ * The atlas_sql callback bridges synchronous WASM calls to the main thread's
11
+ * async database adapter using Atomics.wait (blocking on the worker thread).
12
+ */
13
+
14
+ import { parentPort, workerData } from 'node:worker_threads'
15
+ import { type BridgeBuffers, bridgeRequest, SIGNAL_DONE } from './bridge.ts'
16
+ import { runWasi } from './wasi-host.ts'
17
+
18
+ interface WorkerInit {
19
+ wasmPath: string
20
+ controlBuffer: SharedArrayBuffer
21
+ dataBuffer: SharedArrayBuffer
22
+ stdinData: string
23
+ }
24
+
25
+ const { wasmPath, controlBuffer, dataBuffer, stdinData } = workerData as WorkerInit
26
+
27
+ const buffers: BridgeBuffers = {
28
+ control: controlBuffer,
29
+ data: dataBuffer,
30
+ }
31
+
32
+ // WASM instance memory -- set via onInstance before wasi.start()
33
+ let wasmMemory: WebAssembly.Memory | null = null
34
+
35
+ /**
36
+ * The atlas_sql host function that bridges sync WASM calls
37
+ * to the main thread via SharedArrayBuffer + Atomics.
38
+ *
39
+ * Signature matches Go's //go:wasmimport env atlas_sql:
40
+ * (reqPtr, reqLen, respPtr, respCap) => int64
41
+ *
42
+ * Negative return = buffer too small (abs value = needed size).
43
+ * Positive return = actual response length written.
44
+ */
45
+ function atlasSql(reqPtr: number, reqLen: number, respPtr: number, respCap: number): bigint {
46
+ if (!wasmMemory) {
47
+ throw new Error('WASM memory not yet available in atlas_sql callback')
48
+ }
49
+
50
+ // 1. Read request JSON from WASM memory
51
+ const reqBytes = new Uint8Array(wasmMemory.buffer, reqPtr, reqLen)
52
+ const requestJson = new TextDecoder().decode(reqBytes.slice())
53
+
54
+ // 2. Send request to main thread via bridge and block until response
55
+ const responseJson = bridgeRequest(buffers, requestJson)
56
+
57
+ // 3. Encode response and check capacity
58
+ const respEncoded = new TextEncoder().encode(responseJson)
59
+
60
+ if (respEncoded.byteLength > respCap) {
61
+ return BigInt(-respEncoded.byteLength)
62
+ }
63
+
64
+ // 4. Copy response into WASM memory
65
+ const wasmResp = new Uint8Array(wasmMemory.buffer, respPtr, respCap)
66
+ wasmResp.set(respEncoded)
67
+
68
+ return BigInt(respEncoded.byteLength)
69
+ }
70
+
71
+ async function main(): Promise<void> {
72
+ if (!parentPort) {
73
+ throw new Error('worker.ts must run inside a worker thread')
74
+ }
75
+
76
+ try {
77
+ const result = await runWasi({
78
+ stdinData,
79
+ wasmPath,
80
+ atlasSqlFn: atlasSql,
81
+ onInstance(instance) {
82
+ // Capture WASM memory before execution starts.
83
+ // atlas_sql will use this to read requests from / write responses to WASM memory.
84
+ wasmMemory = instance.exports.memory as WebAssembly.Memory
85
+ },
86
+ })
87
+
88
+ // Signal main thread that WASI execution is done
89
+ const control = new Int32Array(buffers.control)
90
+ Atomics.store(control, 0, SIGNAL_DONE)
91
+ Atomics.notify(control, 0)
92
+
93
+ // Post result back
94
+ parentPort.postMessage({ type: 'result', stdout: result.stdout })
95
+ } catch (err: unknown) {
96
+ // Signal done even on error
97
+ const control = new Int32Array(buffers.control)
98
+ Atomics.store(control, 0, SIGNAL_DONE)
99
+ Atomics.notify(control, 0)
100
+
101
+ const message = err instanceof Error ? err.message : String(err)
102
+ parentPort.postMessage({ type: 'error', error: message })
103
+ }
104
+ }
105
+
106
+ main()
Binary file