Created
January 29, 2026 16:03
-
-
Save shreyassanthu77/3c237923e1a2c19cefd14245ffad05cb to your computer and use it in GitHub Desktop.
sql proxy? no idea if this works well but seems like it's good enough?
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
| import { createClient, type Client, type InArgs } from "@libsql/client/node"; | |
| import { getPort } from "get-port-please"; | |
| import * as devalue from "devalue"; | |
| export class SqlProxy { | |
| #initPromise: Promise<void>; | |
| #state!: | |
| | { | |
| kind: "server"; | |
| s: Deno.HttpServer; | |
| client: Client; | |
| lockFile: Deno.FsFile; | |
| } | |
| | { | |
| kind: "client"; | |
| execute: (sql: string, args: InArgs) => Promise<unknown[]>; | |
| }; | |
| get isServer() { | |
| return this.#state.kind === "server"; | |
| } | |
| constructor(dbURL: string) { | |
| this.#initPromise = this.#init(dbURL); | |
| } | |
| async execute(sql: string, args: InArgs = []): Promise<unknown[]> { | |
| await this.#initPromise; | |
| if (this.#state.kind === "server") { | |
| const { rows, columns } = await this.#state.client.execute({ | |
| sql, | |
| args, | |
| }); | |
| return rows.map((row) => { | |
| const obj: Record<string, unknown> = {}; | |
| for (let i = 0; i < columns.length; i++) { | |
| const column = columns[i]; | |
| obj[column] = row[i]; | |
| } | |
| return obj; | |
| }); | |
| } else { | |
| return this.#state.execute(sql, args); | |
| } | |
| } | |
| async executeMultiple(sql: string): Promise<void> { | |
| await this.#initPromise; | |
| if (this.#state.kind !== "server") { | |
| throw new Error("executeMultiple can only be called on the server"); | |
| } | |
| await this.#state.client.executeMultiple(sql); | |
| } | |
| async #init(url: string, isRetry = false) { | |
| const { pid, port, lockFile } = await this.#acquireLock( | |
| url.replaceAll(/[:/]/g, "_"), | |
| ); | |
| if (pid === Deno.pid) { | |
| const client = createClient({ | |
| url, | |
| }); | |
| const s = Deno.serve({ port }, async (req) => { | |
| const method = req.method.toLowerCase(); | |
| const path = new URL(req.url).pathname; | |
| if (method !== "post" || path !== "/sql") { | |
| return new Response("Invalid request", { status: 400 }); | |
| } | |
| let body: unknown; | |
| try { | |
| const text = await req.text(); | |
| body = devalue.parse(text); | |
| } catch (e) { | |
| console.error(e); | |
| return new Response("Invalid request body", { status: 400 }); | |
| } | |
| if ( | |
| typeof body !== "object" || | |
| body === null || | |
| !("sql" in body) || | |
| typeof body.sql !== "string" || | |
| !("args" in body) || | |
| !Array.isArray(body.args) | |
| ) { | |
| return new Response("Invalid request body", { status: 400 }); | |
| } | |
| const { sql, args } = body; | |
| try { | |
| const { rows, columns } = await client.execute({ | |
| sql, | |
| args, | |
| }); | |
| const data = rows.map((row) => { | |
| const obj: Record<string, unknown> = {}; | |
| for (let i = 0; i < columns.length; i++) { | |
| const column = columns[i]; | |
| obj[column] = row[i]; | |
| } | |
| return obj; | |
| }); | |
| const stringified = devalue.stringify(data); | |
| return new Response(stringified, { | |
| headers: { | |
| "Content-Type": "application/devalue+json", | |
| }, | |
| }); | |
| } catch (e) { | |
| console.error("SQL execution error:", e); | |
| return new Response(String(e), { status: 500 }); | |
| } | |
| }); | |
| this.#state = { | |
| kind: "server", | |
| s, | |
| client, | |
| lockFile: lockFile!, | |
| }; | |
| } else { | |
| const fetchUrl = `http://localhost:${port}/sql`; | |
| // deno-lint-ignore no-this-alias | |
| const parent = this; | |
| // deno-lint-ignore no-inner-declarations | |
| async function execute(sql: string, args: InArgs) { | |
| const body = devalue.stringify({ sql, args }); | |
| let res: Response; | |
| try { | |
| res = await fetch(fetchUrl, { | |
| method: "POST", | |
| headers: { | |
| "Content-Type": "application/devalue+json", | |
| }, | |
| body, | |
| }); | |
| } catch (e) { | |
| if (isRetry) { | |
| console.error(e); | |
| throw e; | |
| } | |
| await (parent.#initPromise = parent.#init(url, true)); | |
| return parent.execute(sql, args); | |
| } | |
| const text = await res.text(); | |
| if (!res.ok) { | |
| throw new Error(`SQL proxy error (${res.status}): ${text}`); | |
| } | |
| const data = devalue.parse(text); | |
| return data; | |
| } | |
| this.#state = { | |
| kind: "client", | |
| execute, | |
| }; | |
| } | |
| } | |
| async #acquireLock(id: string) { | |
| const lockFilePath = `/tmp/openmemory-${id}.lock`; | |
| const timeout = 5000; | |
| const start = Date.now(); | |
| while (Date.now() - start < timeout) { | |
| try { | |
| const lockFile = Deno.openSync(lockFilePath, { | |
| createNew: true, | |
| write: true, | |
| }); | |
| lockFile.lockSync(true); | |
| const pid = Deno.pid; | |
| const port = await getPort({ random: true }); | |
| lockFile.writeSync( | |
| new TextEncoder().encode(JSON.stringify({ pid, port })), | |
| ); | |
| return { pid, port, lockFile }; | |
| } catch (e) { | |
| if (e instanceof Deno.errors.AlreadyExists) { | |
| try { | |
| const existing: { | |
| pid: number; | |
| port: number; | |
| lockFile: null; | |
| } = JSON.parse(Deno.readTextFileSync(lockFilePath)); | |
| Deno.kill(existing.pid, 0); | |
| return existing; | |
| } catch (e) { | |
| if (e instanceof Deno.errors.NotFound) { | |
| // process is dead, remove lock file | |
| Deno.removeSync(lockFilePath); | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| } else { | |
| throw e; | |
| } | |
| } | |
| } else throw e; | |
| } | |
| } | |
| throw new Error("Timeout acquiring lock"); | |
| } | |
| async deinit() { | |
| await this.#initPromise; | |
| if (this.#state?.kind === "server") { | |
| await this.#state.s.shutdown(); | |
| this.#state.lockFile.close(); | |
| this.#state.client.close(); | |
| } | |
| } | |
| [Symbol.asyncDispose]() { | |
| return this.deinit(); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment