@sqldoc/db 0.0.3
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.
- package/package.json +41 -0
- package/src/bridge.ts +152 -0
- package/src/db/mysql-docker.ts +54 -0
- package/src/db/mysql.ts +44 -0
- package/src/db/pglite.ts +77 -0
- package/src/db/postgres-docker.ts +91 -0
- package/src/db/postgres.ts +44 -0
- package/src/db/sqlite.ts +104 -0
- package/src/db/types.ts +26 -0
- package/src/extensions.ts +116 -0
- package/src/index.ts +122 -0
- package/src/runner.ts +284 -0
- package/src/types.ts +308 -0
- package/src/wasi-host.ts +175 -0
- package/src/worker.ts +106 -0
- package/wasm/atlas.wasm +0 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// ── Atlas WASI command/result types ─────────────────────────────────
|
|
2
|
+
// Hand-crafted from atlas/cmd/atlas-wasi/marshal.go (cycle-free flat types).
|
|
3
|
+
// Schema fields use lowercase json tags from the flat marshaler.
|
|
4
|
+
// Attr variants (Tag, Comment, Check) use PascalCase from map[string]string.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A known rename (from @docs.previously tags) to pass to Atlas before diffing.
|
|
8
|
+
*/
|
|
9
|
+
export interface AtlasRename {
|
|
10
|
+
type: 'column' | 'table'
|
|
11
|
+
table: string
|
|
12
|
+
oldName: string
|
|
13
|
+
newName: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A potential rename detected by Atlas during diff (drop+add pair with same type).
|
|
18
|
+
*/
|
|
19
|
+
export interface AtlasRenameCandidate {
|
|
20
|
+
type: 'column' | 'table'
|
|
21
|
+
table: string
|
|
22
|
+
oldName: string
|
|
23
|
+
newName: string
|
|
24
|
+
colType?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Command sent to Atlas WASI module via stdin.
|
|
29
|
+
*/
|
|
30
|
+
export interface AtlasCommand {
|
|
31
|
+
type: 'inspect' | 'diff' | 'apply'
|
|
32
|
+
dialect: 'postgres' | 'mysql' | 'sqlite'
|
|
33
|
+
schema?: string
|
|
34
|
+
files?: string[]
|
|
35
|
+
fileNames?: string[]
|
|
36
|
+
from?: string[]
|
|
37
|
+
to?: string[]
|
|
38
|
+
fromConnection?: string
|
|
39
|
+
toConnection?: string
|
|
40
|
+
renames?: AtlasRename[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A structured schema change description returned from Atlas diff.
|
|
45
|
+
*/
|
|
46
|
+
export interface AtlasChange {
|
|
47
|
+
type:
|
|
48
|
+
| 'add_table'
|
|
49
|
+
| 'drop_table'
|
|
50
|
+
| 'rename_table'
|
|
51
|
+
| 'add_column'
|
|
52
|
+
| 'drop_column'
|
|
53
|
+
| 'rename_column'
|
|
54
|
+
| 'modify_column'
|
|
55
|
+
| 'add_index'
|
|
56
|
+
| 'drop_index'
|
|
57
|
+
| 'add_view'
|
|
58
|
+
| 'drop_view'
|
|
59
|
+
| 'add_function'
|
|
60
|
+
| 'drop_function'
|
|
61
|
+
table: string
|
|
62
|
+
name?: string
|
|
63
|
+
detail?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Result returned from Atlas WASI module via stdout.
|
|
68
|
+
* Top-level fields use lowercase json tags.
|
|
69
|
+
*/
|
|
70
|
+
export interface AtlasResult {
|
|
71
|
+
schema?: AtlasRealm
|
|
72
|
+
statements?: string[]
|
|
73
|
+
changes?: AtlasChange[]
|
|
74
|
+
renameCandidates?: AtlasRenameCandidate[]
|
|
75
|
+
error?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Schema types — lowercase (json tags in marshal.go flat types) ───
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A Realm describes a domain of schema resources (physical database instance).
|
|
82
|
+
* Maps to flatRealm in marshal.go.
|
|
83
|
+
*/
|
|
84
|
+
export interface AtlasRealm {
|
|
85
|
+
schemas: AtlasSchema[]
|
|
86
|
+
attrs?: AtlasAttr[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A Schema describes a named database schema (e.g. "public").
|
|
91
|
+
* Maps to flatSchema in marshal.go.
|
|
92
|
+
*/
|
|
93
|
+
export interface AtlasSchema {
|
|
94
|
+
name: string
|
|
95
|
+
tables?: AtlasTable[]
|
|
96
|
+
views?: AtlasView[]
|
|
97
|
+
funcs?: AtlasFunc[]
|
|
98
|
+
procs?: AtlasProc[]
|
|
99
|
+
attrs?: AtlasAttr[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A Table represents a table definition.
|
|
104
|
+
* Maps to flatTable in marshal.go.
|
|
105
|
+
*/
|
|
106
|
+
export interface AtlasTable {
|
|
107
|
+
name: string
|
|
108
|
+
columns?: AtlasColumn[]
|
|
109
|
+
indexes?: AtlasIndex[]
|
|
110
|
+
primary_key?: AtlasIndex
|
|
111
|
+
foreign_keys?: AtlasForeignKey[]
|
|
112
|
+
attrs?: AtlasAttr[]
|
|
113
|
+
triggers?: AtlasTrigger[]
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A Column represents a column definition.
|
|
118
|
+
* Maps to flatColumn in marshal.go.
|
|
119
|
+
*/
|
|
120
|
+
export interface AtlasColumn {
|
|
121
|
+
name: string
|
|
122
|
+
type?: AtlasColumnType
|
|
123
|
+
default?: AtlasExpr
|
|
124
|
+
attrs?: AtlasAttr[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* ColumnType represents a column type.
|
|
129
|
+
* Maps to flatColumnType in marshal.go.
|
|
130
|
+
* The `T` field is the type name (e.g., "bigint", "text").
|
|
131
|
+
*/
|
|
132
|
+
export type TypeCategory =
|
|
133
|
+
| 'string'
|
|
134
|
+
| 'integer'
|
|
135
|
+
| 'float'
|
|
136
|
+
| 'decimal'
|
|
137
|
+
| 'boolean'
|
|
138
|
+
| 'time'
|
|
139
|
+
| 'binary'
|
|
140
|
+
| 'json'
|
|
141
|
+
| 'uuid'
|
|
142
|
+
| 'spatial'
|
|
143
|
+
| 'enum'
|
|
144
|
+
| 'composite'
|
|
145
|
+
| 'array'
|
|
146
|
+
| 'unknown'
|
|
147
|
+
|
|
148
|
+
export interface AtlasColumnType {
|
|
149
|
+
T?: string
|
|
150
|
+
raw?: string
|
|
151
|
+
null?: boolean
|
|
152
|
+
/** Normalized type category — dialect-independent */
|
|
153
|
+
category?: TypeCategory
|
|
154
|
+
/** Whether this is a user-defined type (enum, composite, domain) */
|
|
155
|
+
is_custom?: boolean
|
|
156
|
+
/** Enum values (when category is 'enum') */
|
|
157
|
+
enum_values?: string[]
|
|
158
|
+
/** Composite type fields (when category is 'composite') */
|
|
159
|
+
composite_fields?: Array<{ name: string; type: string }>
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* An Index represents an index definition.
|
|
164
|
+
* Maps to flatIndex in marshal.go.
|
|
165
|
+
*/
|
|
166
|
+
export interface AtlasIndex {
|
|
167
|
+
name?: string
|
|
168
|
+
unique?: boolean
|
|
169
|
+
parts?: AtlasIndexPart[]
|
|
170
|
+
attrs?: AtlasAttr[]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* An IndexPart represents a single part of an index.
|
|
175
|
+
* Maps to flatIndexPart in marshal.go.
|
|
176
|
+
*/
|
|
177
|
+
export interface AtlasIndexPart {
|
|
178
|
+
column?: string
|
|
179
|
+
desc?: boolean
|
|
180
|
+
attrs?: AtlasAttr[]
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* A ForeignKey represents a foreign key constraint.
|
|
185
|
+
* Maps to flatForeignKey in marshal.go.
|
|
186
|
+
*/
|
|
187
|
+
export interface AtlasForeignKey {
|
|
188
|
+
symbol?: string
|
|
189
|
+
columns?: string[]
|
|
190
|
+
ref_columns?: string[]
|
|
191
|
+
ref_table?: string
|
|
192
|
+
on_update?: string
|
|
193
|
+
on_delete?: string
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A View represents a view definition.
|
|
198
|
+
* Maps to flatView in marshal.go.
|
|
199
|
+
*/
|
|
200
|
+
export interface AtlasView {
|
|
201
|
+
name: string
|
|
202
|
+
def?: string
|
|
203
|
+
columns?: AtlasColumn[]
|
|
204
|
+
attrs?: AtlasAttr[]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* A Func represents a function definition.
|
|
209
|
+
* Maps to flatFunc in marshal.go.
|
|
210
|
+
*/
|
|
211
|
+
export interface AtlasFunc {
|
|
212
|
+
name: string
|
|
213
|
+
args?: AtlasFuncArg[]
|
|
214
|
+
ret?: AtlasColumnType
|
|
215
|
+
lang?: string
|
|
216
|
+
attrs?: AtlasAttr[]
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface AtlasFuncArg {
|
|
220
|
+
name?: string
|
|
221
|
+
type?: AtlasColumnType
|
|
222
|
+
mode?: string
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A Proc represents a procedure definition.
|
|
227
|
+
* Maps to flatProc in marshal.go.
|
|
228
|
+
*/
|
|
229
|
+
export interface AtlasProc {
|
|
230
|
+
name: string
|
|
231
|
+
attrs?: AtlasAttr[]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A Trigger represents a trigger definition.
|
|
236
|
+
* Maps to flatTrigger in marshal.go.
|
|
237
|
+
*/
|
|
238
|
+
export interface AtlasTrigger {
|
|
239
|
+
name: string
|
|
240
|
+
attrs?: AtlasAttr[]
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* An expression in schema DDL.
|
|
245
|
+
* Go serializes RawExpr as { X: string }, Literal as { V: string }.
|
|
246
|
+
*/
|
|
247
|
+
export type AtlasExpr = { X: string } | { V: string } | unknown
|
|
248
|
+
|
|
249
|
+
// ── Attr union — PascalCase (map[string]string in Go marshal) ───────
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Attrs is a heterogeneous array (Tag, Comment, Check all mixed).
|
|
253
|
+
* We model the known variants as a discriminated union with a fallback.
|
|
254
|
+
* Note: Attr fields use PascalCase (serialized via map[string]string in Go).
|
|
255
|
+
*/
|
|
256
|
+
export type AtlasAttr = AtlasTag | AtlasComment | AtlasCheck | Record<string, unknown>
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Tag attr. Serialized as { Name: string, Args: string }.
|
|
260
|
+
* PascalCase because Go marshal uses map[string]string{"Name": ..., "Args": ...}.
|
|
261
|
+
*/
|
|
262
|
+
export interface AtlasTag {
|
|
263
|
+
Name: string
|
|
264
|
+
Args: string
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Comment attr. Serialized as { Text: string }.
|
|
269
|
+
*/
|
|
270
|
+
export interface AtlasComment {
|
|
271
|
+
Text: string
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Check constraint attr. Serialized as { Name?: string, Expr: string }.
|
|
276
|
+
*/
|
|
277
|
+
export interface AtlasCheck {
|
|
278
|
+
Name?: string
|
|
279
|
+
Expr: string
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ── Helper functions ────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Type guard: returns true if the given attr is a Tag.
|
|
286
|
+
* Tags have `Name` and `Args` fields but NOT an `Expr` field
|
|
287
|
+
* (which would indicate a Check constraint instead).
|
|
288
|
+
*/
|
|
289
|
+
export function isTag(attr: AtlasAttr): attr is AtlasTag {
|
|
290
|
+
return (
|
|
291
|
+
typeof attr === 'object' &&
|
|
292
|
+
attr !== null &&
|
|
293
|
+
'Name' in attr &&
|
|
294
|
+
typeof (attr as AtlasTag).Name === 'string' &&
|
|
295
|
+
'Args' in attr &&
|
|
296
|
+
typeof (attr as AtlasTag).Args === 'string' &&
|
|
297
|
+
!('Expr' in attr)
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Extract all Tag attrs from a mixed Attrs array.
|
|
303
|
+
* Returns an empty array if attrs is undefined or empty.
|
|
304
|
+
*/
|
|
305
|
+
export function findTags(attrs?: AtlasAttr[]): AtlasTag[] {
|
|
306
|
+
if (!attrs) return []
|
|
307
|
+
return attrs.filter(isTag)
|
|
308
|
+
}
|
package/src/wasi-host.ts
ADDED
|
@@ -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()
|
package/wasm/atlas.wasm
ADDED
|
Binary file
|