UNPKG

@electric-sql/pglite-socket

Version:

A socket implementation for PGlite enabling remote connections

16 lines (15 loc) 14.5 kB
#!/usr/bin/env node "use strict";var b=require("@electric-sql/pglite");var m=require("net"),k=6e4,u=class u extends EventTarget{constructor(e){super();this.socket=null;this.active=!1;this.db=e.db,this.closeOnDetach=e.closeOnDetach??!1,this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.id=u.nextHandlerId++,this.log("constructor: created new handler")}get handlerId(){return this.id}log(e,...t){this.debug&&console.log(`[PGLiteSocketHandler#${this.id}] ${e}`,...t)}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.log("attach: waiting for PGlite to be ready"),await this.db.waitReady,this.log("attach: acquiring exclusive lock on PGlite instance"),await new Promise(t=>{this.db.runExclusive(()=>(t(),new Promise((i,n)=>{this.resolveLock=i,this.rejectLock=n})))}),this.log("attach: setting up socket event handlers"),e.on("data",async t=>{try{let i=await this.handleData(t);this.log(`socket on data sent: ${i} bytes`)}catch(i){this.log("socket on data error: ",i)}}),e.on("error",t=>this.handleError(t)),e.on("close",()=>this.handleClose()),this}detach(e){return this.log(`detach: detaching socket, close=${e??this.closeOnDetach}`),this.socket?(this.socket.removeAllListeners("data"),this.socket.removeAllListeners("error"),this.socket.removeAllListeners("close"),(e??this.closeOnDetach)&&this.socket.writable&&(this.log("detach: closing socket"),this.socket.end(),this.socket.destroy()),this.log("detach: releasing exclusive lock on PGlite instance"),this.resolveLock?.(),this.socket=null,this.active=!1,this):(this.log("detach: no socket attached, nothing to do"),this)}get isAttached(){return this.socket!==null}async handleData(e){if(!this.socket||!this.active)return this.log("handleData: no active socket, ignoring data"),new Promise((t,i)=>i("no active socket"));this.log(`handleData: received ${e.length} bytes`),this.inspectData("incoming",e);try{this.log("handleData: sending data to PGlite for processing");let t=await this.db.execProtocolRaw(new Uint8Array(e));if(this.log(`handleData: received ${t.length} bytes from PGlite`),this.inspectData("outgoing",t),this.socket&&this.socket.writable&&this.active){if(t.length<=0)return this.log("handleData: cowardly refusing to send empty packet"),new Promise((n,o)=>o("no data"));let i=new Promise((n,o)=>{this.log("handleData: writing response to socket"),this.socket?this.socket.write(Buffer.from(t),c=>{c?o(`Error while writing to the socket ${c.toString()}`):n(t.length)}):o("No socket")});return this.dispatchEvent(new CustomEvent("data",{detail:{incoming:e.length,outgoing:t.length}})),i}else return this.log("handleData: socket no longer writable or active, discarding response"),new Promise((i,n)=>n("No socket, not active or not writeable"))}catch(t){return this.log("handleData: error processing data:",t),this.handleError(t),new Promise((i,n)=>n(`Error while processing data ${t.toString()}`))}}handleError(e){this.log("handleError:",e),this.dispatchEvent(new CustomEvent("error",{detail:e})),this.log("handleError: rejecting exclusive lock on PGlite instance"),this.rejectLock?.(e),this.resolveLock=void 0,this.rejectLock=void 0,this.detach(!0)}handleClose(){this.log("handleClose: socket closed"),this.dispatchEvent(new CustomEvent("close")),this.detach(!1)}inspectData(e,t){if(this.inspect){console.log("-".repeat(75)),console.log(e==="incoming"?"-> incoming":"<- outgoing",t.length,"bytes");for(let i=0;i<t.length;i+=16){let n=Math.min(16,t.length-i),o="";for(let a=0;a<16;a++)if(a<n){let h=t[i+a];o+=h.toString(16).padStart(2,"0")+" "}else o+=" ";let c="";for(let a=0;a<n;a++){let h=t[i+a];c+=h>=32&&h<=126?String.fromCharCode(h):"."}console.log(`${i.toString(16).padStart(8,"0")} ${o} ${c}`)}}}};u.nextHandlerId=1;var v=u,d=class extends EventTarget{constructor(e){super();this.server=null;this.active=!1;this.activeHandler=null;this.connectionQueue=[];this.handlerCount=0;this.db=e.db,e.path?this.path=e.path:(this.port=e.port||5432,this.host=e.host||"127.0.0.1"),this.inspect=e.inspect??!1,this.debug=e.debug??!1,this.connectionQueueTimeout=e.connectionQueueTimeout??k,this.log(`constructor: created server on ${this.host}:${this.port}`),this.log(`constructor: connection queue timeout: ${this.connectionQueueTimeout}ms`)}log(e,...t){this.debug&&console.log(`[PGLiteSocketServer] ${e}`,...t)}async start(){if(this.log(`start: starting server on ${this.getServerConn()}`),this.server)throw new Error("Socket server already started");return this.active=!0,this.server=(0,m.createServer)(e=>this.handleConnection(e)),new Promise((e,t)=>{if(!this.server)return t(new Error("Server not initialized"));this.server.on("error",i=>{this.log("start: server error:",i),this.dispatchEvent(new CustomEvent("error",{detail:i})),t(i)}),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()}):this.server.listen(this.port,this.host,()=>{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(){return this.log("stop: stopping server"),this.active=!1,this.log(`stop: clearing connection queue (${this.connectionQueue.length} connections)`),this.connectionQueue.forEach(e=>{clearTimeout(e.timeoutId),e.socket.writable&&(this.log(`stop: closing queued connection from ${e.clientInfo.clientAddress}:${e.clientInfo.clientPort}`),e.socket.end())}),this.connectionQueue=[],this.activeHandler&&(this.log(`stop: detaching active handler #${this.activeHandlerId}`),this.activeHandler.detach(!0),this.activeHandler=null),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())}get activeHandlerId(){return this.activeHandler?.handlerId??null}async handleConnection(e){let t={clientAddress:e.remoteAddress||"unknown",clientPort:e.remotePort||0};if(this.log(`handleConnection: new connection from ${t.clientAddress}:${t.clientPort}`),!this.active){this.log("handleConnection: server not active, closing connection"),e.end();return}if(!this.activeHandler||!this.activeHandler.isAttached){this.log("handleConnection: no active handler, attaching socket directly"),this.dispatchEvent(new CustomEvent("connection",{detail:t})),await this.attachSocketToNewHandler(e,t);return}this.log(`handleConnection: active handler #${this.activeHandlerId} exists, queueing connection`),this.enqueueConnection(e,t)}enqueueConnection(e,t){this.log(`enqueueConnection: queueing connection from ${t.clientAddress}:${t.clientPort}, timeout: ${this.connectionQueueTimeout}ms`);let i=setTimeout(()=>{this.log(`enqueueConnection: timeout for connection from ${t.clientAddress}:${t.clientPort}`),this.connectionQueue=this.connectionQueue.filter(n=>n.socket!==e),e.writable&&(this.log("enqueueConnection: closing timed out connection"),e.end()),this.dispatchEvent(new CustomEvent("queueTimeout",{detail:{...t,queueSize:this.connectionQueue.length}}))},this.connectionQueueTimeout);this.connectionQueue.push({socket:e,clientInfo:t,timeoutId:i}),this.log(`enqueueConnection: connection queued, queue size: ${this.connectionQueue.length}`),this.dispatchEvent(new CustomEvent("queuedConnection",{detail:{...t,queueSize:this.connectionQueue.length}}))}processNextInQueue(){if(this.log(`processNextInQueue: processing next connection, queue size: ${this.connectionQueue.length}`),this.connectionQueue.length===0||!this.active){this.log("processNextInQueue: no connections in queue or server not active, nothing to do");return}let e=this.connectionQueue.shift();if(e){if(this.log(`processNextInQueue: processing connection from ${e.clientInfo.clientAddress}:${e.clientInfo.clientPort}`),clearTimeout(e.timeoutId),!e.socket.writable){this.log("processNextInQueue: socket no longer writable, skipping to next connection"),this.processNextInQueue();return}this.attachSocketToNewHandler(e.socket,e.clientInfo).catch(t=>{this.log("processNextInQueue: error attaching socket:",t),this.dispatchEvent(new CustomEvent("error",{detail:t})),this.processNextInQueue()})}}async attachSocketToNewHandler(e,t){this.handlerCount++,this.log(`attachSocketToNewHandler: creating new handler for ${t.clientAddress}:${t.clientPort} (handler #${this.handlerCount})`);let i=new v({db:this.db,closeOnDetach:!0,inspect:this.inspect,debug:this.debug});i.addEventListener("error",n=>{this.log(`handler #${i.handlerId}: error from handler:`,n.detail),this.dispatchEvent(new CustomEvent("error",{detail:n.detail}))}),i.addEventListener("close",()=>{this.log(`handler #${i.handlerId}: closed`),this.activeHandler===i&&(this.log(`handler #${i.handlerId}: was active handler, processing next connection in queue`),this.activeHandler=null,this.processNextInQueue())});try{this.activeHandler=i,this.log(`handler #${i.handlerId}: attaching socket`),await i.attach(e),this.dispatchEvent(new CustomEvent("connection",{detail:t}))}catch(n){throw this.log(`handler #${i.handlerId}: error attaching socket:`,n),this.activeHandler=null,e.writable&&e.end(),n}}};var f=require("util"),w=require("child_process"),r=(0,f.parseArgs)({options:{db:{type:"string",short:"d",default:"memory://",help:"Database path (relative or absolute). Use memory:// for in-memory database."},port:{type:"string",short:"p",default:"5432",help:"Port to listen on"},host:{type:"string",short:"h",default:"127.0.0.1",help:"Host to bind to"},path:{type:"string",short:"u",default:void 0,help:"unix socket to bind to. Takes precedence over host:port"},debug:{type:"string",short:"v",default:"0",help:"Debug level (0-5)"},run:{type:"string",short:"r",default:void 0,help:"Command to run after server starts"},"include-database-url":{type:"boolean",default:!1,help:"Include DATABASE_URL in the environment of the subprocess"},"shutdown-timeout":{type:"string",default:"5000",help:"Timeout in milliseconds for graceful subprocess shutdown (default: 5000)"},help:{type:"boolean",short:"?",default:!1,help:"Show help"}}}),E=`PGlite Socket Server Usage: pglite-server [options] Options: -d, --db=PATH Database path (default: memory://) -p, --port=PORT Port to listen on (default: 5432) -h, --host=HOST Host to bind to (default: 127.0.0.1) -u, --path=UNIX Unix socket to bind to (default: undefined). Takes precedence over host:port -v, --debug=LEVEL Debug level 0-5 (default: 0) -r, --run=COMMAND Command to run after server starts --include-database-url Include DATABASE_URL in subprocess environment --shutdown-timeout=MS Timeout for graceful subprocess shutdown in ms (default: 5000) `,g=class{constructor(s){this.db=null;this.server=null;this.subprocessManager=null;this.config=s}static parseConfig(){return{dbPath:r.values.db,port:parseInt(r.values.port,10),host:r.values.host,path:r.values.path,debugLevel:parseInt(r.values.debug,10),runCommand:r.values.run,includeDatabaseUrl:r.values["include-database-url"],shutdownTimeout:parseInt(r.values["shutdown-timeout"],10)}}createDatabaseUrl(){let{host:s,port:e,path:t}=this.config;if(t){let i=t.endsWith("/.s.PGSQL.5432")?t.slice(0,-13):t;return`postgresql://postgres:postgres@/postgres?host=${encodeURIComponent(i)}`}else return`postgresql://postgres:postgres@${s}:${e}/postgres`}async initializeDatabase(){console.log(`Initializing PGLite with database: ${this.config.dbPath}`),console.log(`Debug level: ${this.config.debugLevel}`),this.db=new b.PGlite(this.config.dbPath,{debug:this.config.debugLevel}),await this.db.waitReady,console.log("PGlite database initialized")}setupServerEventHandlers(){if(!this.server||!this.subprocessManager)throw new Error("Server or subprocess manager not initialized");this.server.addEventListener("listening",s=>{let e=s.detail;if(console.log(`PGLiteSocketServer listening on ${JSON.stringify(e)}`),this.config.runCommand&&this.subprocessManager){let t=this.createDatabaseUrl();this.subprocessManager.spawn(this.config.runCommand,t,this.config.includeDatabaseUrl)}}),this.server.addEventListener("connection",s=>{let{clientAddress:e,clientPort:t}=s.detail;console.log(`Client connected from ${e}:${t}`)}),this.server.addEventListener("error",s=>{let e=s.detail;console.error("Socket server error:",e)})}setupSignalHandlers(){process.on("SIGINT",()=>this.shutdown()),process.on("SIGTERM",()=>this.shutdown())}async start(){try{if(await this.initializeDatabase(),!this.db)throw new Error("Database initialization failed");this.server=new d({db:this.db,port:this.config.port,host:this.config.host,path:this.config.path,inspect:this.config.debugLevel>0}),this.subprocessManager=new p(s=>{this.shutdown(s)}),this.setupServerEventHandlers(),this.setupSignalHandlers(),await this.server.start()}catch(s){throw console.error("Failed to start PGLiteSocketServer:",s),s}}async shutdown(s=0){console.log(` Shutting down PGLiteSocketServer...`),this.subprocessManager&&this.subprocessManager.terminate(this.config.shutdownTimeout),this.server&&await this.server.stop(),this.db&&await this.db.close(),console.log("Server stopped"),process.exit(s)}},p=class{constructor(s){this.childProcess=null;this.onExit=s}get process(){return this.childProcess}spawn(s,e,t){console.log(`Running command: ${s}`);let i={...process.env};t&&(i.DATABASE_URL=e,console.log(`Setting DATABASE_URL=${e}`));let n=s.trim().split(/\s+/);this.childProcess=(0,w.spawn)(n[0],n.slice(1),{env:i,stdio:"inherit"}),this.childProcess.on("error",o=>{console.error("Error running command:",o),console.log("Subprocess failed to start, shutting down..."),this.onExit(1)}),this.childProcess.on("close",o=>{console.log(`Command exited with code ${o}`),this.childProcess=null,o!==null&&o!==0&&(console.log(`Child process failed with exit code ${o}, shutting down...`),this.onExit(o))})}terminate(s){this.childProcess&&(console.log("Terminating child process..."),this.childProcess.kill("SIGTERM"),setTimeout(()=>{this.childProcess&&!this.childProcess.killed&&(console.log("Force killing child process..."),this.childProcess.kill("SIGKILL"))},s))}};async function S(){r.values.help&&(console.log(E),process.exit(0));try{let l=g.parseConfig();await new g(l).start()}catch(l){console.error("Unhandled error:",l),process.exit(1)}}S(); //# sourceMappingURL=server.cjs.map