@silkline/hasura-sim 0.1.0

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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # @silkline/hasura-sim
2
+
3
+ An in-memory Hasura GraphQL engine for tests. The real Hasura GraphQL Engine v2
4
+ (Haskell) is compiled to WebAssembly and paired with [pglite](https://pglite.dev)
5
+ (WASM Postgres). Feed it static Hasura metadata and a migrated pglite instance,
6
+ then run GraphQL operations and get back exactly what the real engine returns —
7
+ no Docker, no Postgres server, no network.
8
+
9
+ ## How it works
10
+
11
+ ```
12
+ GraphQL op ─▶ sim_execute (wasm Hasura engine)
13
+ │ parse → IR → SQL (real engine: permissions,
14
+ │ session vars, relationships, …)
15
+
16
+ pg_exec bridge ──▶ pglite (in-process Postgres)
17
+ ▲ │
18
+ └──── rows ──────┘
19
+ ```
20
+
21
+ The engine runs the real schema-cache builder and query/mutation translator.
22
+ SQL is executed against pglite over the PostgreSQL wire protocol (`src/wire.mjs`),
23
+ so binary params + OIDs and result formats round-trip faithfully.
24
+
25
+ ## Usage
26
+
27
+ ```js
28
+ import { PGlite } from "@electric-sql/pglite";
29
+ import { createSimulator } from "@silkline/hasura-sim";
30
+
31
+ const db = new PGlite();
32
+ await db.exec(/* your migrations */);
33
+
34
+ const sim = await createSimulator({
35
+ db,
36
+ metadataJson: {
37
+ version: 3,
38
+ sources: [
39
+ {
40
+ name: "default",
41
+ kind: "postgres",
42
+ configuration: {
43
+ connection_info: { database_url: "postgres://localhost/db" },
44
+ },
45
+ tables: [{ table: { schema: "public", name: "widget" } }],
46
+ },
47
+ ],
48
+ },
49
+ });
50
+
51
+ const { data, errors } = await sim.execute({
52
+ query: "query ($id: Int!) { widget(where: { id: { _eq: $id } }) { id name } }",
53
+ variables: { id: 2 },
54
+ role: "user", // default "admin"
55
+ sessionVariables: { "x-hasura-user-id": "42" },
56
+ });
57
+ ```
58
+
59
+ `metadataJson` is consolidated Hasura metadata (`hasura metadata export -o json`).
60
+ The `database_url` is ignored — all SQL is routed to the provided pglite `db`.
61
+
62
+ ## Status
63
+
64
+ Validated end-to-end: queries (param-free + parameterized) and mutations
65
+ (insert, committing). Remote schemas and actions are out of scope (stubbed).
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@silkline/hasura-sim",
3
+ "version": "0.1.0",
4
+ "description": "In-memory Hasura GraphQL Engine (compiled to WebAssembly) + pglite — runs real GraphQL queries/mutations offline for tests and codegen, with no Docker/Postgres/network.",
5
+ "type": "module",
6
+ "main": "src/index.mjs",
7
+ "exports": {
8
+ ".": "./src/index.mjs"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "wasm/reactor.wasm",
13
+ "wasm/reactor.js",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build:wasm": "./scripts/build-wasm.sh"
18
+ },
19
+ "dependencies": {
20
+ "graphql": "^16.14.0"
21
+ },
22
+ "peerDependencies": {
23
+ "@electric-sql/pglite": ">=0.2.0"
24
+ },
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/Silkline/hasura-wasm.git",
34
+ "directory": "packages/hasura-sim"
35
+ },
36
+ "license": "Apache-2.0"
37
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,331 @@
1
+ // @silkline/hasura-sim — in-memory Hasura GraphQL engine (compiled to wasm)
2
+ // paired with pglite. Feed it static Hasura metadata + a migrated pglite
3
+ // instance, then run GraphQL operations and get back exactly what the real
4
+ // engine would return, with no Docker/Postgres/network.
5
+
6
+ import { WASI } from "node:wasi";
7
+ import { readFile } from "node:fs/promises";
8
+ import { fileURLToPath } from "node:url";
9
+ import { dirname, join } from "node:path";
10
+ import { PGlite } from "@electric-sql/pglite";
11
+ import {
12
+ buildClientSchema,
13
+ buildSchema,
14
+ getIntrospectionQuery,
15
+ graphql,
16
+ introspectionFromSchema,
17
+ printSchema,
18
+ } from "graphql";
19
+
20
+ // Build an executable schema from SDL + resolvers using only graphql-js (no
21
+ // @graphql-tools) so there is a single `graphql` instance — mixing two copies
22
+ // trips graphql-js's "Duplicate graphql modules" guard.
23
+ function buildExecutableSchema(sdl, resolvers) {
24
+ const schema = buildSchema(sdl);
25
+ for (const [typeName, fieldResolvers] of Object.entries(resolvers ?? {})) {
26
+ const type = schema.getType(typeName);
27
+ if (!type || typeof type.getFields !== "function") continue;
28
+ const fields = type.getFields();
29
+ for (const [fieldName, resolve] of Object.entries(fieldResolvers)) {
30
+ if (fields[fieldName]) fields[fieldName].resolve = resolve;
31
+ }
32
+ }
33
+ return schema;
34
+ }
35
+ import {
36
+ buildExtendedQuery,
37
+ buildSimpleQuery,
38
+ decodeResponse,
39
+ } from "./wire.mjs";
40
+
41
+ // Re-exported so consumers can build/seed a pglite instance without taking
42
+ // their own @electric-sql/pglite dependency.
43
+ export { PGlite } from "@electric-sql/pglite";
44
+
45
+ const here = dirname(fileURLToPath(import.meta.url));
46
+
47
+ // Run one Haskell-bridge request against a pglite connection via the raw wire
48
+ // protocol. Exported for tests; normally reached through the global below.
49
+ export async function execOnPglite(db, requestJson) {
50
+ const req = JSON.parse(requestJson);
51
+ const params = req.params ?? [];
52
+ const rfmt = req.rfmt ?? 0;
53
+ // The extended protocol (Parse/Bind) is the faithful path: it honours the
54
+ // requested result format (rfmt=1 ⇒ binary), which Hasura's decoders rely on
55
+ // — e.g. the mutation permission-check boolean must come back as a binary
56
+ // 0x01/0x00, not text "t"/"f". Its one limitation is that a Parse may carry
57
+ // only a single statement, so Hasura's multi-statement connection setup
58
+ // ("SET a; SET b;") falls back to the simple query protocol, whose text-only
59
+ // results are harmless there (SET returns no data).
60
+ const extended = decodeResponse(
61
+ await db.execProtocolRaw(buildExtendedQuery({ sql: req.sql, params, rfmt })),
62
+ );
63
+ if (isMultiStatementError(extended)) {
64
+ return JSON.stringify(
65
+ decodeResponse(await db.execProtocolRaw(buildSimpleQuery(req.sql))),
66
+ );
67
+ }
68
+ return JSON.stringify(extended);
69
+ }
70
+
71
+ // The extended protocol rejects a Parse containing more than one statement
72
+ // (SQLSTATE 42601, "cannot insert multiple commands into a prepared statement").
73
+ function isMultiStatementError(res) {
74
+ return (
75
+ res.status === "FatalError" &&
76
+ res.error != null &&
77
+ /multiple commands/i.test(res.error.message ?? "")
78
+ );
79
+ }
80
+
81
+ // Normalise a remote-schema definition to the introspection *response* shape
82
+ // the engine expects ({"data":{"__schema":…}}). Accepts:
83
+ // - an SDL string (e.g. the contents of server-schema.gql),
84
+ // - { sdl: "<SDL>" },
85
+ // - { introspection: <{__schema} or {data:{__schema}}> }.
86
+ export function remoteSchemaToIntrospection(def) {
87
+ if (typeof def === "string") {
88
+ return { data: introspectionFromSchema(buildSchema(def)) };
89
+ }
90
+ if (def?.sdl) {
91
+ return { data: introspectionFromSchema(buildSchema(def.sdl)) };
92
+ }
93
+ if (def?.introspection) {
94
+ const i = def.introspection;
95
+ return i.data ? i : { data: i };
96
+ }
97
+ throw new Error(
98
+ "hasura-sim: remote schema definition must be an SDL string, { sdl }, or { introspection }",
99
+ );
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // HTTP bridge: the wasm engine issues all remote-schema and action HTTP through
104
+ // a Manager backed by `js_http_exec` (see server/lib/hasura-wasm-sim's
105
+ // HttpBridge). We get the raw HTTP/1.1 request text and return raw response
106
+ // text; dispatch is by the rewritten URL (host marks kind, path carries name).
107
+
108
+ function parseHttpRequest(raw) {
109
+ const sep = raw.indexOf("\r\n\r\n");
110
+ const head = sep === -1 ? raw : raw.slice(0, sep);
111
+ const body = sep === -1 ? "" : raw.slice(sep + 4);
112
+ const lines = head.split("\r\n");
113
+ const [method, path] = (lines[0] ?? "").split(" ");
114
+ const headers = {};
115
+ for (let i = 1; i < lines.length; i++) {
116
+ const idx = lines[i].indexOf(":");
117
+ if (idx > 0) {
118
+ headers[lines[i].slice(0, idx).trim().toLowerCase()] = lines[i]
119
+ .slice(idx + 1)
120
+ .trim();
121
+ }
122
+ }
123
+ return { method, path: path ?? "/", headers, body };
124
+ }
125
+
126
+ function httpResponse(status, bodyObj) {
127
+ const body = typeof bodyObj === "string" ? bodyObj : JSON.stringify(bodyObj);
128
+ const len = Buffer.byteLength(body, "utf8");
129
+ const reason =
130
+ { 200: "OK", 400: "Bad Request", 404: "Not Found", 500: "Server Error" }[
131
+ status
132
+ ] ?? "OK";
133
+ return (
134
+ `HTTP/1.1 ${status} ${reason}\r\n` +
135
+ `content-type: application/json\r\n` +
136
+ `content-length: ${len}\r\n` +
137
+ `connection: close\r\n\r\n` +
138
+ body
139
+ );
140
+ }
141
+
142
+ // Build the `js_http_exec` handler from the registered remote schemas + actions.
143
+ function makeHttpHandler({ remotes, actions }) {
144
+ return async (rawRequest) => {
145
+ let req;
146
+ try {
147
+ req = parseHttpRequest(rawRequest);
148
+ } catch {
149
+ return httpResponse(400, { errors: [{ message: "hasura-sim: malformed request" }] });
150
+ }
151
+ const host = req.headers["host"] ?? "";
152
+ const name = decodeURIComponent(req.path.replace(/^\//, ""));
153
+ try {
154
+ if (host === "remote.local") {
155
+ const entry = remotes[name];
156
+ const gql = req.body ? JSON.parse(req.body) : {};
157
+ if (entry?.handler) return httpResponse(200, await entry.handler(gql));
158
+ if (entry?.schema) {
159
+ const result = await graphql({
160
+ schema: entry.schema,
161
+ source: gql.query,
162
+ variableValues: gql.variables ?? undefined,
163
+ operationName: gql.operationName ?? undefined,
164
+ });
165
+ return httpResponse(200, result);
166
+ }
167
+ return httpResponse(200, {
168
+ errors: [
169
+ {
170
+ message: `hasura-sim: no resolvers/handler registered for remote schema "${name}". Pass remoteSchemas["${name}"].resolvers or .handler.`,
171
+ },
172
+ ],
173
+ });
174
+ }
175
+ if (host === "action.local") {
176
+ const payload = req.body ? JSON.parse(req.body) : {};
177
+ const actionName = name || payload?.action?.name;
178
+ const handler = actions[actionName];
179
+ if (!handler) {
180
+ return httpResponse(400, {
181
+ message: `hasura-sim: no action handler registered for "${actionName}". Pass actions["${actionName}"].`,
182
+ });
183
+ }
184
+ // Hasura's action webhook `input` field is the action's *arguments*
185
+ // object (keyed by your action's arg names, e.g. { input: {...} }).
186
+ // Pass it as the handler's first arg so `({ input }) => …` reads
187
+ // naturally; session vars + raw payload come as the second arg.
188
+ const out = await handler(payload.input ?? {}, {
189
+ sessionVariables: payload.session_variables,
190
+ payload,
191
+ });
192
+ return httpResponse(200, out ?? null);
193
+ }
194
+ return httpResponse(404, {
195
+ errors: [{ message: `hasura-sim: unexpected request host "${host}"` }],
196
+ });
197
+ } catch (err) {
198
+ return httpResponse(500, {
199
+ errors: [{ message: `hasura-sim handler error: ${err?.message ?? err}` }],
200
+ });
201
+ }
202
+ };
203
+ }
204
+
205
+ async function instantiateReactor() {
206
+ const wasmBytes = await readFile(join(here, "..", "wasm", "reactor.wasm"));
207
+ const wasi = new WASI({
208
+ version: "preview1",
209
+ args: ["reactor.wasm"],
210
+ env: {},
211
+ preopens: { "/": "/" },
212
+ });
213
+ const mod = await WebAssembly.compile(wasmBytes);
214
+ const exportsBag = {};
215
+ const glue = (await import(join(here, "..", "wasm", "reactor.js"))).default(
216
+ exportsBag,
217
+ );
218
+ const instance = await WebAssembly.instantiate(mod, {
219
+ wasi_snapshot_preview1: wasi.wasiImport,
220
+ ghc_wasm_jsffi: glue,
221
+ });
222
+ Object.assign(exportsBag, instance.exports);
223
+ wasi.initialize(instance);
224
+ return instance;
225
+ }
226
+
227
+ /**
228
+ * Create a simulator.
229
+ *
230
+ * @param {object} opts
231
+ * @param {object|string} [opts.metadataJson] consolidated Hasura metadata; if
232
+ * given, the schema cache is built immediately.
233
+ * @param {PGlite} [opts.db] a pre-migrated pglite instance (else a fresh one).
234
+ * @returns {Promise<{db, execute, init, version}>}
235
+ */
236
+ export async function createSimulator({
237
+ metadataJson,
238
+ db,
239
+ remoteSchemas = {},
240
+ actions = {},
241
+ } = {}) {
242
+ const pg = db ?? new PGlite();
243
+ await pg.waitReady;
244
+
245
+ // Build executable schemas / handlers for remote-schema *execution* (the
246
+ // static SDL still feeds schema-cache build via remoteSchemaToIntrospection
247
+ // in init()). Each remoteSchemas[name] is an SDL string, or
248
+ // { sdl, resolvers? , handler? }, or { introspection }.
249
+ const remotes = {};
250
+ for (const [name, def] of Object.entries(remoteSchemas)) {
251
+ const entry = {};
252
+ if (def && typeof def === "object" && !Array.isArray(def)) {
253
+ if (typeof def.handler === "function") {
254
+ entry.handler = def.handler;
255
+ } else if (def.resolvers && def.sdl) {
256
+ entry.schema = buildExecutableSchema(def.sdl, def.resolvers);
257
+ }
258
+ }
259
+ remotes[name] = entry;
260
+ }
261
+
262
+ // The wasm bridges call these globals. Registered before sim_init so the
263
+ // introspection / remote / action calls the build + execute issue reach them.
264
+ globalThis.pg_connect = () => 1; // one in-memory database
265
+ globalThis.pg_exec = (_connId, requestJson) => execOnPglite(pg, requestJson);
266
+ globalThis.js_http_exec = makeHttpHandler({ remotes, actions });
267
+
268
+ const instance = await instantiateReactor();
269
+ const { sim_init, sim_execute, sim_version } = instance.exports;
270
+
271
+ const init = async (metadata, remotes = {}) => {
272
+ const metaPayload =
273
+ typeof metadata === "string" ? metadata : JSON.stringify(metadata);
274
+ // Static remote-schema definitions: { [remoteSchemaName]: SDL | { sdl } |
275
+ // { introspection } }. wasm can't introspect over the network, so each is
276
+ // turned into the introspection response the engine merges into the schema.
277
+ const introspections = Object.fromEntries(
278
+ Object.entries(remotes ?? {}).map(([name, def]) => [
279
+ name,
280
+ remoteSchemaToIntrospection(def),
281
+ ]),
282
+ );
283
+ return JSON.parse(await sim_init(metaPayload, JSON.stringify(introspections)));
284
+ };
285
+
286
+ if (metadataJson !== undefined) {
287
+ const result = await init(metadataJson, remoteSchemas ?? {});
288
+ if (result.error) {
289
+ throw new Error(`hasura-sim: sim_init failed: ${result.error}`);
290
+ }
291
+ }
292
+
293
+ return {
294
+ db: pg,
295
+ init,
296
+ async version() {
297
+ return sim_version();
298
+ },
299
+ async execute({ query, variables, role, sessionVariables } = {}) {
300
+ const raw = await sim_execute(
301
+ role ?? "admin",
302
+ JSON.stringify(sessionVariables ?? {}),
303
+ query,
304
+ JSON.stringify(variables ?? {}),
305
+ );
306
+ return JSON.parse(raw);
307
+ },
308
+ // Run the standard GraphQL introspection for a role and return the
309
+ // introspection result ({ __schema: … }). The schema is role-scoped, so
310
+ // pass the role (and any session vars the role expects).
311
+ async introspect({ role, sessionVariables } = {}) {
312
+ const res = await this.execute({
313
+ query: getIntrospectionQuery(),
314
+ role,
315
+ sessionVariables,
316
+ });
317
+ if (res.errors) {
318
+ throw new Error(
319
+ `hasura-sim: introspection failed: ${JSON.stringify(res.errors)}`,
320
+ );
321
+ }
322
+ return res.data;
323
+ },
324
+ // Role-scoped schema as SDL — suitable as a static codegen base, generated
325
+ // entirely offline.
326
+ async schemaSDL({ role, sessionVariables } = {}) {
327
+ const introspection = await this.introspect({ role, sessionVariables });
328
+ return printSchema(buildClientSchema(introspection));
329
+ },
330
+ };
331
+ }
package/src/wire.mjs ADDED
@@ -0,0 +1,230 @@
1
+ // Minimal PostgreSQL extended-query wire-protocol codec.
2
+ //
3
+ // The Haskell pg-client bridge (postgresql-libpq shim) hands us a request of
4
+ // { sql, params: [{oid,fmt,val(base64)}|null], rfmt } and expects back
5
+ // { status, fields: [{name,oid}], rows: [[base64|null]], cmd, error }. To
6
+ // reproduce exactly what a real Postgres would return (binary params in,
7
+ // text/binary results out), we drive pglite via its raw wire protocol rather
8
+ // than its JS-value query() API.
9
+
10
+ const enc = new TextEncoder();
11
+
12
+ class Writer {
13
+ constructor() {
14
+ this.chunks = [];
15
+ }
16
+ u8(n) {
17
+ this.chunks.push(Uint8Array.of(n & 0xff));
18
+ return this;
19
+ }
20
+ i16(n) {
21
+ const b = new Uint8Array(2);
22
+ new DataView(b.buffer).setInt16(0, n);
23
+ this.chunks.push(b);
24
+ return this;
25
+ }
26
+ i32(n) {
27
+ const b = new Uint8Array(4);
28
+ new DataView(b.buffer).setInt32(0, n);
29
+ this.chunks.push(b);
30
+ return this;
31
+ }
32
+ bytes(u8) {
33
+ this.chunks.push(u8);
34
+ return this;
35
+ }
36
+ cstr(s) {
37
+ this.chunks.push(enc.encode(s));
38
+ this.chunks.push(Uint8Array.of(0));
39
+ return this;
40
+ }
41
+ build() {
42
+ const len = this.chunks.reduce((a, c) => a + c.length, 0);
43
+ const out = new Uint8Array(len);
44
+ let o = 0;
45
+ for (const c of this.chunks) {
46
+ out.set(c, o);
47
+ o += c.length;
48
+ }
49
+ return out;
50
+ }
51
+ }
52
+
53
+ // A typed frontend message: 1-byte tag + int32 length (covering length + body).
54
+ function frame(tag, bodyWriter) {
55
+ const body = bodyWriter.build();
56
+ const w = new Writer();
57
+ w.u8(tag.charCodeAt(0));
58
+ w.i32(body.length + 4);
59
+ w.bytes(body);
60
+ return w.build();
61
+ }
62
+
63
+ function concat(arrays) {
64
+ const len = arrays.reduce((a, c) => a + c.length, 0);
65
+ const out = new Uint8Array(len);
66
+ let o = 0;
67
+ for (const a of arrays) {
68
+ out.set(a, o);
69
+ o += a.length;
70
+ }
71
+ return out;
72
+ }
73
+
74
+ // Simple query protocol ('Q'): a single message carrying the SQL text. Unlike
75
+ // the extended protocol it permits multiple statements (e.g. Hasura's
76
+ // "SET a; SET b;" connection setup) and needs no Parse/Bind. Results come back
77
+ // as text, which is what the bridge's rfmt=0 path expects.
78
+ export function buildSimpleQuery(sql) {
79
+ return frame("Q", new Writer().cstr(sql));
80
+ }
81
+
82
+ // Build Parse + Bind + Describe(portal) + Execute + Sync for one statement.
83
+ export function buildExtendedQuery({ sql, params, rfmt }) {
84
+ const parse = frame(
85
+ "P",
86
+ new Writer()
87
+ .cstr("") // unnamed prepared statement
88
+ .cstr(sql)
89
+ .i16(params.length)
90
+ .bytes(
91
+ concat(
92
+ params.map((p) => new Writer().i32(p ? p.oid : 0).build()),
93
+ ),
94
+ ),
95
+ );
96
+
97
+ const bindBody = new Writer()
98
+ .cstr("") // unnamed portal
99
+ .cstr("") // unnamed statement
100
+ .i16(params.length); // per-param format codes
101
+ for (const p of params) bindBody.i16(p ? p.fmt : 0);
102
+ bindBody.i16(params.length); // param values
103
+ for (const p of params) {
104
+ if (!p) {
105
+ bindBody.i32(-1); // SQL NULL
106
+ } else {
107
+ const bytes = Buffer.from(p.val, "base64");
108
+ bindBody.i32(bytes.length).bytes(new Uint8Array(bytes));
109
+ }
110
+ }
111
+ bindBody.i16(1).i16(rfmt); // one result-format code, applied to all columns
112
+ const bind = frame("B", bindBody);
113
+
114
+ const describe = frame("D", new Writer().u8("P".charCodeAt(0)).cstr(""));
115
+ const execute = frame("E", new Writer().cstr("").i32(0)); // no row limit
116
+ const sync = frame("S", new Writer());
117
+
118
+ return concat([parse, bind, describe, execute, sync]);
119
+ }
120
+
121
+ // Decode the backend's raw response bytes into the bridge JSON shape.
122
+ export function decodeResponse(buf) {
123
+ const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf);
124
+ const dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
125
+ let off = 0;
126
+
127
+ let fields = [];
128
+ const rows = [];
129
+ let cmd = "";
130
+ let sawRowDescription = false;
131
+ let error = null;
132
+ let empty = false;
133
+
134
+ const readCStringFrom = (start) => {
135
+ let i = start;
136
+ while (u8[i] !== 0) i++;
137
+ return [Buffer.from(u8.subarray(start, i)).toString("utf8"), i + 1];
138
+ };
139
+
140
+ while (off + 5 <= u8.length) {
141
+ const tag = String.fromCharCode(u8[off]);
142
+ const len = dv.getInt32(off + 1);
143
+ const bodyStart = off + 5;
144
+ const bodyEnd = off + 1 + len;
145
+ off = bodyEnd;
146
+
147
+ switch (tag) {
148
+ case "T": {
149
+ // RowDescription
150
+ sawRowDescription = true;
151
+ let p = bodyStart;
152
+ const count = dv.getInt16(p);
153
+ p += 2;
154
+ fields = [];
155
+ for (let i = 0; i < count; i++) {
156
+ const [name, next] = readCStringFrom(p);
157
+ p = next;
158
+ // tableOID(4) colAttr(2) typeOID(4) typeLen(2) typeMod(4) fmt(2)
159
+ const typeOID = dv.getInt32(p + 6);
160
+ p += 18;
161
+ fields.push({ name, oid: typeOID });
162
+ }
163
+ break;
164
+ }
165
+ case "D": {
166
+ // DataRow
167
+ let p = bodyStart;
168
+ const count = dv.getInt16(p);
169
+ p += 2;
170
+ const cells = [];
171
+ for (let i = 0; i < count; i++) {
172
+ const cellLen = dv.getInt32(p);
173
+ p += 4;
174
+ if (cellLen === -1) {
175
+ cells.push(null);
176
+ } else {
177
+ cells.push(
178
+ Buffer.from(u8.subarray(p, p + cellLen)).toString("base64"),
179
+ );
180
+ p += cellLen;
181
+ }
182
+ }
183
+ rows.push(cells);
184
+ break;
185
+ }
186
+ case "C": {
187
+ // CommandComplete
188
+ [cmd] = readCStringFrom(bodyStart);
189
+ break;
190
+ }
191
+ case "I": {
192
+ empty = true;
193
+ break;
194
+ }
195
+ case "E": {
196
+ // ErrorResponse: fieldCode byte + cstring, terminated by 0 byte.
197
+ let p = bodyStart;
198
+ const fieldsMap = {};
199
+ while (u8[p] !== 0) {
200
+ const code = String.fromCharCode(u8[p]);
201
+ p += 1;
202
+ const [val, next] = readCStringFrom(p);
203
+ p = next;
204
+ fieldsMap[code] = val;
205
+ }
206
+ error = {
207
+ sqlstate: fieldsMap.C,
208
+ message: fieldsMap.M,
209
+ detail: fieldsMap.D,
210
+ hint: fieldsMap.H,
211
+ };
212
+ break;
213
+ }
214
+ default:
215
+ // ParseComplete(1), BindComplete(2), NoData(n), ReadyForQuery(Z),
216
+ // ParameterDescription(t), etc. — ignored.
217
+ break;
218
+ }
219
+ }
220
+
221
+ let status;
222
+ if (error) status = "FatalError";
223
+ else if (empty) status = "EmptyQuery";
224
+ else if (sawRowDescription) status = "TuplesOk";
225
+ else status = "CommandOk";
226
+
227
+ const result = { status, fields, rows, cmd };
228
+ if (error) result.error = error;
229
+ return result;
230
+ }
@@ -0,0 +1,119 @@
1
+ // This file implements the JavaScript runtime logic for Haskell
2
+ // modules that use JSFFI. It is not an ESM module, but the template
3
+ // of one; the post-linker script will copy all contents into a new
4
+ // ESM module.
5
+
6
+ // Manage a mapping from 32-bit ids to actual JavaScript values.
7
+ class JSValManager {
8
+ #lastk = 0;
9
+ #kv = new Map();
10
+
11
+ newJSVal(v) {
12
+ const k = ++this.#lastk;
13
+ this.#kv.set(k, v);
14
+ return k;
15
+ }
16
+
17
+ // A separate has() call to ensure we can store undefined as a value
18
+ // too. Also, unconditionally check this since the check is cheap
19
+ // anyway, if the check fails then there's a use-after-free to be
20
+ // fixed.
21
+ getJSVal(k) {
22
+ if (!this.#kv.has(k)) {
23
+ throw new WebAssembly.RuntimeError(`getJSVal(${k})`);
24
+ }
25
+ return this.#kv.get(k);
26
+ }
27
+
28
+ // Check for double free as well.
29
+ freeJSVal(k) {
30
+ if (!this.#kv.delete(k)) {
31
+ throw new WebAssembly.RuntimeError(`freeJSVal(${k})`);
32
+ }
33
+ }
34
+ }
35
+
36
+ // The actual setImmediate() to be used. This is a ESM module top
37
+ // level binding and doesn't pollute the globalThis namespace.
38
+ //
39
+ // To benchmark different setImmediate() implementations in the
40
+ // browser, use https://github.com/jphpsf/setImmediate-shim-demo as a
41
+ // starting point.
42
+ const setImmediate = await (async () => {
43
+ // node, bun, or other scripts might have set this up in the browser
44
+ if (globalThis.setImmediate) {
45
+ return globalThis.setImmediate;
46
+ }
47
+
48
+ // deno
49
+ if (globalThis.Deno) {
50
+ try {
51
+ return (await import("node:timers")).setImmediate;
52
+ } catch {}
53
+ }
54
+
55
+ // https://developer.mozilla.org/en-US/docs/Web/API/Scheduler/postTask
56
+ if (globalThis.scheduler) {
57
+ return (cb, ...args) => scheduler.postTask(() => cb(...args));
58
+ }
59
+
60
+ // Cloudflare workers doesn't support MessageChannel
61
+ if (globalThis.MessageChannel) {
62
+ // A simple & fast setImmediate() implementation for browsers. It's
63
+ // not a drop-in replacement for node.js setImmediate() because:
64
+ // 1. There's no clearImmediate(), and setImmediate() doesn't return
65
+ // anything
66
+ // 2. There's no guarantee that callbacks scheduled by setImmediate()
67
+ // are executed in the same order (in fact it's the opposite lol),
68
+ // but you are never supposed to rely on this assumption anyway
69
+ class SetImmediate {
70
+ #fs = [];
71
+ #mc = new MessageChannel();
72
+
73
+ constructor() {
74
+ this.#mc.port1.addEventListener("message", () => {
75
+ this.#fs.pop()();
76
+ });
77
+ this.#mc.port1.start();
78
+ }
79
+
80
+ setImmediate(cb, ...args) {
81
+ this.#fs.push(() => cb(...args));
82
+ this.#mc.port2.postMessage(undefined);
83
+ }
84
+ }
85
+
86
+ const sm = new SetImmediate();
87
+ return (cb, ...args) => sm.setImmediate(cb, ...args);
88
+ }
89
+
90
+ return (cb, ...args) => setTimeout(cb, 0, ...args);
91
+ })();
92
+
93
+ export default (__exports) => {
94
+ const __ghc_wasm_jsffi_jsval_manager = new JSValManager();
95
+ const __ghc_wasm_jsffi_finalization_registry = globalThis.FinalizationRegistry ? new FinalizationRegistry(sp => __exports.rts_freeStablePtr(sp)) : { register: () => {}, unregister: () => true };
96
+ return {
97
+ newJSVal: (v) => __ghc_wasm_jsffi_jsval_manager.newJSVal(v),
98
+ getJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.getJSVal(k),
99
+ freeJSVal: (k) => __ghc_wasm_jsffi_jsval_manager.freeJSVal(k),
100
+ scheduleWork: () => setImmediate(__exports.rts_schedulerLoop),
101
+ ZC7ZCghczminternalZCGHCziInternalziWasmziPrimziImportsZC: ($1,$2) => ($1.then(res => __exports.rts_promiseResolveWord32($2, res), err => __exports.rts_promiseReject($2, err))),
102
+ ZC17ZCghczminternalZCGHCziInternalziWasmziPrimziImportsZC: ($1,$2) => ($1.then(res => __exports.rts_promiseResolveJSVal($2, res), err => __exports.rts_promiseReject($2, err))),
103
+ ZC18ZCghczminternalZCGHCziInternalziWasmziPrimziImportsZC: ($1,$2) => ($1.then(() => __exports.rts_promiseResolveUnit($2), err => __exports.rts_promiseReject($2, err))),
104
+ ZC0ZCghczminternalZCGHCziInternalziWasmziPrimziConcziInternalZC: async ($1) => (new Promise(res => setTimeout(res, $1 / 1000))),
105
+ ZC0ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1) => (`${$1.stack ? $1.stack : $1}`),
106
+ ZC1ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1,$2) => ((new TextDecoder('utf-8', {fatal: true})).decode(new Uint8Array(__exports.memory.buffer, $1, $2))),
107
+ ZC2ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1,$2,$3) => ((new TextEncoder()).encodeInto($1, new Uint8Array(__exports.memory.buffer, $2, $3)).written),
108
+ ZC3ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1) => ($1.length),
109
+ ZC4ZCghczminternalZCGHCziInternalziWasmziPrimziTypesZC: ($1) => {try { __ghc_wasm_jsffi_finalization_registry.unregister($1); } catch {}},
110
+ ZC0ZCpostgresqlzmlibpqzm0zi11zi0zi0zminplaceZCDatabaseziPostgreSQLziLibPQziBridgeZC: async ($1,$2) => (pg_exec($1,$2)),
111
+ ZC1ZCpostgresqlzmlibpqzm0zi11zi0zi0zminplaceZCDatabaseziPostgreSQLziLibPQziBridgeZC: async ($1) => (pg_connect($1)),
112
+ ZC0ZChasurazmwasmzmsimzm1zi0zi0zminplaceZCHasuraziWasmSimziHttpBridgeziFfiZC: async ($1) => (js_http_exec($1)),
113
+ ZC0ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.reject(new WebAssembly.RuntimeError($2))),
114
+ ZC18ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => ($1.resolve($2)),
115
+ ZC20ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1) => {$1.throwTo = () => {};},
116
+ ZC21ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: ($1,$2) => {$1.throwTo = (err) => __exports.rts_promiseThrowTo($2, err);},
117
+ ZC22ZCghczminternalZCGHCziInternalziWasmziPrimziExportsZC: () => {let res, rej; const p = new Promise((resolve, reject) => { res = resolve; rej = reject; }); p.resolve = res; p.reject = rej; return p;},
118
+ };
119
+ };
Binary file