From a3ed5884e0d0ae6010ee63a7706a9862ad394c8c Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Sat, 29 Aug 2026 02:14:25 -0700 Subject: [PATCH] Fix the dev-db socket test wedge: bind conflicts hung server.start() --- apps/cloud/scripts/test-globalsetup.ts | 9 +- .../db/dev-db-socket-concurrency.node.test.ts | 123 ++++++++++++++---- .../src/test-globalsetup-exit.node.test.ts | 13 +- .../@electric-sql%2Fpglite-socket@0.1.4.patch | 7 +- 4 files changed, 116 insertions(+), 36 deletions(-) diff --git a/apps/cloud/scripts/test-globalsetup.ts b/apps/cloud/scripts/test-globalsetup.ts index 4362c3285d..7efc25afe0 100644 --- a/apps/cloud/scripts/test-globalsetup.ts +++ b/apps/cloud/scripts/test-globalsetup.ts @@ -13,12 +13,17 @@ import { fileURLToPath } from "node:url"; const __dirname = dirname(fileURLToPath(import.meta.url)); +// 0 asks the OS for a free port — used by the globalsetup-exit fixture, whose +// test never connects to the database. A suite whose tests DO connect (the +// default 5434 path, matched by DATABASE_URL in vitest.config.ts) must pass a +// real port. Fixed ports must stay below 32768: the Linux ephemeral range +// (32768-60999) is contested by every concurrent suite's outbound sockets. const parsePort = (input: string | undefined): number => { if (input === undefined) return 5434; if (!/^\d+$/.test(input)) throw new Error("CLOUD_TEST_DB_PORT must be an integer"); const port = Number(input); - if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) { - throw new Error("CLOUD_TEST_DB_PORT must be between 1 and 65535"); + if (!Number.isSafeInteger(port) || port > 65_535) { + throw new Error("CLOUD_TEST_DB_PORT must be between 0 and 65535"); } return port; }; diff --git a/apps/cloud/src/db/dev-db-socket-concurrency.node.test.ts b/apps/cloud/src/db/dev-db-socket-concurrency.node.test.ts index 3a2d5d1675..94c6228071 100644 --- a/apps/cloud/src/db/dev-db-socket-concurrency.node.test.ts +++ b/apps/cloud/src/db/dev-db-socket-concurrency.node.test.ts @@ -22,16 +22,42 @@ // one PGLiteSocketServer and asserts zero protocol corruption. import { setTimeout as sleep } from "node:timers/promises"; -import { connect, type Socket } from "node:net"; +import { connect, createServer, type Socket } from "node:net"; import { describe, expect, it } from "@effect/vitest"; import { PGlite } from "@electric-sql/pglite"; import { PGLiteSocketServer } from "@electric-sql/pglite-socket"; import postgres from "postgres"; -const PORT = 45998; const CLIENTS = 6; const QUERIES_PER_CLIENT = 40; +/** + * Start a server bound to an OS-assigned port and return that port. + * + * Every server in this file binds port 0. The fixed ports this file used to + * bind (45993-45998) sit inside the default Linux ephemeral port range + * (32768-60999): on a busy CI runner any other socket — an outbound connection + * from a sibling suite in the same turbo shard, or a leaked e2e server, which + * squat exactly this block — can hold one of them at bind time. The stock + * server then swallowed the EADDRINUSE (start() rejected only when `active` + * was false, and start() sets `active` true before listen), so + * `await server.start()` never settled and the test died as a bare vitest + * timeout with zero diagnostics — the CI signature of every wedge in this + * family. macOS assigns ephemeral ports from 49152, which is why hundreds of + * local replays never reproduced it. + */ +const startOnOsPort = async (server: PGLiteSocketServer): Promise => { + const listening = new Promise((resolve) => { + server.addEventListener( + "listening", + (event) => resolve((event as CustomEvent<{ readonly port: number }>).detail.port), + { once: true }, + ); + }); + await server.start(); + return await listening; +}; + const makeClient = (port: number, connectTimeout = 5) => postgres(`postgres://postgres:postgres@127.0.0.1:${port}/postgres`, { max: 1, @@ -71,14 +97,16 @@ const openWireClient = async (port: number): Promise => { * Run a bystander's query with a bounded deadline and, on the deadline, fail * with the server's internals instead of vitest's bare 30s timeout. * - * The reap/ghost scenarios have each wedged ONCE in CI (runs 32933818134 and - * 33019527020: the bystander's startup was served, then its query hung until - * the test timeout) while ~800 replays of the isolated scenarios on macOS and - * Linux, idle and CPU-starved, never reproduced it. Until it fires again there - * is nothing to fix, so make the next occurrence carry its own diagnosis: - * the queue/handler stats at wedge time, plus whether a FRESH connection still - * completes startup (a latched queue serves nobody; per-handler affinity - * pinning still answers new startups). + * The reap/ghost scenarios each wedged in CI (runs 32933818134 and + * 33019527020) while ~800 replays of the isolated scenarios on macOS and + * Linux, idle and CPU-starved, never reproduced it. Those bare timeouts are + * now attributed: a bind conflict on this file's old fixed ephemeral-range + * ports left `server.start()` pending forever (see startOnOsPort), which a + * bare vitest timeout cannot distinguish from a queue wedge. The wrapper + * stays so that any FUTURE wedge that really is in the queue carries its own + * diagnosis: the queue/handler stats at wedge time, plus whether a FRESH + * connection still completes startup (a latched queue serves nobody; + * per-handler affinity pinning still answers new startups). */ const diagnoseWedge = async ( run: () => Promise, @@ -133,17 +161,17 @@ describe("dev-db PGlite socket under concurrent connections", () => { const db = await PGlite.create(); const server = new PGLiteSocketServer({ db, - port: PORT, + port: 0, host: "127.0.0.1", maxConnections: 100, }); - await server.start(); + const port = await startOnOsPort(server); let ok = 0; const errors: string[] = []; const worker = async (id: number) => { - const sql = makeClient(PORT, 10); + const sql = makeClient(port, 10); // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: postgres.js is promise-native and the socket must be closed on every path try { for (let q = 0; q < QUERIES_PER_CLIENT; q++) { @@ -192,10 +220,14 @@ describe("dev-db PGlite socket under concurrent connections", () => { "a rejected query fails one client, not the whole socket server", { timeout: 30_000 }, async () => { - const port = 45997; const db = await PGlite.create(); - const server = new PGLiteSocketServer({ db, port, host: "127.0.0.1", maxConnections: 100 }); - await server.start(); + const server = new PGLiteSocketServer({ + db, + port: 0, + host: "127.0.0.1", + maxConnections: 100, + }); + const port = await startOnOsPort(server); const first = makeClient(port); // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path @@ -243,16 +275,15 @@ describe("dev-db PGlite socket under concurrent connections", () => { // only fires on a connection that is actually blocking the shared session — // an open pipeline or an open transaction. it("an idle-at-rest connection outlives the idle backstop", { timeout: 30_000 }, async () => { - const port = 45996; const db = await PGlite.create(); const server = new PGLiteSocketServer({ db, - port, + port: 0, host: "127.0.0.1", maxConnections: 100, idleTimeout: 250, }); - await server.start(); + const port = await startOnOsPort(server); const sql = makeClient(port); // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path @@ -276,16 +307,15 @@ describe("dev-db PGlite socket under concurrent connections", () => { "a client stalled mid-pipeline is reaped and the queue recovers", { timeout: 30_000 }, async () => { - const port = 45995; const db = await PGlite.create(); const server = new PGLiteSocketServer({ db, - port, + port: 0, host: "127.0.0.1", maxConnections: 100, idleTimeout: 250, }); - await server.start(); + const port = await startOnOsPort(server); // Hand-rolled wire client: complete the trust-auth startup, then send a // lone Parse. Its last frame type ('P') marks the pipeline open, so the @@ -320,16 +350,15 @@ describe("dev-db PGlite socket under concurrent connections", () => { // as the same CONNECT_TIMEOUT cascade as the queue wedges. The server now // drops the handler when it dispatches its terminal error. it("reaped handlers release their connection slots", { timeout: 30_000 }, async () => { - const port = 45993; const db = await PGlite.create(); const server = new PGLiteSocketServer({ db, - port, + port: 0, host: "127.0.0.1", maxConnections: 2, idleTimeout: 250, }); - await server.start(); + const port = await startOnOsPort(server); // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: sockets must be closed on every path try { @@ -372,7 +401,6 @@ describe("dev-db PGlite socket under concurrent connections", () => { "a client that dies mid-execution does not leave the queue pinned to its ghost", { timeout: 30_000 }, async () => { - const port = 45994; const db = await PGlite.create(); // Hold the marker query in flight long enough that the disconnect below @@ -384,8 +412,13 @@ describe("dev-db PGlite socket under concurrent connections", () => { return real(...args); }; - const server = new PGLiteSocketServer({ db, port, host: "127.0.0.1", maxConnections: 100 }); - await server.start(); + const server = new PGLiteSocketServer({ + db, + port: 0, + host: "127.0.0.1", + maxConnections: 100, + }); + const port = await startOnOsPort(server); const ghost = await openWireClient(port); ghost.write(parseFrame("select 'ghost_marker'")); @@ -408,4 +441,38 @@ describe("dev-db PGlite socket under concurrent connections", () => { } }, ); + + // Regression for the CI wedge behind every bare-timeout flake in this file: + // stock pglite-socket start() only rejected its promise while `active` was + // false, but start() sets `active` true BEFORE listen(), so a bind failure + // (EADDRINUSE — this file used to bind fixed ports inside the Linux + // ephemeral range, where any concurrent suite's outbound socket or a leaked + // e2e server can sit) dispatched an 'error' event nobody listened to and + // left `await server.start()` pending forever. The test then died as a raw + // vitest timeout with zero diagnostics. The patch rejects start() on server + // errors; a settled promise ignores later rejects, so post-listen errors + // still only surface through the 'error' event. + it( + "a bind conflict rejects start() instead of hanging forever", + { timeout: 30_000 }, + async () => { + const squatter = createServer(); + await new Promise((resolve) => squatter.listen(0, "127.0.0.1", () => resolve())); + const address = squatter.address(); + if (address === null || typeof address !== "object") { + expect.unreachable("squatter listener has no bound address"); + } + + const db = await PGlite.create(); + const server = new PGLiteSocketServer({ db, port: address.port, host: "127.0.0.1" }); + // oxlint-disable-next-line executor/no-try-catch-or-throw -- test boundary: the squatter and PGlite must be released on every path + try { + await expect(server.start()).rejects.toThrow(/EADDRINUSE/); + } finally { + await server.stop(); + await db.close(); + await new Promise((resolve) => squatter.close(() => resolve())); + } + }, + ); }); diff --git a/apps/cloud/src/test-globalsetup-exit.node.test.ts b/apps/cloud/src/test-globalsetup-exit.node.test.ts index 8eff5a4d20..c36a4ff7d2 100644 --- a/apps/cloud/src/test-globalsetup-exit.node.test.ts +++ b/apps/cloud/src/test-globalsetup-exit.node.test.ts @@ -7,14 +7,19 @@ const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const vitestBin = resolve(appRoot, "../../node_modules/vitest/vitest.mjs"); const fixtureConfig = resolve(appRoot, "test-fixtures/test-globalsetup-exit/vitest.config.ts"); -const runFixture = (port: number, shouldPass: boolean) => +// The nested globalsetup binds an OS-assigned port (0): the fixture test never +// connects to the database, and the fixed ports this file used to pass +// (45435/45436) sat inside the Linux ephemeral range, where a concurrent +// suite's outbound socket could hold them — the nested vitest then hung on the +// swallowed bind failure until spawnSync's timeout killed it (signal !== null). +const runFixture = (shouldPass: boolean) => spawnSync(process.execPath, [vitestBin, "run", "--config", fixtureConfig], { cwd: appRoot, encoding: "utf8", timeout: 60_000, env: { ...process.env, - CLOUD_TEST_DB_PORT: String(port), + CLOUD_TEST_DB_PORT: "0", TEST_GLOBALSETUP_SHOULD_PASS: String(shouldPass), }, }); @@ -24,7 +29,7 @@ const diagnostic = (result: ReturnType): string => describe("cloud test global setup", () => { it("does not let PGlite teardown turn a passed test red", { timeout: 60_000 }, () => { - const result = runFixture(45_435, true); + const result = runFixture(true); expect(result.error).toBeUndefined(); expect(result.signal).toBeNull(); @@ -32,7 +37,7 @@ describe("cloud test global setup", () => { }); it("does not let PGlite teardown turn a failed test green", { timeout: 60_000 }, () => { - const result = runFixture(45_436, false); + const result = runFixture(false); expect(result.error).toBeUndefined(); expect(result.signal).toBeNull(); diff --git a/patches/@electric-sql%2Fpglite-socket@0.1.4.patch b/patches/@electric-sql%2Fpglite-socket@0.1.4.patch index 6e29eb0beb..5cecf777a5 100644 --- a/patches/@electric-sql%2Fpglite-socket@0.1.4.patch +++ b/patches/@electric-sql%2Fpglite-socket@0.1.4.patch @@ -4,6 +4,9 @@ index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2 diff --git a/node_modules/@electric-sql/pglite-socket/.bun-tag-a8fabe72c1056a8f b/.bun-tag-a8fabe72c1056a8f new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 +diff --git a/node_modules/@electric-sql/pglite-socket/.bun-tag-dbf9bed28ab5e7fa b/.bun-tag-dbf9bed28ab5e7fa +new file mode 100644 +index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/node_modules/@electric-sql/pglite-socket/.bun-tag-eaa11f63ffd98a26 b/.bun-tag-eaa11f63ffd98a26 new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 @@ -11,13 +14,13 @@ diff --git a/node_modules/@electric-sql/pglite-socket/.bun-tag-fbab1bb0bfbef953 new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/dist/chunk-NSUMFCRM.js b/dist/chunk-NSUMFCRM.js -index 37d45ebc5150c43c919fbdb1c7fffb51b18fda8c..ab5ff4de0ca78cc8a4b7905fe30f93b655473c14 100644 +index 37d45ebc5150c43c919fbdb1c7fffb51b18fda8c..90a2e42135dea2c8a52219353bd189254f09c425 100644 --- a/dist/chunk-NSUMFCRM.js +++ b/dist/chunk-NSUMFCRM.js @@ -1,3 +1,3 @@ -import{createServer as m}from"net";var b=6e4,c=class{constructor(s,e=!1){this.queue=[];this.processing=!1;this.lastHandlerId=null;this.db=s,this.debug=e}log(s,...e){this.debug&&console.log(`[QueryQueueManager] ${s}`,...e)}async enqueue(s,e,i){return new Promise((t,r)=>{let o={handlerId:s,message:e,resolve:t,reject:r,timestamp:Date.now(),onData:i};this.queue.push(o),this.log(`enqueued query from handler #${s}, queue size: ${this.queue.length}`),this.processing||this.processQueue()})}async processQueue(){if(!(this.processing||this.queue.length===0)){for(this.processing=!0;this.queue.length>0;){let s;if(this.db.isInTransaction()&&this.lastHandlerId){let t=this.queue.findIndex(r=>r.handlerId===this.lastHandlerId);t===-1?(this.log("transaction started, but no query from the same handler id found in queue",this.lastHandlerId),s=null):s=this.queue.splice(t,1)[0]}else s=this.queue.shift();if(!s)break;let e=Date.now()-s.timestamp;this.log(`processing query from handler #${s.handlerId} (waited ${e}ms)`);let i=0;try{await this.db.runExclusive(async()=>await this.db.execProtocolRawStream(s.message,{onRawData:t=>{i+=t.length,s.onData(t)}}))}catch(t){this.log(`query from handler #${s.handlerId} failed:`,t),s.reject(t);return}this.log(`query from handler #${s.handlerId} completed, ${i} bytes`),this.lastHandlerId=s.handlerId,s.resolve(i)}this.processing=!1,this.log("queue processing complete, queue length is",this.queue.length)}}getQueueLength(){return this.queue.length}clearQueueForHandler(s){let e=this.queue.length;this.queue=this.queue.filter(t=>t.handlerId===s?(t.reject(new Error("Handler disconnected")),!1):!0);let i=e-this.queue.length;i>0&&this.log(`cleared ${i} queries for handler #${s}`)}async clearTransactionIfNeeded(s){this.db.isInTransaction()&&this.lastHandlerId===s&&(await this.db.exec("ROLLBACK"),this.lastHandlerId=null,await this.processQueue())}},l=class l extends EventTarget{constructor(e){super();this.socket=null;this.active=!1;this.messageBuffer=Buffer.alloc(0);this.lastActivityTime=Date.now();this.queryQueue=e.queryQueue,this.closeOnDetach=e.closeOnDetach??!1,this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.idleTimeout=e.idleTimeout??0,this.id=l.nextHandlerId++,this.log("constructor: created new handler")}get handlerId(){return this.id}log(e,...i){this.debug&&console.log(`[PGLiteSocketHandler#${this.id}] ${e}`,...i)}async attach(e){if(this.log(`attach: attaching socket from ${e.remoteAddress}:${e.remotePort}`),this.socket)throw new Error("Socket already attached");return this.socket=e,this.active=!0,this.lastActivityTime=Date.now(),e.setNoDelay(!0),this.idleTimeout>0&&this.resetIdleTimer(),this.log("attach: setting up socket event handlers"),e.on("data",i=>{this.lastActivityTime=Date.now(),this.resetIdleTimer(),setImmediate(async()=>{try{await this.handleData(i)}catch(t){this.log("socket on data error: ",t),this.handleError(t)}})}),e.on("error",i=>{setImmediate(()=>this.handleError(i))}),e.on("close",()=>{setImmediate(()=>this.handleClose())}),this.log("attach: socket handler ready"),this}resetIdleTimer(){this.idleTimeout<=0||(this.idleTimer&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{let e=Date.now()-this.lastActivityTime;this.log(`idle timeout after ${e}ms`),this.handleError(new Error("Idle timeout"))},this.idleTimeout))}async detach(e){if(this.log(`detach: detaching socket, close=${e??this.closeOnDetach}`),this.idleTimer&&(clearTimeout(this.idleTimer),this.idleTimer=void 0),this.queryQueue.clearQueueForHandler(this.id),await this.queryQueue.clearTransactionIfNeeded(this.id),!this.socket)return this.log("detach: no socket attached, nothing to do"),this;if(this.socket.removeAllListeners("data"),this.socket.removeAllListeners("error"),this.socket.removeAllListeners("close"),(e??this.closeOnDetach)&&this.socket.writable){this.log("detach: closing socket");try{this.socket.end(),this.socket.destroy()}catch(i){this.log("detach: error closing socket:",i)}}return this.socket=null,this.active=!1,this.messageBuffer=Buffer.alloc(0),this.log("detach: handler cleaned up"),this}get isAttached(){return this.socket!==null}async handleData(e){if(!this.socket||!this.active)return this.log("handleData: no active socket, ignoring data"),0;this.log(`handleData: received ${e.length} bytes`),this.messageBuffer=Buffer.concat([this.messageBuffer,e]),this.inspectData("incoming",e);try{let i=0;for(;this.messageBuffer.length>0;){let t=0,r=!1;if(this.messageBuffer.length>=4){let n=this.messageBuffer.readInt32BE(0);if(this.messageBuffer.length>=8){let a=this.messageBuffer.readInt32BE(4);(a===196608||a===196608)&&(t=n,r=this.messageBuffer.length>=t)}!r&&this.messageBuffer.length>=5&&(t=1+this.messageBuffer.readInt32BE(1),r=this.messageBuffer.length>=t)}if(!r||t===0){this.log(`handleData: incomplete message, buffering ${this.messageBuffer.length} bytes`);break}let o=this.messageBuffer.slice(0,t);if(this.messageBuffer=this.messageBuffer.slice(t),this.log(`handleData: processing message of ${o.length} bytes`),!this.active||!this.socket){this.log("handleData: socket no longer active, stopping processing");break}let h;if(await this.queryQueue.enqueue(this.id,new Uint8Array(o),n=>{this.log(`handleData: received ${n.length} bytes from PGlite`),this.inspectData("outgoing",n),n.length>0&&this.socket&&this.socket.writable&&this.active&&(this.log("handleData: writing response to socket"),this.socket?.writable?this.socket.write(Buffer.from(n),a=>{a?(this.log("handleData: error writing to socket:",a),h=a):this.log(`handleData: socket sent: ${n.length} bytes`)}):this.log("handleData: socket no longer writable")),i+=n.length}),h)throw h}return this.dispatchEvent(new CustomEvent("data",{detail:{incoming:e.length,outgoing:i}})),i}catch(i){throw this.log("handleData: error processing data:",i),i}}handleError(e){if(!this.active){this.log("handleError: handler not active, ignoring error");return}e.message?.includes("ECONNRESET")?this.log("handleError: client disconnected (ECONNRESET) - normal behavior"):e.message?.includes("Idle timeout")?this.log("handleError: connection idle timeout"):this.log("handleError:",e),this.active=!1,this.dispatchEvent(new CustomEvent("error",{detail:e})),this.detach(!0)}handleClose(){this.log("handleClose: socket closed"),this.active=!1,this.dispatchEvent(new CustomEvent("close")),this.detach(!1)}inspectData(e,i){if(this.inspect){console.log("-".repeat(75)),console.log(e==="incoming"?"-> incoming":"<- outgoing",i.length,"bytes");for(let t=0;t=32&&a<=126?String.fromCharCode(a):"."}console.log(`${t.toString(16).padStart(8,"0")} ${o} ${h}`)}}}};l.nextHandlerId=1;var d=l,u=class extends EventTarget{constructor(e){super();this.server=null;this.active=!1;this.handlers=new Set;this.db=e.db,e.path?this.path=e.path:(typeof e.port=="number"?this.port=e.port??e.port:this.port=5432,this.host=e.host||"127.0.0.1"),this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.idleTimeout=e.idleTimeout??0,this.maxConnections=e.maxConnections??1,this.queryQueue=new c(this.db,this.debug),this.log(`constructor: created server on ${this.getServerConn()}`),this.log(`constructor: max connections: ${this.maxConnections}`),this.idleTimeout>0&&this.log(`constructor: idle timeout: ${this.idleTimeout}ms`)}log(e,...i){this.debug&&console.log(`[PGLiteSocketServer] ${e}`,...i)}async start(){if(this.log(`start: starting server on ${this.getServerConn()}`),this.server)throw new Error("Socket server already started");return await this.db.waitReady,this.active=!0,this.server=m(e=>{setImmediate(()=>this.handleConnection(e))}),this.server.maxConnections=this.maxConnections,new Promise((e,i)=>{if(!this.server)return i(new Error("Server not initialized"));if(this.server.on("error",t=>{this.log("start: server error:",t),this.dispatchEvent(new CustomEvent("error",{detail:t})),this.active||i(t)}),this.path)this.server.listen(this.path,()=>{this.log(`start: server listening on ${this.getServerConn()}`),this.dispatchEvent(new CustomEvent("listening",{detail:{path:this.path}})),e()});else{let t=this.server;t.listen(this.port,this.host,()=>{let r=t.address();if(r===null||typeof r!="object")throw Error("Expected address info");this.port=r.port,this.log(`start: server listening on ${this.getServerConn()}`),this.dispatchEvent(new CustomEvent("listening",{detail:{port:this.port,host:this.host}})),e()})}})}getServerConn(){return this.path?this.path:`${this.host}:${this.port}`}async stop(){this.log("stop: stopping server"),this.active=!1,this.log(`stop: detaching ${this.handlers.size} handlers`);for(let e of this.handlers)e.detach(!0);return this.handlers.clear(),this.server?new Promise(e=>{if(!this.server)return e();this.server.close(()=>{this.log("stop: server closed"),this.server=null,this.dispatchEvent(new CustomEvent("close")),e()})}):(this.log("stop: server not running, nothing to do"),Promise.resolve())}async handleConnection(e){let i={clientAddress:e.remoteAddress||"unknown",clientPort:e.remotePort||0};if(this.log(`handleConnection: new connection from ${i.clientAddress}:${i.clientPort}`),this.log(`handleConnection: active connections: ${this.handlers.size}, queued queries: ${this.queryQueue.getQueueLength()}`),!this.active){this.log("handleConnection: server not active, closing connection");try{e.end()}catch(r){this.log("handleConnection: error closing socket:",r)}return}if(this.handlers.size>=this.maxConnections){this.log("handleConnection: max connections reached, rejecting"),e.write(Buffer.from(`Too many connections -`)),e.end();return}let t=new d({queryQueue:this.queryQueue,closeOnDetach:!0,inspect:this.inspect,debug:this.debug,idleTimeout:this.idleTimeout});this.handlers.add(t),t.addEventListener("error",r=>{let o=r.detail;o?.message?.includes("ECONNRESET")?this.log(`handler #${t.handlerId}: client disconnected (ECONNRESET)`):o?.message?.includes("Idle timeout")?this.log(`handler #${t.handlerId}: idle timeout`):this.log(`handler #${t.handlerId}: error:`,o)}),t.addEventListener("close",()=>{this.log(`handler #${t.handlerId}: closed`),this.handlers.delete(t),this.log(`handleConnection: active connections: ${this.handlers.size}`)});try{await t.attach(e),this.dispatchEvent(new CustomEvent("connection",{detail:i}))}catch(r){this.log("handleConnection: error attaching socket:",r),this.handlers.delete(t),this.dispatchEvent(new CustomEvent("error",{detail:r}));try{e.end()}catch(o){this.log("handleConnection: error closing socket:",o)}}}getStats(){return{activeConnections:this.handlers.size,queuedQueries:this.queryQueue.getQueueLength(),maxConnections:this.maxConnections}}};export{b as a,d as b,u as c}; -+import{createServer as m}from"net";var b=6e4,c=class{constructor(s,e=!1){this.queue=[];this.processing=!1;this.lastHandlerId=null;this.pipelineHandlerId=null;this.dead=new Set();this.db=s,this.debug=e}log(s,...e){this.debug&&console.log(`[QueryQueueManager] ${s}`,...e)}async enqueue(s,e,i,S=!0){return new Promise((t,r)=>{let o={handlerId:s,message:e,resolve:t,reject:r,timestamp:Date.now(),onData:i,closes:S};this.queue.push(o),this.log(`enqueued query from handler #${s}, queue size: ${this.queue.length}`),this.processing||this.processQueue()})}async processQueue(){if(!(this.processing||this.queue.length===0)){for(this.processing=!0;this.queue.length>0;){let s;let __affine=this.db.isInTransaction()&&this.lastHandlerId?this.lastHandlerId:this.pipelineHandlerId;if(__affine&&this.dead.has(__affine)){this.log("affinity held by detached handler, recovering",__affine);if(this.db.isInTransaction()&&this.lastHandlerId===__affine){await this.db.exec("ROLLBACK").catch(()=>{});this.lastHandlerId=null}if(this.pipelineHandlerId===__affine){this.pipelineHandlerId=null;await this.db.runExclusive(async()=>await this.db.execProtocolRawStream(new Uint8Array([83,0,0,0,4]),{onRawData:()=>{}})).catch(()=>{})}continue}if(__affine){let t=this.queue.findIndex(r=>r.handlerId===__affine);t===-1?(this.log("affinity held, waiting for handler",__affine),s=null):s=this.queue.splice(t,1)[0]}else s=this.queue.shift();if(!s)break;let e=Date.now()-s.timestamp;this.log(`processing query from handler #${s.handlerId} (waited ${e}ms)`);let i=0;try{await this.db.runExclusive(async()=>await this.db.execProtocolRawStream(s.message,{onRawData:t=>{i+=t.length,s.onData(t)}}))}catch(t){this.log(`query from handler #${s.handlerId} failed:`,t),s.reject(t),this.pipelineHandlerId=null;continue}this.log(`query from handler #${s.handlerId} completed, ${i} bytes`),this.lastHandlerId=s.handlerId,this.pipelineHandlerId=s.closes?null:s.handlerId,s.resolve(i)}this.processing=!1,this.log("queue processing complete, queue length is",this.queue.length)}}getQueueLength(){return this.queue.length}holdsAffinity(s){return this.pipelineHandlerId===s||this.db.isInTransaction()&&this.lastHandlerId===s}clearQueueForHandler(s){this.dead.add(s);let e=this.queue.length;this.queue=this.queue.filter(t=>t.handlerId===s?(t.reject(new Error("Handler disconnected")),!1):!0);let i=e-this.queue.length;i>0&&this.log(`cleared ${i} queries for handler #${s}`)}async clearPipelineIfNeeded(s){this.pipelineHandlerId===s&&(this.pipelineHandlerId=null,await this.db.runExclusive(async()=>await this.db.execProtocolRawStream(new Uint8Array([83,0,0,0,4]),{onRawData:()=>{}})).catch(()=>{}),this.processQueue())}async clearTransactionIfNeeded(s){this.db.isInTransaction()&&this.lastHandlerId===s&&(await this.db.exec("ROLLBACK"),this.lastHandlerId=null,await this.processQueue())}},l=class l extends EventTarget{constructor(e){super();this.socket=null;this.active=!1;this.messageBuffer=Buffer.alloc(0);this.lastActivityTime=Date.now();this.queryQueue=e.queryQueue,this.closeOnDetach=e.closeOnDetach??!1,this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.idleTimeout=e.idleTimeout??0,this.id=l.nextHandlerId++,this.log("constructor: created new handler")}get handlerId(){return this.id}log(e,...i){this.debug&&console.log(`[PGLiteSocketHandler#${this.id}] ${e}`,...i)}async attach(e){if(this.log(`attach: attaching socket from ${e.remoteAddress}:${e.remotePort}`),this.socket)throw new Error("Socket already attached");return this.socket=e,this.active=!0,this.lastActivityTime=Date.now(),e.setNoDelay(!0),this.idleTimeout>0&&this.resetIdleTimer(),this.log("attach: setting up socket event handlers"),e.on("data",i=>{this.lastActivityTime=Date.now(),this.resetIdleTimer(),setImmediate(async()=>{try{await this.handleData(i)}catch(t){this.log("socket on data error: ",t),this.handleError(t)}})}),e.on("error",i=>{setImmediate(()=>this.handleError(i))}),e.on("close",()=>{setImmediate(()=>this.handleClose())}),this.log("attach: socket handler ready"),this}resetIdleTimer(){this.idleTimeout<=0||(this.idleTimer&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{if(!this.queryQueue.holdsAffinity(this.id)){this.resetIdleTimer();return}let e=Date.now()-this.lastActivityTime;this.log(`idle timeout after ${e}ms`),this.handleError(new Error("Idle timeout"))},this.idleTimeout))}async detach(e){if(this.log(`detach: detaching socket, close=${e??this.closeOnDetach}`),this.idleTimer&&(clearTimeout(this.idleTimer),this.idleTimer=void 0),this.queryQueue.clearQueueForHandler(this.id),await this.queryQueue.clearTransactionIfNeeded(this.id),await this.queryQueue.clearPipelineIfNeeded(this.id),!this.socket)return this.log("detach: no socket attached, nothing to do"),this;if(this.socket.removeAllListeners("data"),this.socket.removeAllListeners("error"),this.socket.removeAllListeners("close"),(e??this.closeOnDetach)&&this.socket.writable){this.log("detach: closing socket");try{this.socket.end(),this.socket.destroy()}catch(i){this.log("detach: error closing socket:",i)}}return this.socket=null,this.active=!1,this.messageBuffer=Buffer.alloc(0),this.log("detach: handler cleaned up"),this}get isAttached(){return this.socket!==null}async handleData(e){if(!this.socket||!this.active)return this.log("handleData: no active socket, ignoring data"),0;this.log(`handleData: received ${e.length} bytes`),this.messageBuffer=Buffer.concat([this.messageBuffer,e]),this.inspectData("incoming",e);try{let i=0;const __frames=[];for(;this.messageBuffer.length>0;){let t=0,r=!1;if(this.messageBuffer.length>=4){let n=this.messageBuffer.readInt32BE(0);if(this.messageBuffer.length>=8){let a=this.messageBuffer.readInt32BE(4);(a===196608||a===196608)&&(t=n,r=this.messageBuffer.length>=t)}!r&&this.messageBuffer.length>=5&&(t=1+this.messageBuffer.readInt32BE(1),r=this.messageBuffer.length>=t)}if(!r||t===0){this.log(`handleData: incomplete message, buffering ${this.messageBuffer.length} bytes`);break}let o=this.messageBuffer.slice(0,t);if(this.messageBuffer=this.messageBuffer.slice(t),this.log(`handleData: processing message of ${o.length} bytes`),!this.active||!this.socket){this.log("handleData: socket no longer active, stopping processing");break}__frames.push(o)}if(__frames.length===0)return this.dispatchEvent(new CustomEvent("data",{detail:{incoming:e.length,outgoing:i}})),i;{let o=__frames.length===1?__frames[0]:Buffer.concat(__frames);const __lastF=__frames[__frames.length-1];const __lt=__lastF[0]>=65?__lastF[0]:null;const __closes=__lt===null||__lt===83||__lt===81||__lt===88;let h;if(await this.queryQueue.enqueue(this.id,new Uint8Array(o),n=>{this.log(`handleData: received ${n.length} bytes from PGlite`),this.inspectData("outgoing",n),n.length>0&&this.socket&&this.socket.writable&&this.active&&(this.log("handleData: writing response to socket"),this.socket?.writable?this.socket.write(Buffer.from(n),a=>{a?(this.log("handleData: error writing to socket:",a),h=a):this.log(`handleData: socket sent: ${n.length} bytes`)}):this.log("handleData: socket no longer writable")),i+=n.length},__closes),h)throw h}return this.dispatchEvent(new CustomEvent("data",{detail:{incoming:e.length,outgoing:i}})),i}catch(i){throw this.log("handleData: error processing data:",i),i}}handleError(e){if(!this.active){this.log("handleError: handler not active, ignoring error");return}e.message?.includes("ECONNRESET")?this.log("handleError: client disconnected (ECONNRESET) - normal behavior"):e.message?.includes("Idle timeout")?this.log("handleError: connection idle timeout"):this.log("handleError:",e),this.active=!1,this.dispatchEvent(new CustomEvent("error",{detail:e})),this.detach(!0).catch(()=>{})}handleClose(){this.log("handleClose: socket closed"),this.active=!1,this.dispatchEvent(new CustomEvent("close")),this.detach(!1).catch(()=>{})}inspectData(e,i){if(this.inspect){console.log("-".repeat(75)),console.log(e==="incoming"?"-> incoming":"<- outgoing",i.length,"bytes");for(let t=0;t=32&&a<=126?String.fromCharCode(a):"."}console.log(`${t.toString(16).padStart(8,"0")} ${o} ${h}`)}}}};l.nextHandlerId=1;var d=l,u=class extends EventTarget{constructor(e){super();this.server=null;this.active=!1;this.handlers=new Set;this.db=e.db,e.path?this.path=e.path:(typeof e.port=="number"?this.port=e.port??e.port:this.port=5432,this.host=e.host||"127.0.0.1"),this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.idleTimeout=e.idleTimeout??0,this.maxConnections=e.maxConnections??1,this.queryQueue=new c(this.db,this.debug),this.log(`constructor: created server on ${this.getServerConn()}`),this.log(`constructor: max connections: ${this.maxConnections}`),this.idleTimeout>0&&this.log(`constructor: idle timeout: ${this.idleTimeout}ms`)}log(e,...i){this.debug&&console.log(`[PGLiteSocketServer] ${e}`,...i)}async start(){if(this.log(`start: starting server on ${this.getServerConn()}`),this.server)throw new Error("Socket server already started");return await this.db.waitReady,this.active=!0,this.server=m(e=>{setImmediate(()=>this.handleConnection(e))}),this.server.maxConnections=this.maxConnections,new Promise((e,i)=>{if(!this.server)return i(new Error("Server not initialized"));if(this.server.on("error",t=>{this.log("start: server error:",t),this.dispatchEvent(new CustomEvent("error",{detail:t})),this.active||i(t)}),this.path)this.server.listen(this.path,()=>{this.log(`start: server listening on ${this.getServerConn()}`),this.dispatchEvent(new CustomEvent("listening",{detail:{path:this.path}})),e()});else{let t=this.server;t.listen(this.port,this.host,()=>{let r=t.address();if(r===null||typeof r!="object")throw Error("Expected address info");this.port=r.port,this.log(`start: server listening on ${this.getServerConn()}`),this.dispatchEvent(new CustomEvent("listening",{detail:{port:this.port,host:this.host}})),e()})}})}getServerConn(){return this.path?this.path:`${this.host}:${this.port}`}async stop(){this.log("stop: stopping server"),this.active=!1,this.log(`stop: detaching ${this.handlers.size} handlers`);for(let e of this.handlers)e.detach(!0).catch(()=>{});return this.handlers.clear(),this.server?new Promise(e=>{if(!this.server)return e();this.server.close(()=>{this.log("stop: server closed"),this.server=null,this.dispatchEvent(new CustomEvent("close")),e()})}):(this.log("stop: server not running, nothing to do"),Promise.resolve())}async handleConnection(e){let i={clientAddress:e.remoteAddress||"unknown",clientPort:e.remotePort||0};if(this.log(`handleConnection: new connection from ${i.clientAddress}:${i.clientPort}`),this.log(`handleConnection: active connections: ${this.handlers.size}, queued queries: ${this.queryQueue.getQueueLength()}`),!this.active){this.log("handleConnection: server not active, closing connection");try{e.end()}catch(r){this.log("handleConnection: error closing socket:",r)}return}if(this.handlers.size>=this.maxConnections){this.log("handleConnection: max connections reached, rejecting"),e.write(Buffer.from(`Too many connections ++import{createServer as m}from"net";var b=6e4,c=class{constructor(s,e=!1){this.queue=[];this.processing=!1;this.lastHandlerId=null;this.pipelineHandlerId=null;this.dead=new Set();this.db=s,this.debug=e}log(s,...e){this.debug&&console.log(`[QueryQueueManager] ${s}`,...e)}async enqueue(s,e,i,S=!0){return new Promise((t,r)=>{let o={handlerId:s,message:e,resolve:t,reject:r,timestamp:Date.now(),onData:i,closes:S};this.queue.push(o),this.log(`enqueued query from handler #${s}, queue size: ${this.queue.length}`),this.processing||this.processQueue()})}async processQueue(){if(!(this.processing||this.queue.length===0)){for(this.processing=!0;this.queue.length>0;){let s;let __affine=this.db.isInTransaction()&&this.lastHandlerId?this.lastHandlerId:this.pipelineHandlerId;if(__affine&&this.dead.has(__affine)){this.log("affinity held by detached handler, recovering",__affine);if(this.db.isInTransaction()&&this.lastHandlerId===__affine){await this.db.exec("ROLLBACK").catch(()=>{});this.lastHandlerId=null}if(this.pipelineHandlerId===__affine){this.pipelineHandlerId=null;await this.db.runExclusive(async()=>await this.db.execProtocolRawStream(new Uint8Array([83,0,0,0,4]),{onRawData:()=>{}})).catch(()=>{})}continue}if(__affine){let t=this.queue.findIndex(r=>r.handlerId===__affine);t===-1?(this.log("affinity held, waiting for handler",__affine),s=null):s=this.queue.splice(t,1)[0]}else s=this.queue.shift();if(!s)break;let e=Date.now()-s.timestamp;this.log(`processing query from handler #${s.handlerId} (waited ${e}ms)`);let i=0;try{await this.db.runExclusive(async()=>await this.db.execProtocolRawStream(s.message,{onRawData:t=>{i+=t.length,s.onData(t)}}))}catch(t){this.log(`query from handler #${s.handlerId} failed:`,t),s.reject(t),this.pipelineHandlerId=null;continue}this.log(`query from handler #${s.handlerId} completed, ${i} bytes`),this.lastHandlerId=s.handlerId,this.pipelineHandlerId=s.closes?null:s.handlerId,s.resolve(i)}this.processing=!1,this.log("queue processing complete, queue length is",this.queue.length)}}getQueueLength(){return this.queue.length}holdsAffinity(s){return this.pipelineHandlerId===s||this.db.isInTransaction()&&this.lastHandlerId===s}clearQueueForHandler(s){this.dead.add(s);let e=this.queue.length;this.queue=this.queue.filter(t=>t.handlerId===s?(t.reject(new Error("Handler disconnected")),!1):!0);let i=e-this.queue.length;i>0&&this.log(`cleared ${i} queries for handler #${s}`)}async clearPipelineIfNeeded(s){this.pipelineHandlerId===s&&(this.pipelineHandlerId=null,await this.db.runExclusive(async()=>await this.db.execProtocolRawStream(new Uint8Array([83,0,0,0,4]),{onRawData:()=>{}})).catch(()=>{}),this.processQueue())}async clearTransactionIfNeeded(s){this.db.isInTransaction()&&this.lastHandlerId===s&&(await this.db.exec("ROLLBACK"),this.lastHandlerId=null,await this.processQueue())}},l=class l extends EventTarget{constructor(e){super();this.socket=null;this.active=!1;this.messageBuffer=Buffer.alloc(0);this.lastActivityTime=Date.now();this.queryQueue=e.queryQueue,this.closeOnDetach=e.closeOnDetach??!1,this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.idleTimeout=e.idleTimeout??0,this.id=l.nextHandlerId++,this.log("constructor: created new handler")}get handlerId(){return this.id}log(e,...i){this.debug&&console.log(`[PGLiteSocketHandler#${this.id}] ${e}`,...i)}async attach(e){if(this.log(`attach: attaching socket from ${e.remoteAddress}:${e.remotePort}`),this.socket)throw new Error("Socket already attached");return this.socket=e,this.active=!0,this.lastActivityTime=Date.now(),e.setNoDelay(!0),this.idleTimeout>0&&this.resetIdleTimer(),this.log("attach: setting up socket event handlers"),e.on("data",i=>{this.lastActivityTime=Date.now(),this.resetIdleTimer(),setImmediate(async()=>{try{await this.handleData(i)}catch(t){this.log("socket on data error: ",t),this.handleError(t)}})}),e.on("error",i=>{setImmediate(()=>this.handleError(i))}),e.on("close",()=>{setImmediate(()=>this.handleClose())}),this.log("attach: socket handler ready"),this}resetIdleTimer(){this.idleTimeout<=0||(this.idleTimer&&clearTimeout(this.idleTimer),this.idleTimer=setTimeout(()=>{if(!this.queryQueue.holdsAffinity(this.id)){this.resetIdleTimer();return}let e=Date.now()-this.lastActivityTime;this.log(`idle timeout after ${e}ms`),this.handleError(new Error("Idle timeout"))},this.idleTimeout))}async detach(e){if(this.log(`detach: detaching socket, close=${e??this.closeOnDetach}`),this.idleTimer&&(clearTimeout(this.idleTimer),this.idleTimer=void 0),this.queryQueue.clearQueueForHandler(this.id),await this.queryQueue.clearTransactionIfNeeded(this.id),await this.queryQueue.clearPipelineIfNeeded(this.id),!this.socket)return this.log("detach: no socket attached, nothing to do"),this;if(this.socket.removeAllListeners("data"),this.socket.removeAllListeners("error"),this.socket.removeAllListeners("close"),(e??this.closeOnDetach)&&this.socket.writable){this.log("detach: closing socket");try{this.socket.end(),this.socket.destroy()}catch(i){this.log("detach: error closing socket:",i)}}return this.socket=null,this.active=!1,this.messageBuffer=Buffer.alloc(0),this.log("detach: handler cleaned up"),this}get isAttached(){return this.socket!==null}async handleData(e){if(!this.socket||!this.active)return this.log("handleData: no active socket, ignoring data"),0;this.log(`handleData: received ${e.length} bytes`),this.messageBuffer=Buffer.concat([this.messageBuffer,e]),this.inspectData("incoming",e);try{let i=0;const __frames=[];for(;this.messageBuffer.length>0;){let t=0,r=!1;if(this.messageBuffer.length>=4){let n=this.messageBuffer.readInt32BE(0);if(this.messageBuffer.length>=8){let a=this.messageBuffer.readInt32BE(4);(a===196608||a===196608)&&(t=n,r=this.messageBuffer.length>=t)}!r&&this.messageBuffer.length>=5&&(t=1+this.messageBuffer.readInt32BE(1),r=this.messageBuffer.length>=t)}if(!r||t===0){this.log(`handleData: incomplete message, buffering ${this.messageBuffer.length} bytes`);break}let o=this.messageBuffer.slice(0,t);if(this.messageBuffer=this.messageBuffer.slice(t),this.log(`handleData: processing message of ${o.length} bytes`),!this.active||!this.socket){this.log("handleData: socket no longer active, stopping processing");break}__frames.push(o)}if(__frames.length===0)return this.dispatchEvent(new CustomEvent("data",{detail:{incoming:e.length,outgoing:i}})),i;{let o=__frames.length===1?__frames[0]:Buffer.concat(__frames);const __lastF=__frames[__frames.length-1];const __lt=__lastF[0]>=65?__lastF[0]:null;const __closes=__lt===null||__lt===83||__lt===81||__lt===88;let h;if(await this.queryQueue.enqueue(this.id,new Uint8Array(o),n=>{this.log(`handleData: received ${n.length} bytes from PGlite`),this.inspectData("outgoing",n),n.length>0&&this.socket&&this.socket.writable&&this.active&&(this.log("handleData: writing response to socket"),this.socket?.writable?this.socket.write(Buffer.from(n),a=>{a?(this.log("handleData: error writing to socket:",a),h=a):this.log(`handleData: socket sent: ${n.length} bytes`)}):this.log("handleData: socket no longer writable")),i+=n.length},__closes),h)throw h}return this.dispatchEvent(new CustomEvent("data",{detail:{incoming:e.length,outgoing:i}})),i}catch(i){throw this.log("handleData: error processing data:",i),i}}handleError(e){if(!this.active){this.log("handleError: handler not active, ignoring error");return}e.message?.includes("ECONNRESET")?this.log("handleError: client disconnected (ECONNRESET) - normal behavior"):e.message?.includes("Idle timeout")?this.log("handleError: connection idle timeout"):this.log("handleError:",e),this.active=!1,this.dispatchEvent(new CustomEvent("error",{detail:e})),this.detach(!0).catch(()=>{})}handleClose(){this.log("handleClose: socket closed"),this.active=!1,this.dispatchEvent(new CustomEvent("close")),this.detach(!1).catch(()=>{})}inspectData(e,i){if(this.inspect){console.log("-".repeat(75)),console.log(e==="incoming"?"-> incoming":"<- outgoing",i.length,"bytes");for(let t=0;t=32&&a<=126?String.fromCharCode(a):"."}console.log(`${t.toString(16).padStart(8,"0")} ${o} ${h}`)}}}};l.nextHandlerId=1;var d=l,u=class extends EventTarget{constructor(e){super();this.server=null;this.active=!1;this.handlers=new Set;this.db=e.db,e.path?this.path=e.path:(typeof e.port=="number"?this.port=e.port??e.port:this.port=5432,this.host=e.host||"127.0.0.1"),this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.idleTimeout=e.idleTimeout??0,this.maxConnections=e.maxConnections??1,this.queryQueue=new c(this.db,this.debug),this.log(`constructor: created server on ${this.getServerConn()}`),this.log(`constructor: max connections: ${this.maxConnections}`),this.idleTimeout>0&&this.log(`constructor: idle timeout: ${this.idleTimeout}ms`)}log(e,...i){this.debug&&console.log(`[PGLiteSocketServer] ${e}`,...i)}async start(){if(this.log(`start: starting server on ${this.getServerConn()}`),this.server)throw new Error("Socket server already started");return await this.db.waitReady,this.active=!0,this.server=m(e=>{setImmediate(()=>this.handleConnection(e))}),this.server.maxConnections=this.maxConnections,new Promise((e,i)=>{if(!this.server)return i(new Error("Server not initialized"));if(this.server.on("error",t=>{this.log("start: server error:",t),this.dispatchEvent(new CustomEvent("error",{detail:t})),i(t)}),this.path)this.server.listen(this.path,()=>{this.log(`start: server listening on ${this.getServerConn()}`),this.dispatchEvent(new CustomEvent("listening",{detail:{path:this.path}})),e()});else{let t=this.server;t.listen(this.port,this.host,()=>{let r=t.address();if(r===null||typeof r!="object")throw Error("Expected address info");this.port=r.port,this.log(`start: server listening on ${this.getServerConn()}`),this.dispatchEvent(new CustomEvent("listening",{detail:{port:this.port,host:this.host}})),e()})}})}getServerConn(){return this.path?this.path:`${this.host}:${this.port}`}async stop(){this.log("stop: stopping server"),this.active=!1,this.log(`stop: detaching ${this.handlers.size} handlers`);for(let e of this.handlers)e.detach(!0).catch(()=>{});return this.handlers.clear(),this.server?new Promise(e=>{if(!this.server)return e();this.server.close(()=>{this.log("stop: server closed"),this.server=null,this.dispatchEvent(new CustomEvent("close")),e()})}):(this.log("stop: server not running, nothing to do"),Promise.resolve())}async handleConnection(e){let i={clientAddress:e.remoteAddress||"unknown",clientPort:e.remotePort||0};if(this.log(`handleConnection: new connection from ${i.clientAddress}:${i.clientPort}`),this.log(`handleConnection: active connections: ${this.handlers.size}, queued queries: ${this.queryQueue.getQueueLength()}`),!this.active){this.log("handleConnection: server not active, closing connection");try{e.end()}catch(r){this.log("handleConnection: error closing socket:",r)}return}if(this.handlers.size>=this.maxConnections){this.log("handleConnection: max connections reached, rejecting"),e.write(Buffer.from(`Too many connections +`)),e.end();return}let t=new d({queryQueue:this.queryQueue,closeOnDetach:!0,inspect:this.inspect,debug:this.debug,idleTimeout:this.idleTimeout});this.handlers.add(t),t.addEventListener("error",r=>{let o=r.detail;o?.message?.includes("ECONNRESET")?this.log(`handler #${t.handlerId}: client disconnected (ECONNRESET)`):o?.message?.includes("Idle timeout")?this.log(`handler #${t.handlerId}: idle timeout`):this.log(`handler #${t.handlerId}: error:`,o);this.handlers.delete(t)}),t.addEventListener("close",()=>{this.log(`handler #${t.handlerId}: closed`),this.handlers.delete(t),this.log(`handleConnection: active connections: ${this.handlers.size}`)});try{await t.attach(e),this.dispatchEvent(new CustomEvent("connection",{detail:i}))}catch(r){this.log("handleConnection: error attaching socket:",r),this.handlers.delete(t),this.dispatchEvent(new CustomEvent("error",{detail:r}));try{e.end()}catch(o){this.log("handleConnection: error closing socket:",o)}}}getStats(){return{activeConnections:this.handlers.size,queuedQueries:this.queryQueue.getQueueLength(),maxConnections:this.maxConnections}}};export{b as a,d as b,u as c}; //# sourceMappingURL=chunk-NSUMFCRM.js.map \ No newline at end of file