>4&7;if(void 0===u[f])throw new Error("invalid block size");for(i&&(h+=8),h++;;){var p;if(p=s.readU32(e,h),h+=4,0===p)break;if(r&&(h+=4),0!=(p&l)){p&=2147483647;for(var m=0;m>8,l++;var h=u[7],d=e.length,f=0;for(function(e){for(var t=0;t0;){var p,m=d>h?h:d;if((p=t.compressBlock(e,i,f,m,a))>m||0===p){s.writeU32(n,l,2147483648|m),l+=4;for(var g=f+m;f{t.hashU32=function(e){return-1252372727^(e=(e=(e=374761393+(e=-949894596^(e=2127912214+(e|=0)+(e<<12)|0)^e>>>19)+(e<<5)|0)-744332180^e<<9)-42973499+(e<<3)|0)^e>>>16|0},t.readU64=function(e,t){var n=0;return n|=e[t++]<<0,n|=e[t++]<<8,n|=e[t++]<<16,n|=e[t++]<<24,n|=e[t++]<<32,n|=e[t++]<<40,(n|=e[t++]<<48)|e[t++]<<56},t.readU32=function(e,t){var n=0;return n|=e[t++]<<0,n|=e[t++]<<8,(n|=e[t++]<<16)|e[t++]<<24},t.writeU32=function(e,t,n){e[t++]=n>>0&255,e[t++]=n>>8&255,e[t++]=n>>16&255,e[t++]=n>>24&255},t.imul=function(e,t){var n=65535&e,r=65535&t;return n*r+((e>>>16)*r+n*(t>>>16)<<16)|0}},887:(e,t,n)=>{var r=n(325),s=2654435761,o=2246822519,i=3266489917,a=374761393;function c(e,t){return(e|=0)>>>(32-(t|=0)|0)|e<>>(32-t|0)|e<>>(t|=0)^e|0}function h(e,t,n,s,o){return l(r.imul(t,n)+e,s,o)}function d(e,t,n){return l(e+r.imul(t[n],a),11,s)}function f(e,t,n){return h(e,r.readU32(t,n),i,17,668265263)}function p(e,t,n){return[h(e[0],r.readU32(t,n+0),o,13,s),h(e[1],r.readU32(t,n+4),o,13,s),h(e[2],r.readU32(t,n+8),o,13,s),h(e[3],r.readU32(t,n+12),o,13,s)]}t.hash=function(e,t,n,l){var h,m;if(m=l,l>=16){for(h=[e+s+o,e+o,e,e-s];l>=16;)h=p(h,t,n),n+=16,l-=16;h=c(h[0],1)+c(h[1],7)+c(h[2],12)+c(h[3],18)+m}else h=e+a+l>>>0;for(;l>=4;)h=f(h,t,n),n+=4,l-=4;for(;l>0;)h=d(h,t,n),n++,l--;return(h=u(r.imul(u(r.imul(u(h,15),o),13),i),16))>>>0}},824:e=>{var t=1e3,n=60*t,r=60*n,s=24*r;function o(e,t,n,r){var s=t>=1.5*n;return Math.round(e/n)+" "+r+(s?"s":"")}e.exports=function(e,i){i=i||{};var a,c,l=typeof e;if("string"===l&&e.length>0)return function(e){if(!((e=String(e)).length>100)){var o=/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(e);if(o){var i=parseFloat(o[1]);switch((o[2]||"ms").toLowerCase()){case"years":case"year":case"yrs":case"yr":case"y":return 315576e5*i;case"weeks":case"week":case"w":return 6048e5*i;case"days":case"day":case"d":return i*s;case"hours":case"hour":case"hrs":case"hr":case"h":return i*r;case"minutes":case"minute":case"mins":case"min":case"m":return i*n;case"seconds":case"second":case"secs":case"sec":case"s":return i*t;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return i;default:return}}}}(e);if("number"===l&&isFinite(e))return i.long?(a=e,(c=Math.abs(a))>=s?o(a,c,s,"day"):c>=r?o(a,c,r,"hour"):c>=n?o(a,c,n,"minute"):c>=t?o(a,c,t,"second"):a+" ms"):function(e){var o=Math.abs(e);return o>=s?Math.round(e/s)+"d":o>=r?Math.round(e/r)+"h":o>=n?Math.round(e/n)+"m":o>=t?Math.round(e/t)+"s":e+"ms"}(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},761:()=>{},59:()=>{},419:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.hasCORS=void 0;let n=!1;try{n="undefined"!=typeof XMLHttpRequest&&"withCredentials"in new XMLHttpRequest}catch(e){}t.hasCORS=n},754:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.decode=t.encode=void 0,t.encode=function(e){let t="";for(let n in e)e.hasOwnProperty(n)&&(t.length&&(t+="&"),t+=encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t},t.decode=function(e){let t={},n=e.split("&");for(let e=0,r=n.length;e{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.parse=void 0;const n=/^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.parse=function(e){if(e.length>2e3)throw"URI too long";const t=e,s=e.indexOf("["),o=e.indexOf("]");-1!=s&&-1!=o&&(e=e.substring(0,s)+e.substring(s,o).replace(/:/g,";")+e.substring(o,e.length));let i=n.exec(e||""),a={},c=14;for(;c--;)a[r[c]]=i[c]||"";return-1!=s&&-1!=o&&(a.source=t,a.host=a.host.substring(1,a.host.length-1).replace(/;/g,":"),a.authority=a.authority.replace("[","").replace("]","").replace(/;/g,":"),a.ipv6uri=!0),a.pathNames=function(e,t){const n=t.replace(/\/{2,9}/g,"/").split("/");return"/"!=t.slice(0,1)&&0!==t.length||n.splice(0,1),"/"==t.slice(-1)&&n.splice(n.length-1,1),n}(0,a.path),a.queryKey=function(e,t){const n={};return t.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(e,t,r){t&&(n[t]=r)})),n}(0,a.query),a}},726:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.yeast=t.decode=t.encode=void 0;const n="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_".split(""),r={};let s,o=0,i=0;function a(e){let t="";do{t=n[e%64]+t,e=Math.floor(e/64)}while(e>0);return t}for(t.encode=a,t.decode=function(e){let t=0;for(i=0;i{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.globalThisShim=void 0,t.globalThisShim="undefined"!=typeof self?self:"undefined"!=typeof window?window:Function("return this")()},679:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.nextTick=t.parse=t.installTimerFunctions=t.transports=t.TransportError=t.Transport=t.protocol=t.Socket=void 0;const r=n(481);Object.defineProperty(t,"Socket",{enumerable:!0,get:function(){return r.Socket}}),t.protocol=r.Socket.protocol;var s=n(870);Object.defineProperty(t,"Transport",{enumerable:!0,get:function(){return s.Transport}}),Object.defineProperty(t,"TransportError",{enumerable:!0,get:function(){return s.TransportError}});var o=n(385);Object.defineProperty(t,"transports",{enumerable:!0,get:function(){return o.transports}});var i=n(622);Object.defineProperty(t,"installTimerFunctions",{enumerable:!0,get:function(){return i.installTimerFunctions}});var a=n(222);Object.defineProperty(t,"parse",{enumerable:!0,get:function(){return a.parse}});var c=n(552);Object.defineProperty(t,"nextTick",{enumerable:!0,get:function(){return c.nextTick}})},481:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Socket=void 0;const s=n(385),o=n(622),i=n(754),a=n(222),c=r(n(227)),l=n(260),u=n(373),h=n(552),d=(0,c.default)("engine.io-client:socket");class f extends l.Emitter{constructor(e,t={}){super(),this.binaryType=h.defaultBinaryType,this.writeBuffer=[],e&&"object"==typeof e&&(t=e,e=null),e?(e=(0,a.parse)(e),t.hostname=e.host,t.secure="https"===e.protocol||"wss"===e.protocol,t.port=e.port,e.query&&(t.query=e.query)):t.host&&(t.hostname=(0,a.parse)(t.host).host),(0,o.installTimerFunctions)(this,t),this.secure=null!=t.secure?t.secure:"undefined"!=typeof location&&"https:"===location.protocol,t.hostname&&!t.port&&(t.port=this.secure?"443":"80"),this.hostname=t.hostname||("undefined"!=typeof location?location.hostname:"localhost"),this.port=t.port||("undefined"!=typeof location&&location.port?location.port:this.secure?"443":"80"),this.transports=t.transports||["polling","websocket","webtransport"],this.writeBuffer=[],this.prevBufferLen=0,this.opts=Object.assign({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},t),this.opts.path=this.opts.path.replace(/\/$/,"")+(this.opts.addTrailingSlash?"/":""),"string"==typeof this.opts.query&&(this.opts.query=(0,i.decode)(this.opts.query)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingTimeoutTimer=null,"function"==typeof addEventListener&&(this.opts.closeOnBeforeunload&&(this.beforeunloadEventListener=()=>{this.transport&&(this.transport.removeAllListeners(),this.transport.close())},addEventListener("beforeunload",this.beforeunloadEventListener,!1)),"localhost"!==this.hostname&&(this.offlineEventListener=()=>{this.onClose("transport close",{description:"network connection lost"})},addEventListener("offline",this.offlineEventListener,!1))),this.open()}createTransport(e){d('creating transport "%s"',e);const t=Object.assign({},this.opts.query);t.EIO=u.protocol,t.transport=e,this.id&&(t.sid=this.id);const n=Object.assign({},this.opts,{query:t,socket:this,hostname:this.hostname,secure:this.secure,port:this.port},this.opts.transportOptions[e]);return d("options: %j",n),new s.transports[e](n)}open(){let e;if(this.opts.rememberUpgrade&&f.priorWebsocketSuccess&&-1!==this.transports.indexOf("websocket"))e="websocket";else{if(0===this.transports.length)return void this.setTimeoutFn((()=>{this.emitReserved("error","No transports available")}),0);e=this.transports[0]}this.readyState="opening";try{e=this.createTransport(e)}catch(e){return d("error while creating transport: %s",e),this.transports.shift(),void this.open()}e.open(),this.setTransport(e)}setTransport(e){d("setting transport %s",e.name),this.transport&&(d("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=e,e.on("drain",this.onDrain.bind(this)).on("packet",this.onPacket.bind(this)).on("error",this.onError.bind(this)).on("close",(e=>this.onClose("transport close",e)))}probe(e){d('probing transport "%s"',e);let t=this.createTransport(e),n=!1;f.priorWebsocketSuccess=!1;const r=()=>{n||(d('probe transport "%s" opened',e),t.send([{type:"ping",data:"probe"}]),t.once("packet",(r=>{if(!n)if("pong"===r.type&&"probe"===r.data){if(d('probe transport "%s" pong',e),this.upgrading=!0,this.emitReserved("upgrading",t),!t)return;f.priorWebsocketSuccess="websocket"===t.name,d('pausing current transport "%s"',this.transport.name),this.transport.pause((()=>{n||"closed"!==this.readyState&&(d("changing transport and sending upgrade packet"),l(),this.setTransport(t),t.send([{type:"upgrade"}]),this.emitReserved("upgrade",t),t=null,this.upgrading=!1,this.flush())}))}else{d('probe transport "%s" failed',e);const n=new Error("probe error");n.transport=t.name,this.emitReserved("upgradeError",n)}})))};function s(){n||(n=!0,l(),t.close(),t=null)}const o=n=>{const r=new Error("probe error: "+n);r.transport=t.name,s(),d('probe transport "%s" failed because of error: %s',e,n),this.emitReserved("upgradeError",r)};function i(){o("transport closed")}function a(){o("socket closed")}function c(e){t&&e.name!==t.name&&(d('"%s" works - aborting "%s"',e.name,t.name),s())}const l=()=>{t.removeListener("open",r),t.removeListener("error",o),t.removeListener("close",i),this.off("close",a),this.off("upgrading",c)};t.once("open",r),t.once("error",o),t.once("close",i),this.once("close",a),this.once("upgrading",c),-1!==this.upgrades.indexOf("webtransport")&&"webtransport"!==e?this.setTimeoutFn((()=>{n||t.open()}),200):t.open()}onOpen(){if(d("socket open"),this.readyState="open",f.priorWebsocketSuccess="websocket"===this.transport.name,this.emitReserved("open"),this.flush(),"open"===this.readyState&&this.opts.upgrade){d("starting upgrade probes");let e=0;const t=this.upgrades.length;for(;e{this.onClose("ping timeout")}),this.pingInterval+this.pingTimeout),this.opts.autoUnref&&this.pingTimeoutTimer.unref()}onDrain(){this.writeBuffer.splice(0,this.prevBufferLen),this.prevBufferLen=0,0===this.writeBuffer.length?this.emitReserved("drain"):this.flush()}flush(){if("closed"!==this.readyState&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length){const e=this.getWritablePackets();d("flushing %d packets in socket",e.length),this.transport.send(e),this.prevBufferLen=e.length,this.emitReserved("flush")}}getWritablePackets(){if(!(this.maxPayload&&"polling"===this.transport.name&&this.writeBuffer.length>1))return this.writeBuffer;let e=1;for(let t=0;t0&&e>this.maxPayload)return d("only send %d out of %d packets",t,this.writeBuffer.length),this.writeBuffer.slice(0,t);e+=2}return d("payload size is %d (max: %d)",e,this.maxPayload),this.writeBuffer}write(e,t,n){return this.sendPacket("message",e,t,n),this}send(e,t,n){return this.sendPacket("message",e,t,n),this}sendPacket(e,t,n,r){if("function"==typeof t&&(r=t,t=void 0),"function"==typeof n&&(r=n,n=null),"closing"===this.readyState||"closed"===this.readyState)return;(n=n||{}).compress=!1!==n.compress;const s={type:e,data:t,options:n};this.emitReserved("packetCreate",s),this.writeBuffer.push(s),r&&this.once("flush",r),this.flush()}close(){const e=()=>{this.onClose("forced close"),d("socket closing - telling transport to close"),this.transport.close()},t=()=>{this.off("upgrade",t),this.off("upgradeError",t),e()},n=()=>{this.once("upgrade",t),this.once("upgradeError",t)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(()=>{this.upgrading?n():e()})):this.upgrading?n():e()),this}onError(e){d("socket error %j",e),f.priorWebsocketSuccess=!1,this.emitReserved("error",e),this.onClose("transport error",e)}onClose(e,t){"opening"!==this.readyState&&"open"!==this.readyState&&"closing"!==this.readyState||(d('socket close with reason: "%s"',e),this.clearTimeoutFn(this.pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),"function"==typeof removeEventListener&&(removeEventListener("beforeunload",this.beforeunloadEventListener,!1),removeEventListener("offline",this.offlineEventListener,!1)),this.readyState="closed",this.id=null,this.emitReserved("close",e,t),this.writeBuffer=[],this.prevBufferLen=0)}filterUpgrades(e){const t=[];let n=0;const r=e.length;for(;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.transports=void 0;const r=n(484),s=n(308),o=n(20);t.transports={websocket:s.WS,webtransport:o.WT,polling:r.Polling}},484:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Request=t.Polling=void 0;const s=n(870),o=r(n(227)),i=n(726),a=n(373),c=n(666),l=n(260),u=n(622),h=n(242),d=(0,o.default)("engine.io-client:polling");function f(){}const p=null!=new c.XHR({xdomain:!1}).responseType;class m extends s.Transport{constructor(e){if(super(e),this.polling=!1,"undefined"!=typeof location){const t="https:"===location.protocol;let n=location.port;n||(n=t?"443":"80"),this.xd="undefined"!=typeof location&&e.hostname!==location.hostname||n!==e.port}const t=e&&e.forceBase64;this.supportsBinary=p&&!t,this.opts.withCredentials&&(this.cookieJar=(0,c.createCookieJar)())}get name(){return"polling"}doOpen(){this.poll()}pause(e){this.readyState="pausing";const t=()=>{d("paused"),this.readyState="paused",e()};if(this.polling||!this.writable){let e=0;this.polling&&(d("we are currently polling - waiting to pause"),e++,this.once("pollComplete",(function(){d("pre-pause polling complete"),--e||t()}))),this.writable||(d("we are currently writing - waiting to pause"),e++,this.once("drain",(function(){d("pre-pause writing complete"),--e||t()})))}else t()}poll(){d("polling"),this.polling=!0,this.doPoll(),this.emitReserved("poll")}onData(e){d("polling got data %s",e),(0,a.decodePayload)(e,this.socket.binaryType).forEach((e=>{if("opening"===this.readyState&&"open"===e.type&&this.onOpen(),"close"===e.type)return this.onClose({description:"transport closed by the server"}),!1;this.onPacket(e)})),"closed"!==this.readyState&&(this.polling=!1,this.emitReserved("pollComplete"),"open"===this.readyState?this.poll():d('ignoring poll - transport state "%s"',this.readyState))}doClose(){const e=()=>{d("writing close packet"),this.write([{type:"close"}])};"open"===this.readyState?(d("transport open - closing"),e()):(d("transport not open - deferring close"),this.once("open",e))}write(e){this.writable=!1,(0,a.encodePayload)(e,(e=>{this.doWrite(e,(()=>{this.writable=!0,this.emitReserved("drain")}))}))}uri(){const e=this.opts.secure?"https":"http",t=this.query||{};return!1!==this.opts.timestampRequests&&(t[this.opts.timestampParam]=(0,i.yeast)()),this.supportsBinary||t.sid||(t.b64=1),this.createUri(e,t)}request(e={}){return Object.assign(e,{xd:this.xd,cookieJar:this.cookieJar},this.opts),new g(this.uri(),e)}doWrite(e,t){const n=this.request({method:"POST",data:e});n.on("success",t),n.on("error",((e,t)=>{this.onError("xhr post error",e,t)}))}doPoll(){d("xhr poll");const e=this.request();e.on("data",this.onData.bind(this)),e.on("error",((e,t)=>{this.onError("xhr poll error",e,t)})),this.pollXhr=e}}t.Polling=m;class g extends l.Emitter{constructor(e,t){super(),(0,u.installTimerFunctions)(this,t),this.opts=t,this.method=t.method||"GET",this.uri=e,this.data=void 0!==t.data?t.data:null,this.create()}create(){var e;const t=(0,u.pick)(this.opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");t.xdomain=!!this.opts.xd;const n=this.xhr=new c.XHR(t);try{d("xhr open %s: %s",this.method,this.uri),n.open(this.method,this.uri,!0);try{if(this.opts.extraHeaders){n.setDisableHeaderCheck&&n.setDisableHeaderCheck(!0);for(let e in this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(e)&&n.setRequestHeader(e,this.opts.extraHeaders[e])}}catch(e){}if("POST"===this.method)try{n.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(e){}try{n.setRequestHeader("Accept","*/*")}catch(e){}null===(e=this.opts.cookieJar)||void 0===e||e.addCookies(n),"withCredentials"in n&&(n.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(n.timeout=this.opts.requestTimeout),n.onreadystatechange=()=>{var e;3===n.readyState&&(null===(e=this.opts.cookieJar)||void 0===e||e.parseCookies(n)),4===n.readyState&&(200===n.status||1223===n.status?this.onLoad():this.setTimeoutFn((()=>{this.onError("number"==typeof n.status?n.status:0)}),0))},d("xhr data %s",this.data),n.send(this.data)}catch(e){return void this.setTimeoutFn((()=>{this.onError(e)}),0)}"undefined"!=typeof document&&(this.index=g.requestsCount++,g.requests[this.index]=this)}onError(e){this.emitReserved("error",e,this.xhr),this.cleanup(!0)}cleanup(e){if(void 0!==this.xhr&&null!==this.xhr){if(this.xhr.onreadystatechange=f,e)try{this.xhr.abort()}catch(e){}"undefined"!=typeof document&&delete g.requests[this.index],this.xhr=null}}onLoad(){const e=this.xhr.responseText;null!==e&&(this.emitReserved("data",e),this.emitReserved("success"),this.cleanup())}abort(){this.cleanup()}}if(t.Request=g,g.requestsCount=0,g.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",y);else if("function"==typeof addEventListener){const e="onpagehide"in h.globalThisShim?"pagehide":"unload";addEventListener(e,y,!1)}function y(){for(let e in g.requests)g.requests.hasOwnProperty(e)&&g.requests[e].abort()}},552:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.defaultBinaryType=t.usingBrowserWebSocket=t.WebSocket=t.nextTick=void 0;const r=n(242);t.nextTick="function"==typeof Promise&&"function"==typeof Promise.resolve?e=>Promise.resolve().then(e):(e,t)=>t(e,0),t.WebSocket=r.globalThisShim.WebSocket||r.globalThisShim.MozWebSocket,t.usingBrowserWebSocket=!0,t.defaultBinaryType="arraybuffer"},308:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.WS=void 0;const s=n(870),o=n(726),i=n(622),a=n(552),c=r(n(227)),l=n(373),u=(0,c.default)("engine.io-client:websocket"),h="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase();class d extends s.Transport{constructor(e){super(e),this.supportsBinary=!e.forceBase64}get name(){return"websocket"}doOpen(){if(!this.check())return;const e=this.uri(),t=this.opts.protocols,n=h?{}:(0,i.pick)(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(n.headers=this.opts.extraHeaders);try{this.ws=a.usingBrowserWebSocket&&!h?t?new a.WebSocket(e,t):new a.WebSocket(e):new a.WebSocket(e,t,n)}catch(e){return this.emitReserved("error",e)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}addEventListeners(){this.ws.onopen=()=>{this.opts.autoUnref&&this.ws._socket.unref(),this.onOpen()},this.ws.onclose=e=>this.onClose({description:"websocket connection closed",context:e}),this.ws.onmessage=e=>this.onData(e.data),this.ws.onerror=e=>this.onError("websocket error",e)}write(e){this.writable=!1;for(let t=0;t{const t={};!a.usingBrowserWebSocket&&(n.options&&(t.compress=n.options.compress),this.opts.perMessageDeflate)&&("string"==typeof e?Buffer.byteLength(e):e.length){this.writable=!0,this.emitReserved("drain")}),this.setTimeoutFn)}))}}doClose(){void 0!==this.ws&&(this.ws.close(),this.ws=null)}uri(){const e=this.opts.secure?"wss":"ws",t=this.query||{};return this.opts.timestampRequests&&(t[this.opts.timestampParam]=(0,o.yeast)()),this.supportsBinary||(t.b64=1),this.createUri(e,t)}check(){return!!a.WebSocket}}t.WS=d},20:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.WT=void 0;const s=n(870),o=n(552),i=n(373),a=(0,r(n(227)).default)("engine.io-client:webtransport");class c extends s.Transport{get name(){return"webtransport"}doOpen(){"function"==typeof WebTransport&&(this.transport=new WebTransport(this.createUri("https"),this.opts.transportOptions[this.name]),this.transport.closed.then((()=>{a("transport closed gracefully"),this.onClose()})).catch((e=>{a("transport closed due to %s",e),this.onError("webtransport error",e)})),this.transport.ready.then((()=>{this.transport.createBidirectionalStream().then((e=>{const t=(0,i.createPacketDecoderStream)(Number.MAX_SAFE_INTEGER,this.socket.binaryType),n=e.readable.pipeThrough(t).getReader(),r=(0,i.createPacketEncoderStream)();r.readable.pipeTo(e.writable),this.writer=r.writable.getWriter();const s=()=>{n.read().then((({done:e,value:t})=>{e?a("session is closed"):(a("received chunk: %o",t),this.onPacket(t),s())})).catch((e=>{a("an error occurred while reading: %s",e)}))};s();const o={type:"open"};this.query.sid&&(o.data=`{"sid":"${this.query.sid}"}`),this.writer.write(o).then((()=>this.onOpen()))}))})))}write(e){this.writable=!1;for(let t=0;t{r&&(0,o.nextTick)((()=>{this.writable=!0,this.emitReserved("drain")}),this.setTimeoutFn)}))}}doClose(){var e;null===(e=this.transport)||void 0===e||e.close()}}t.WT=c},666:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createCookieJar=t.XHR=void 0;const r=n(419),s=n(242);t.XHR=function(e){const t=e.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!t||r.hasCORS))return new XMLHttpRequest}catch(e){}if(!t)try{return new(s.globalThisShim[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(e){}},t.createCookieJar=function(){}},622:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.byteLength=t.installTimerFunctions=t.pick=void 0;const r=n(242);t.pick=function(e,...t){return t.reduce(((t,n)=>(e.hasOwnProperty(n)&&(t[n]=e[n]),t)),{})};const s=r.globalThisShim.setTimeout,o=r.globalThisShim.clearTimeout;t.installTimerFunctions=function(e,t){t.useNativeTimers?(e.setTimeoutFn=s.bind(r.globalThisShim),e.clearTimeoutFn=o.bind(r.globalThisShim)):(e.setTimeoutFn=r.globalThisShim.setTimeout.bind(r.globalThisShim),e.clearTimeoutFn=r.globalThisShim.clearTimeout.bind(r.globalThisShim))},t.byteLength=function(e){return"string"==typeof e?function(e){let t=0,n=0;for(let r=0,s=e.length;r=57344?n+=3:(r++,n+=4);return n}(e):Math.ceil(1.33*(e.byteLength||e.size))}},87:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ERROR_PACKET=t.PACKET_TYPES_REVERSE=t.PACKET_TYPES=void 0;const n=Object.create(null);t.PACKET_TYPES=n,n.open="0",n.close="1",n.ping="2",n.pong="3",n.message="4",n.upgrade="5",n.noop="6";const r=Object.create(null);t.PACKET_TYPES_REVERSE=r,Object.keys(n).forEach((e=>{r[n[e]]=e})),t.ERROR_PACKET={type:"error",data:"parser error"}},469:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.decode=t.encode=void 0;const n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",r="undefined"==typeof Uint8Array?[]:new Uint8Array(256);for(let e=0;e<64;e++)r[n.charCodeAt(e)]=e;t.encode=e=>{let t,r=new Uint8Array(e),s=r.length,o="";for(t=0;t>2],o+=n[(3&r[t])<<4|r[t+1]>>4],o+=n[(15&r[t+1])<<2|r[t+2]>>6],o+=n[63&r[t+2]];return s%3==2?o=o.substring(0,o.length-1)+"=":s%3==1&&(o=o.substring(0,o.length-2)+"=="),o},t.decode=e=>{let t,n,s,o,i,a=.75*e.length,c=e.length,l=0;"="===e[e.length-1]&&(a--,"="===e[e.length-2]&&a--);const u=new ArrayBuffer(a),h=new Uint8Array(u);for(t=0;t>4,h[l++]=(15&s)<<4|o>>2,h[l++]=(3&o)<<6|63&i;return u}},572:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.decodePacket=void 0;const r=n(87),s=n(469),o="function"==typeof ArrayBuffer;t.decodePacket=(e,t)=>{if("string"!=typeof e)return{type:"message",data:a(e,t)};const n=e.charAt(0);return"b"===n?{type:"message",data:i(e.substring(1),t)}:r.PACKET_TYPES_REVERSE[n]?e.length>1?{type:r.PACKET_TYPES_REVERSE[n],data:e.substring(1)}:{type:r.PACKET_TYPES_REVERSE[n]}:r.ERROR_PACKET};const i=(e,t)=>{if(o){const n=(0,s.decode)(e);return a(n,t)}return{base64:!0,data:e}},a=(e,t)=>"blob"===t?e instanceof Blob?e:new Blob([e]):e instanceof ArrayBuffer?e:e.buffer},908:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.encodePacket=t.encodePacketToBinary=void 0;const r=n(87),s="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),o="function"==typeof ArrayBuffer,i=e=>"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer instanceof ArrayBuffer,a=({type:e,data:t},n,a)=>s&&t instanceof Blob?n?a(t):c(t,a):o&&(t instanceof ArrayBuffer||i(t))?n?a(t):c(new Blob([t]),a):a(r.PACKET_TYPES[e]+(t||""));t.encodePacket=a;const c=(e,t)=>{const n=new FileReader;return n.onload=function(){const e=n.result.split(",")[1];t("b"+(e||""))},n.readAsDataURL(e)};function l(e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}let u;t.encodePacketToBinary=function(e,t){return s&&e.data instanceof Blob?e.data.arrayBuffer().then(l).then(t):o&&(e.data instanceof ArrayBuffer||i(e.data))?t(l(e.data)):void a(e,!1,(e=>{u||(u=new TextEncoder),t(u.encode(e))}))}},373:(e,t,n)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.decodePayload=t.decodePacket=t.encodePayload=t.encodePacket=t.protocol=t.createPacketDecoderStream=t.createPacketEncoderStream=void 0;const r=n(908);Object.defineProperty(t,"encodePacket",{enumerable:!0,get:function(){return r.encodePacket}});const s=n(572);Object.defineProperty(t,"decodePacket",{enumerable:!0,get:function(){return s.decodePacket}});const o=n(87),i=String.fromCharCode(30);let a;function c(e){return e.reduce(((e,t)=>e+t.length),0)}function l(e,t){if(e[0].length===t)return e.shift();const n=new Uint8Array(t);let r=0;for(let s=0;s{const n=e.length,s=new Array(n);let o=0;e.forEach(((e,a)=>{(0,r.encodePacket)(e,!1,(e=>{s[a]=e,++o===n&&t(s.join(i))}))}))},t.decodePayload=(e,t)=>{const n=e.split(i),r=[];for(let e=0;e{const r=n.length;let s;if(r<126)s=new Uint8Array(1),new DataView(s.buffer).setUint8(0,r);else if(r<65536){s=new Uint8Array(3);const e=new DataView(s.buffer);e.setUint8(0,126),e.setUint16(1,r)}else{s=new Uint8Array(9);const e=new DataView(s.buffer);e.setUint8(0,127),e.setBigUint64(1,BigInt(r))}e.data&&"string"!=typeof e.data&&(s[0]|=128),t.enqueue(s),t.enqueue(n)}))}})},t.createPacketDecoderStream=function(e,t){a||(a=new TextDecoder);const n=[];let r=0,i=-1,u=!1;return new TransformStream({transform(h,d){for(n.push(h);;){if(0===r){if(c(n)<1)break;const e=l(n,1);u=128==(128&e[0]),i=127&e[0],r=i<126?3:126===i?1:2}else if(1===r){if(c(n)<2)break;const e=l(n,2);i=new DataView(e.buffer,e.byteOffset,e.length).getUint16(0),r=3}else if(2===r){if(c(n)<8)break;const e=l(n,8),t=new DataView(e.buffer,e.byteOffset,e.length),s=t.getUint32(0);if(s>Math.pow(2,21)-1){d.enqueue(o.ERROR_PACKET);break}i=s*Math.pow(2,32)+t.getUint32(4),r=3}else{if(c(n)e){d.enqueue(o.ERROR_PACKET);break}}}})},t.protocol=4},159:(e,t)=>{"use strict";function n(e){e=e||{},this.ms=e.min||100,this.max=e.max||1e4,this.factor=e.factor||2,this.jitter=e.jitter>0&&e.jitter<=1?e.jitter:0,this.attempts=0}Object.defineProperty(t,"__esModule",{value:!0}),t.Backoff=void 0,t.Backoff=n,n.prototype.duration=function(){var e=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var t=Math.random(),n=Math.floor(t*this.jitter*e);e=0==(1&Math.floor(10*t))?e-n:e+n}return 0|Math.min(e,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(e){this.ms=e},n.prototype.setMax=function(e){this.max=e},n.prototype.setJitter=function(e){this.jitter=e}},46:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.default=t.connect=t.io=t.Socket=t.Manager=t.protocol=void 0;const s=n(84),o=n(168);Object.defineProperty(t,"Manager",{enumerable:!0,get:function(){return o.Manager}});const i=n(312);Object.defineProperty(t,"Socket",{enumerable:!0,get:function(){return i.Socket}});const a=r(n(227)).default("socket.io-client"),c={};function l(e,t){"object"==typeof e&&(t=e,e=void 0),t=t||{};const n=s.url(e,t.path||"/socket.io"),r=n.source,i=n.id,l=n.path,u=c[i]&&l in c[i].nsps;let h;return t.forceNew||t["force new connection"]||!1===t.multiplex||u?(a("ignoring socket cache for %s",r),h=new o.Manager(r,t)):(c[i]||(a("new io instance for %s",r),c[i]=new o.Manager(r,t)),h=c[i]),n.query&&!t.query&&(t.query=n.queryKey),h.socket(n.path,t)}t.io=l,t.connect=l,t.default=l,Object.assign(l,{Manager:o.Manager,Socket:i.Socket,io:l,connect:l});var u=n(514);Object.defineProperty(t,"protocol",{enumerable:!0,get:function(){return u.protocol}}),e.exports=l},168:function(e,t,n){"use strict";var r=this&&this.__createBinding||(Object.create?function(e,t,n,r){void 0===r&&(r=n),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,r){void 0===r&&(r=n),e[r]=t[n]}),s=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),o=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var t={};if(null!=e)for(var n in e)"default"!==n&&Object.prototype.hasOwnProperty.call(e,n)&&r(t,e,n);return s(t,e),t},i=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Manager=void 0;const a=n(679),c=n(312),l=o(n(514)),u=n(149),h=n(159),d=n(260),f=i(n(227)).default("socket.io-client:manager");class p extends d.Emitter{constructor(e,t){var n;super(),this.nsps={},this.subs=[],e&&"object"==typeof e&&(t=e,e=void 0),(t=t||{}).path=t.path||"/socket.io",this.opts=t,a.installTimerFunctions(this,t),this.reconnection(!1!==t.reconnection),this.reconnectionAttempts(t.reconnectionAttempts||1/0),this.reconnectionDelay(t.reconnectionDelay||1e3),this.reconnectionDelayMax(t.reconnectionDelayMax||5e3),this.randomizationFactor(null!==(n=t.randomizationFactor)&&void 0!==n?n:.5),this.backoff=new h.Backoff({min:this.reconnectionDelay(),max:this.reconnectionDelayMax(),jitter:this.randomizationFactor()}),this.timeout(null==t.timeout?2e4:t.timeout),this._readyState="closed",this.uri=e;const r=t.parser||l;this.encoder=new r.Encoder,this.decoder=new r.Decoder,this._autoConnect=!1!==t.autoConnect,this._autoConnect&&this.open()}reconnection(e){return arguments.length?(this._reconnection=!!e,this):this._reconnection}reconnectionAttempts(e){return void 0===e?this._reconnectionAttempts:(this._reconnectionAttempts=e,this)}reconnectionDelay(e){var t;return void 0===e?this._reconnectionDelay:(this._reconnectionDelay=e,null===(t=this.backoff)||void 0===t||t.setMin(e),this)}randomizationFactor(e){var t;return void 0===e?this._randomizationFactor:(this._randomizationFactor=e,null===(t=this.backoff)||void 0===t||t.setJitter(e),this)}reconnectionDelayMax(e){var t;return void 0===e?this._reconnectionDelayMax:(this._reconnectionDelayMax=e,null===(t=this.backoff)||void 0===t||t.setMax(e),this)}timeout(e){return arguments.length?(this._timeout=e,this):this._timeout}maybeReconnectOnOpen(){!this._reconnecting&&this._reconnection&&0===this.backoff.attempts&&this.reconnect()}open(e){if(f("readyState %s",this._readyState),~this._readyState.indexOf("open"))return this;f("opening %s",this.uri),this.engine=new a.Socket(this.uri,this.opts);const t=this.engine,n=this;this._readyState="opening",this.skipReconnect=!1;const r=u.on(t,"open",(function(){n.onopen(),e&&e()})),s=t=>{f("error"),this.cleanup(),this._readyState="closed",this.emitReserved("error",t),e?e(t):this.maybeReconnectOnOpen()},o=u.on(t,"error",s);if(!1!==this._timeout){const e=this._timeout;f("connect attempt will timeout after %d",e);const n=this.setTimeoutFn((()=>{f("connect attempt timed out after %d",e),r(),s(new Error("timeout")),t.close()}),e);this.opts.autoUnref&&n.unref(),this.subs.push((()=>{this.clearTimeoutFn(n)}))}return this.subs.push(r),this.subs.push(o),this}connect(e){return this.open(e)}onopen(){f("open"),this.cleanup(),this._readyState="open",this.emitReserved("open");const e=this.engine;this.subs.push(u.on(e,"ping",this.onping.bind(this)),u.on(e,"data",this.ondata.bind(this)),u.on(e,"error",this.onerror.bind(this)),u.on(e,"close",this.onclose.bind(this)),u.on(this.decoder,"decoded",this.ondecoded.bind(this)))}onping(){this.emitReserved("ping")}ondata(e){try{this.decoder.add(e)}catch(e){this.onclose("parse error",e)}}ondecoded(e){a.nextTick((()=>{this.emitReserved("packet",e)}),this.setTimeoutFn)}onerror(e){f("error",e),this.emitReserved("error",e)}socket(e,t){let n=this.nsps[e];return n?this._autoConnect&&!n.active&&n.connect():(n=new c.Socket(this,e,t),this.nsps[e]=n),n}_destroy(e){const t=Object.keys(this.nsps);for(const e of t)if(this.nsps[e].active)return void f("socket %s is still active, skipping close",e);this._close()}_packet(e){f("writing packet %j",e);const t=this.encoder.encode(e);for(let n=0;ne())),this.subs.length=0,this.decoder.destroy()}_close(){f("disconnect"),this.skipReconnect=!0,this._reconnecting=!1,this.onclose("forced close"),this.engine&&this.engine.close()}disconnect(){return this._close()}onclose(e,t){f("closed due to %s",e),this.cleanup(),this.backoff.reset(),this._readyState="closed",this.emitReserved("close",e,t),this._reconnection&&!this.skipReconnect&&this.reconnect()}reconnect(){if(this._reconnecting||this.skipReconnect)return this;const e=this;if(this.backoff.attempts>=this._reconnectionAttempts)f("reconnect failed"),this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{const t=this.backoff.duration();f("will wait %dms before reconnect attempt",t),this._reconnecting=!0;const n=this.setTimeoutFn((()=>{e.skipReconnect||(f("attempting reconnect"),this.emitReserved("reconnect_attempt",e.backoff.attempts),e.skipReconnect||e.open((t=>{t?(f("reconnect attempt error"),e._reconnecting=!1,e.reconnect(),this.emitReserved("reconnect_error",t)):(f("reconnect success"),e.onreconnect())})))}),t);this.opts.autoUnref&&n.unref(),this.subs.push((()=>{this.clearTimeoutFn(n)}))}}onreconnect(){const e=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",e)}}t.Manager=p},149:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.on=void 0,t.on=function(e,t,n){return e.on(t,n),function(){e.off(t,n)}}},312:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.Socket=void 0;const s=n(514),o=n(149),i=n(260),a=r(n(227)).default("socket.io-client:socket"),c=Object.freeze({connect:1,connect_error:1,disconnect:1,disconnecting:1,newListener:1,removeListener:1});class l extends i.Emitter{constructor(e,t,n){super(),this.connected=!1,this.recovered=!1,this.receiveBuffer=[],this.sendBuffer=[],this._queue=[],this._queueSeq=0,this.ids=0,this.acks={},this.flags={},this.io=e,this.nsp=t,n&&n.auth&&(this.auth=n.auth),this._opts=Object.assign({},n),this.io._autoConnect&&this.open()}get disconnected(){return!this.connected}subEvents(){if(this.subs)return;const e=this.io;this.subs=[o.on(e,"open",this.onopen.bind(this)),o.on(e,"packet",this.onpacket.bind(this)),o.on(e,"error",this.onerror.bind(this)),o.on(e,"close",this.onclose.bind(this))]}get active(){return!!this.subs}connect(){return this.connected||(this.subEvents(),this.io._reconnecting||this.io.open(),"open"===this.io._readyState&&this.onopen()),this}open(){return this.connect()}send(...e){return e.unshift("message"),this.emit.apply(this,e),this}emit(e,...t){if(c.hasOwnProperty(e))throw new Error('"'+e.toString()+'" is a reserved event name');if(t.unshift(e),this._opts.retries&&!this.flags.fromQueue&&!this.flags.volatile)return this._addToQueue(t),this;const n={type:s.PacketType.EVENT,data:t,options:{}};if(n.options.compress=!1!==this.flags.compress,"function"==typeof t[t.length-1]){const e=this.ids++;a("emitting packet with ack id %d",e);const r=t.pop();this._registerAckCallback(e,r),n.id=e}const r=this.io.engine&&this.io.engine.transport&&this.io.engine.transport.writable;return!this.flags.volatile||r&&this.connected?this.connected?(this.notifyOutgoingListeners(n),this.packet(n)):this.sendBuffer.push(n):a("discard packet as the transport is not currently writable"),this.flags={},this}_registerAckCallback(e,t){var n;const r=null!==(n=this.flags.timeout)&&void 0!==n?n:this._opts.ackTimeout;if(void 0===r)return void(this.acks[e]=t);const s=this.io.setTimeoutFn((()=>{delete this.acks[e];for(let t=0;t{this.io.clearTimeoutFn(s),t.apply(this,[null,...e])}}emitWithAck(e,...t){const n=void 0!==this.flags.timeout||void 0!==this._opts.ackTimeout;return new Promise(((r,s)=>{t.push(((e,t)=>n?e?s(e):r(t):r(e))),this.emit(e,...t)}))}_addToQueue(e){let t;"function"==typeof e[e.length-1]&&(t=e.pop());const n={id:this._queueSeq++,tryCount:0,pending:!1,args:e,flags:Object.assign({fromQueue:!0},this.flags)};e.push(((e,...r)=>{if(n===this._queue[0])return null!==e?n.tryCount>this._opts.retries&&(a("packet [%d] is discarded after %d tries",n.id,n.tryCount),this._queue.shift(),t&&t(e)):(a("packet [%d] was successfully sent",n.id),this._queue.shift(),t&&t(null,...r)),n.pending=!1,this._drainQueue()})),this._queue.push(n),this._drainQueue()}_drainQueue(e=!1){if(a("draining queue"),!this.connected||0===this._queue.length)return;const t=this._queue[0];!t.pending||e?(t.pending=!0,t.tryCount++,a("sending packet [%d] (try n°%d)",t.id,t.tryCount),this.flags=t.flags,this.emit.apply(this,t.args)):a("packet [%d] has already been sent and is waiting for an ack",t.id)}packet(e){e.nsp=this.nsp,this.io._packet(e)}onopen(){a("transport is open - connecting"),"function"==typeof this.auth?this.auth((e=>{this._sendConnectPacket(e)})):this._sendConnectPacket(this.auth)}_sendConnectPacket(e){this.packet({type:s.PacketType.CONNECT,data:this._pid?Object.assign({pid:this._pid,offset:this._lastOffset},e):e})}onerror(e){this.connected||this.emitReserved("connect_error",e)}onclose(e,t){a("close (%s)",e),this.connected=!1,delete this.id,this.emitReserved("disconnect",e,t)}onpacket(e){if(e.nsp===this.nsp)switch(e.type){case s.PacketType.CONNECT:e.data&&e.data.sid?this.onconnect(e.data.sid,e.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case s.PacketType.EVENT:case s.PacketType.BINARY_EVENT:this.onevent(e);break;case s.PacketType.ACK:case s.PacketType.BINARY_ACK:this.onack(e);break;case s.PacketType.DISCONNECT:this.ondisconnect();break;case s.PacketType.CONNECT_ERROR:this.destroy();const t=new Error(e.data.message);t.data=e.data.data,this.emitReserved("connect_error",t)}}onevent(e){const t=e.data||[];a("emitting event %j",t),null!=e.id&&(a("attaching ack callback to event"),t.push(this.ack(e.id))),this.connected?this.emitEvent(t):this.receiveBuffer.push(Object.freeze(t))}emitEvent(e){if(this._anyListeners&&this._anyListeners.length){const t=this._anyListeners.slice();for(const n of t)n.apply(this,e)}super.emit.apply(this,e),this._pid&&e.length&&"string"==typeof e[e.length-1]&&(this._lastOffset=e[e.length-1])}ack(e){const t=this;let n=!1;return function(...r){n||(n=!0,a("sending ack %j",r),t.packet({type:s.PacketType.ACK,id:e,data:r}))}}onack(e){const t=this.acks[e.id];"function"==typeof t?(a("calling ack %s with %j",e.id,e.data),t.apply(this,e.data),delete this.acks[e.id]):a("bad ack %s",e.id)}onconnect(e,t){a("socket connected with id %s",e),this.id=e,this.recovered=t&&this._pid===t,this._pid=t,this.connected=!0,this.emitBuffered(),this.emitReserved("connect"),this._drainQueue(!0)}emitBuffered(){this.receiveBuffer.forEach((e=>this.emitEvent(e))),this.receiveBuffer=[],this.sendBuffer.forEach((e=>{this.notifyOutgoingListeners(e),this.packet(e)})),this.sendBuffer=[]}ondisconnect(){a("server disconnect (%s)",this.nsp),this.destroy(),this.onclose("io server disconnect")}destroy(){this.subs&&(this.subs.forEach((e=>e())),this.subs=void 0),this.io._destroy(this)}disconnect(){return this.connected&&(a("performing disconnect (%s)",this.nsp),this.packet({type:s.PacketType.DISCONNECT})),this.destroy(),this.connected&&this.onclose("io client disconnect"),this}close(){return this.disconnect()}compress(e){return this.flags.compress=e,this}get volatile(){return this.flags.volatile=!0,this}timeout(e){return this.flags.timeout=e,this}onAny(e){return this._anyListeners=this._anyListeners||[],this._anyListeners.push(e),this}prependAny(e){return this._anyListeners=this._anyListeners||[],this._anyListeners.unshift(e),this}offAny(e){if(!this._anyListeners)return this;if(e){const t=this._anyListeners;for(let n=0;n{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.reconstructPacket=t.deconstructPacket=void 0;const r=n(665);function s(e,t){if(!e)return e;if((0,r.isBinary)(e)){const n={_placeholder:!0,num:t.length};return t.push(e),n}if(Array.isArray(e)){const n=new Array(e.length);for(let r=0;r=0&&e.num{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Decoder=t.Encoder=t.PacketType=t.protocol=void 0;const r=n(260),s=n(269),o=n(665),i=(0,n(227).default)("socket.io-parser"),a=["connect","connect_error","disconnect","disconnecting","newListener","removeListener"];var c;function l(e){return"[object Object]"===Object.prototype.toString.call(e)}t.protocol=5,function(e){e[e.CONNECT=0]="CONNECT",e[e.DISCONNECT=1]="DISCONNECT",e[e.EVENT=2]="EVENT",e[e.ACK=3]="ACK",e[e.CONNECT_ERROR=4]="CONNECT_ERROR",e[e.BINARY_EVENT=5]="BINARY_EVENT",e[e.BINARY_ACK=6]="BINARY_ACK"}(c=t.PacketType||(t.PacketType={})),t.Encoder=class{constructor(e){this.replacer=e}encode(e){return i("encoding packet %j",e),e.type!==c.EVENT&&e.type!==c.ACK||!(0,o.hasBinary)(e)?[this.encodeAsString(e)]:this.encodeAsBinary({type:e.type===c.EVENT?c.BINARY_EVENT:c.BINARY_ACK,nsp:e.nsp,data:e.data,id:e.id})}encodeAsString(e){let t=""+e.type;return e.type!==c.BINARY_EVENT&&e.type!==c.BINARY_ACK||(t+=e.attachments+"-"),e.nsp&&"/"!==e.nsp&&(t+=e.nsp+","),null!=e.id&&(t+=e.id),null!=e.data&&(t+=JSON.stringify(e.data,this.replacer)),i("encoded %j as %s",e,t),t}encodeAsBinary(e){const t=(0,s.deconstructPacket)(e),n=this.encodeAsString(t.packet),r=t.buffers;return r.unshift(n),r}};class u extends r.Emitter{constructor(e){super(),this.reviver=e}add(e){let t;if("string"==typeof e){if(this.reconstructor)throw new Error("got plaintext data when reconstructing a packet");t=this.decodeString(e);const n=t.type===c.BINARY_EVENT;n||t.type===c.BINARY_ACK?(t.type=n?c.EVENT:c.ACK,this.reconstructor=new h(t),0===t.attachments&&super.emitReserved("decoded",t)):super.emitReserved("decoded",t)}else{if(!(0,o.isBinary)(e)&&!e.base64)throw new Error("Unknown type: "+e);if(!this.reconstructor)throw new Error("got binary data when not reconstructing a packet");t=this.reconstructor.takeBinaryData(e),t&&(this.reconstructor=null,super.emitReserved("decoded",t))}}decodeString(e){let t=0;const n={type:Number(e.charAt(0))};if(void 0===c[n.type])throw new Error("unknown packet type "+n.type);if(n.type===c.BINARY_EVENT||n.type===c.BINARY_ACK){const r=t+1;for(;"-"!==e.charAt(++t)&&t!=e.length;);const s=e.substring(r,t);if(s!=Number(s)||"-"!==e.charAt(t))throw new Error("Illegal attachments");n.attachments=Number(s)}if("/"===e.charAt(t+1)){const r=t+1;for(;++t&&","!==e.charAt(t)&&t!==e.length;);n.nsp=e.substring(r,t)}else n.nsp="/";const r=e.charAt(t+1);if(""!==r&&Number(r)==r){const r=t+1;for(;++t;){const n=e.charAt(t);if(null==n||Number(n)!=n){--t;break}if(t===e.length)break}n.id=Number(e.substring(r,t+1))}if(e.charAt(++t)){const r=this.tryParse(e.substr(t));if(!u.isPayloadValid(n.type,r))throw new Error("invalid payload");n.data=r}return i("decoded %s as %j",e,n),n}tryParse(e){try{return JSON.parse(e,this.reviver)}catch(e){return!1}}static isPayloadValid(e,t){switch(e){case c.CONNECT:return l(t);case c.DISCONNECT:return void 0===t;case c.CONNECT_ERROR:return"string"==typeof t||l(t);case c.EVENT:case c.BINARY_EVENT:return Array.isArray(t)&&("number"==typeof t[0]||"string"==typeof t[0]&&-1===a.indexOf(t[0]));case c.ACK:case c.BINARY_ACK:return Array.isArray(t)}}destroy(){this.reconstructor&&(this.reconstructor.finishedReconstruction(),this.reconstructor=null)}}t.Decoder=u;class h{constructor(e){this.packet=e,this.buffers=[],this.reconPack=e}takeBinaryData(e){if(this.buffers.push(e),this.buffers.length===this.reconPack.attachments){const e=(0,s.reconstructPacket)(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null}finishedReconstruction(){this.reconPack=null,this.buffers=[]}}},665:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.hasBinary=t.isBinary=void 0;const n="function"==typeof ArrayBuffer,r=Object.prototype.toString,s="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===r.call(Blob),o="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===r.call(File);function i(e){return n&&(e instanceof ArrayBuffer||(e=>"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(e):e.buffer instanceof ArrayBuffer)(e))||s&&e instanceof Blob||o&&e instanceof File}t.isBinary=i,t.hasBinary=function e(t,n){if(!t||"object"!=typeof t)return!1;if(Array.isArray(t)){for(let n=0,r=t.length;n{"use strict";function r(e){if(e)return function(e){for(var t in r.prototype)e[t]=r.prototype[t];return e}(e)}n.r(t),n.d(t,{Emitter:()=>r}),r.prototype.on=r.prototype.addEventListener=function(e,t){return this._callbacks=this._callbacks||{},(this._callbacks["$"+e]=this._callbacks["$"+e]||[]).push(t),this},r.prototype.once=function(e,t){function n(){this.off(e,n),t.apply(this,arguments)}return n.fn=t,this.on(e,n),this},r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(e,t){if(this._callbacks=this._callbacks||{},0==arguments.length)return this._callbacks={},this;var n,r=this._callbacks["$"+e];if(!r)return this;if(1==arguments.length)return delete this._callbacks["$"+e],this;for(var s=0;s{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})};var r={};return(()=>{"use strict";var e=r;Object.defineProperty(e,"__esModule",{value:!0}),e.validateConfiguration=e.parseConnectionString=e.prepareSql=e.escapeSqlParameter=e.SQLiteCloudRow=e.SQLiteCloudRowset=e.SQLiteCloudError=e.SQLiteCloudConnection=e.Statement=e.Database=void 0;var t=n(751);Object.defineProperty(e,"Database",{enumerable:!0,get:function(){return t.Database}});var s=n(880);Object.defineProperty(e,"Statement",{enumerable:!0,get:function(){return s.Statement}});var o=n(480);Object.defineProperty(e,"SQLiteCloudConnection",{enumerable:!0,get:function(){return o.SQLiteCloudConnection}});var i=n(906);Object.defineProperty(e,"SQLiteCloudError",{enumerable:!0,get:function(){return i.SQLiteCloudError}});var a=n(825);Object.defineProperty(e,"SQLiteCloudRowset",{enumerable:!0,get:function(){return a.SQLiteCloudRowset}}),Object.defineProperty(e,"SQLiteCloudRow",{enumerable:!0,get:function(){return a.SQLiteCloudRow}});var c=n(73);Object.defineProperty(e,"escapeSqlParameter",{enumerable:!0,get:function(){return c.escapeSqlParameter}}),Object.defineProperty(e,"prepareSql",{enumerable:!0,get:function(){return c.prepareSql}}),Object.defineProperty(e,"parseConnectionString",{enumerable:!0,get:function(){return c.parseConnectionString}}),Object.defineProperty(e,"validateConfiguration",{enumerable:!0,get:function(){return c.validateConfiguration}})})(),r})()));
\ No newline at end of file
diff --git a/scripts/gateway-build.sh b/scripts/gateway-build.sh
new file mode 100755
index 0000000..c73afc2
--- /dev/null
+++ b/scripts/gateway-build.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# gateway-build.sh - build gateway as self contained executable for linux and macos
+# If you get an error executing this script run:
+# chmod u+x ./scripts/build.sh
+
+# navigate to the script's directory
+#cd "$(dirname "$0")"
+
+# build bun's one file binary
+mkdir -p ./lib/gateway/public
+bun build ./src/gateway/gateway.ts --compile --outfile ./lib/gateway/gateway.out
+
+# copy the public folder to the build folder
+cp -r ./public/* ./lib/gateway/public/
+cp ./package.json ./lib/gateway/
diff --git a/scripts/gateway-upgrade.sh b/scripts/gateway-upgrade.sh
new file mode 100755
index 0000000..007cd7f
--- /dev/null
+++ b/scripts/gateway-upgrade.sh
@@ -0,0 +1,19 @@
+#
+# upgrade.sh - upload gateway builds to a couple of nodes used for development, restart gateway
+#
+
+# upload ./build/ to test nodes
+# frankfurt01, frankfurt02, ny01
+rsync -avz -e "ssh -i ~/.ssh/id_ed25519" ./build/ sqlitecloud@y8pbz99zp.sqlite.cloud:gateway
+rsync -avz -e "ssh -i ~/.ssh/id_ed25519" ./build/ sqlitecloud@oggdnp3zm.sqlite.cloud:gateway
+
+# connect to nodes using ssh key
+#Â ssh -i ~/.ssh/id_ed25519 sqlitecloud@y8pbz99zp.sqlite.cloud
+#Â ssh -i ~/.ssh/id_ed25519 sqlitecloud@og0wjec-m.sqlite.cloud
+
+# restart gateway (locally)
+#Â pkill gateway-linux-x; cd /home/sqlitecloud/gateway; nohup ./gateway-linux-x64.out
+
+# restart gateway (remotely)
+ssh -i ~/.ssh/id_ed25519 sqlitecloud@y8pbz99zp.sqlite.cloud 'pkill gateway-linux-x; cd /home/sqlitecloud/gateway; nohup ./gateway-linux-x64.out > /dev/null 2>&1 &'
+ssh -i ~/.ssh/id_ed25519 sqlitecloud@oggdnp3zm.sqlite.cloud 'pkill gateway-linux-x; cd /home/sqlitecloud/gateway; nohup ./gateway-linux-x64.out > /dev/null 2>&1 &'
\ No newline at end of file
diff --git a/scripts/sqlitecloud-cli b/scripts/sqlitecloud-cli
new file mode 100755
index 0000000..8125c00
Binary files /dev/null and b/scripts/sqlitecloud-cli differ
diff --git a/src/connection.ts b/src/connection.ts
deleted file mode 100644
index bd6004b..0000000
--- a/src/connection.ts
+++ /dev/null
@@ -1,255 +0,0 @@
-/**
- * connection.ts - handles low level communication with sqlitecloud server
- */
-
-import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
-import { validateConfiguration, isBrowser } from './utilities'
-
-/**
- * Base class for SQLiteCloudConnection handles basics and defines methods.
- * Actual connection management and communication with the server in concrete classes.
- */
-export class SQLiteCloudConnection {
- /** Parse and validate provided connectionString or configuration */
- constructor(config: SQLiteCloudConfig | string, callback?: ErrorCallback) {
- if (typeof config === 'string') {
- this.config = validateConfiguration({ connectionString: config })
- } else {
- this.config = validateConfiguration(config)
- }
-
- // connect transport layer to server
- this.connect(callback)
- }
-
- /** Configuration passed by client or extracted from connection string */
- protected config: SQLiteCloudConfig
-
- /** Transport used to communicate with server */
- protected transport?: ConnectionTransport
-
- /** Operations are serialized by waiting an any pending promises */
- protected operations = new OperationsQueue()
-
- //
- // public properties
- //
-
- /** True if connection is open */
- public get connected(): boolean {
- return this.transport?.connected || false
- }
-
- /** Connect will establish a tls or websocket transport to the server based on configuration and environment */
- protected connect(callback?: ErrorCallback): this {
- this.operations.enqueue(done => {
- // connect using websocket if tls is not supported or if explicitly requested
- if (isBrowser || this.config?.useWebsocket || this.config?.gatewayUrl) {
- // socket.io transport works in both node.js and browser environments and connects via SQLite Cloud Gateway
- import('./transport-ws')
- .then(transport => {
- this.transport = new transport.WebSocketTransport()
- this.transport.connect(this.config, error => {
- if (error) {
- console.error(
- `SQLiteCloudConnection.connect - error while connecting WebSocketTransport: ${error.toString()} to ${this.config.host}:${this.config.port}`,
- error
- )
- this.close()
- }
- callback?.call(this, error || null)
- done(error)
- })
- })
- .catch(error => {
- done(error)
- })
- } else {
- // tls sockets work only in node.js environments
- import('./transport-tls')
- .then(transport => {
- this.transport = new transport.TlsSocketTransport()
- this.transport.connect(this.config, error => {
- if (error) {
- console.error(
- `SQLiteCloudConnection.connect - error while connecting TlsSocketTransport: ${error.toString()} to ${this.config.host}:${this.config.port}`,
- error
- )
- this.close()
- }
- callback?.call(this, error || null)
- done(error)
- })
- })
- .catch(error => {
- done(error)
- })
- }
- })
-
- return this
- }
-
- //
- // private methods
- //
-
- /** Will log to console if verbose mode is enabled */
- protected log(message: string, ...optionalParams: any[]): void {
- if (this.config.verbose) {
- message = anonimizeCommand(message)
- console.log(`${new Date().toISOString()} ${this.config.clientId as string}: ${message}`, ...optionalParams)
- }
- }
-
- //
- // public methods
- //
-
- /** Enable verbose logging for debug purposes */
- public verbose(): void {
- this.config.verbose = true
- }
-
- /** Will enquee a command to be executed and callback with the resulting rowset/result/error */
- public sendCommands(commands: string, callback?: ResultsCallback): this {
- this.operations.enqueue(done => {
- if (this.transport) {
- this.transport.processCommands(commands, (error, result) => {
- callback?.call(this, error, result)
- done(error)
- })
- } else {
- const error = new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' })
- callback?.call(this, error)
- done(error)
- }
- })
-
- return this
- }
-
- /** Disconnect from server, release connection. */
- public close(): this {
- this.operations.clear()
- this.transport?.close()
- this.transport = undefined
- return this
- }
-}
-
-//
-// OperationsQueue - used to linearize operations on the connection
-//
-
-type OperationCallback = (error: Error | null) => void
-type Operation = (done: OperationCallback) => void
-
-export class OperationsQueue {
- private queue: Operation[] = []
- private isProcessing = false
-
- /** Add operations to the queue, process immediately if possible, else wait for previous operations to complete */
- public enqueue(operation: Operation): void {
- this.queue.push(operation)
- if (!this.isProcessing) {
- this.processNext()
- }
- }
-
- /** Clear the queue */
- public clear(): void {
- this.queue = []
- this.isProcessing = false
- }
-
- /** Process the next operation in the queue */
- private processNext(): void {
- if (this.queue.length === 0) {
- this.isProcessing = false
- return
- }
-
- this.isProcessing = true
- const operation = this.queue.shift()
- operation?.(() => {
- // could receive (error) => { ...
- // if (error) {
- // console.warn('OperationQueue.processNext - error in operation', error)
- // }
-
- // process the next operation in the queue
- this.processNext()
- })
- }
-}
-
-//
-// utility functions
-//
-
-/** Messages going to the server are sometimes logged when error conditions occour and need to be stripped of user credentials */
-export function anonimizeCommand(message: string): string {
- // hide password in AUTH command if needed
- message = message.replace(/USER \S+/, 'USER ******')
- message = message.replace(/PASSWORD \S+?(?=;)/, 'PASSWORD ******')
- message = message.replace(/HASH \S+?(?=;)/, 'HASH ******')
- return message
-}
-
-/** Strip message code in error of user credentials */
-export function anonimizeError(error: Error): Error {
- if (error?.message) {
- error.message = anonimizeCommand(error.message)
- }
- return error
-}
-
-/** Initialization commands sent to database when connection is established */
-export function getInitializationCommands(config: SQLiteCloudConfig): string {
- // first user authentication, then all other commands
- let commands = `AUTH USER ${config.username || ''} ${config.passwordHashed ? 'HASH' : 'PASSWORD'} ${config.password || ''}; `
-
- if (config.database) {
- if (config.createDatabase && !config.dbMemory) {
- commands += `CREATE DATABASE ${config.database} IF NOT EXISTS; `
- }
- commands += `USE DATABASE ${config.database}; `
- }
- if (config.compression) {
- commands += 'SET CLIENT KEY COMPRESSION TO 1; '
- }
- if (config.nonlinearizable) {
- commands += 'SET CLIENT KEY NONLINEARIZABLE TO 1; '
- }
- if (config.noBlob) {
- commands += 'SET CLIENT KEY NOBLOB TO 1; '
- }
- if (config.maxData) {
- commands += `SET CLIENT KEY MAXDATA TO ${config.maxData}; `
- }
- if (config.maxRows) {
- commands += `SET CLIENT KEY MAXROWS TO ${config.maxRows}; `
- }
- if (config.maxRowset) {
- commands += `SET CLIENT KEY MAXROWSET TO ${config.maxRowset}; `
- }
-
- return commands
-}
-
-//
-// ConnectionTransport
-//
-
-/** ConnectionTransport implements the underlying transport layer for the connection */
-export interface ConnectionTransport {
- /** True if connection is currently open */
- get connected(): boolean
- /* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
- connect(config: SQLiteCloudConfig, callback?: ErrorCallback): this
- /** Send a command, return the rowset/result or throw an error */
- processCommands(commands: string, callback?: ResultsCallback): this
- /** Disconnect from server, release transport. */
- close(): this
-}
diff --git a/src/drivers/connection-tls.ts b/src/drivers/connection-tls.ts
new file mode 100644
index 0000000..d59cc06
--- /dev/null
+++ b/src/drivers/connection-tls.ts
@@ -0,0 +1,258 @@
+/**
+ * connection-tls.ts - handles low level communication with sqlitecloud server via tls socket and binary protocol
+ */
+
+import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
+import { SQLiteCloudConnection } from './connection'
+import {
+ formatCommand,
+ hasCommandLength,
+ parseCommandLength,
+ popData,
+ decompressBuffer,
+ CMD_COMPRESSED,
+ CMD_ROWSET_CHUNK,
+ bufferEndsWith,
+ ROWSET_CHUNKS_END
+} from './protocol'
+import { getInitializationCommands, anonimizeError, anonimizeCommand } from './utilities'
+import { parseRowsetChunks } from './protocol'
+
+import net from 'net'
+import tls from 'tls'
+
+/**
+ * Implementation of SQLiteCloudConnection that connects directly to the database via tls socket and raw, binary protocol.
+ * Connects with plain socket with no encryption is the ?insecure=1 parameter is specified.
+ * SQLiteCloud low-level connection, will do messaging, handle socket, authentication, etc.
+ * A connection socket is established when the connection is created and closed when the connection is closed.
+ * All operations are serialized by waiting for any pending operations to complete. Once a connection is closed,
+ * it cannot be reopened and you must create a new connection.
+ */
+export class SQLiteCloudTlsConnection extends SQLiteCloudConnection {
+ /** Currently opened tls socket used to communicated with SQLiteCloud server */
+ private socket?: tls.TLSSocket | net.Socket | null
+
+ /** True if connection is open */
+ get connected(): boolean {
+ return !!this.socket
+ }
+
+ /* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
+ connectTransport(config: SQLiteCloudConfig, callback?: ErrorCallback): this {
+ // connection established while we were waiting in line?
+ console.assert(!this.connected, 'Connection already established')
+
+ // clear all listeners and call done in the operations queue
+ const finish: ResultsCallback = error => {
+ if (this.socket) {
+ this.socket.removeAllListeners('data')
+ this.socket.removeAllListeners('error')
+ this.socket.removeAllListeners('close')
+ if (error) {
+ this.close()
+ }
+ }
+ callback?.call(this, error)
+ }
+
+ this.config = config
+ const initializationCommands = getInitializationCommands(config)
+
+ if (config.insecure) {
+ // connect to plain socket, without encryption, only if insecure parameter specified
+ // this option is mainly for testing purposes and is not available on production nodes
+ // which would need to connect using tls and proper certificates as per code below
+ const connectionOptions: net.SocketConnectOpts = {
+ host: config.host,
+ port: config.port as number
+ }
+ this.socket = net.connect(connectionOptions, () => {
+ console.warn(`TlsConnection.connectTransport - connected to ${config.host as string}:${config.port as number} using insecure protocol`)
+ // send initialization commands
+ console.assert(this.socket, 'Connection already closed')
+ this.transportCommands(initializationCommands, error => {
+ if (error && this.socket) {
+ this.close()
+ }
+ if (callback) {
+ callback?.call(this, error)
+ callback = undefined
+ }
+ finish(error)
+ })
+ })
+ } else {
+ // connect to tls socket, initialize connection, setup event handlers
+ this.socket = tls.connect(this.config.port as number, this.config.host, this.config.tlsOptions, () => {
+ const tlsSocket = this.socket as tls.TLSSocket
+ if (!tlsSocket?.authorized) {
+ const anonimizedError = anonimizeError(tlsSocket.authorizationError)
+ console.error('Connection was not authorized', anonimizedError)
+ this.close()
+ finish(new SQLiteCloudError('Connection was not authorized', { cause: anonimizedError }))
+ } else {
+ // the connection was closed before it was even opened,
+ // eg. client closed the connection before the server accepted it
+ if (this.socket === null) {
+ finish(new SQLiteCloudError('Connection was closed before it was done opening'))
+ return
+ }
+
+ // send initialization commands
+ console.assert(this.socket, 'Connection already closed')
+ this.transportCommands(initializationCommands, error => {
+ if (error && this.socket) {
+ this.close()
+ }
+ if (callback) {
+ callback?.call(this, error)
+ callback = undefined
+ }
+ finish(error)
+ })
+ }
+ })
+ }
+
+ this.socket.on('close', () => {
+ this.socket = null
+ finish(new SQLiteCloudError('Connection was closed'))
+ })
+
+ this.socket.once('error', (error: any) => {
+ console.error('Connection error', error)
+ finish(new SQLiteCloudError('Connection error', { cause: error }))
+ })
+
+ return this
+ }
+
+ /** Will send a command immediately (no queueing), return the rowset/result or throw an error */
+ transportCommands(commands: string, callback?: ResultsCallback): this {
+ // connection needs to be established?
+ if (!this.socket) {
+ callback?.call(this, new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }))
+ return this
+ }
+
+ // compose commands following SCPC protocol
+ commands = formatCommand(commands)
+
+ let buffer = Buffer.alloc(0)
+ const rowsetChunks: Buffer[] = []
+ // const startedOn = new Date()
+
+ // define what to do if an answer does not arrive within the set timeout
+ let socketTimeout: NodeJS.Timeout
+
+ // clear all listeners and call done in the operations queue
+ const finish: ResultsCallback = (error, result) => {
+ clearTimeout(socketTimeout)
+ if (this.socket) {
+ this.socket.removeAllListeners('data')
+ this.socket.removeAllListeners('error')
+ this.socket.removeAllListeners('close')
+ }
+ if (callback) {
+ callback?.call(this, error, result)
+ callback = undefined
+ }
+ }
+
+ // define the Promise that waits for the server response
+ const readData = (data: Uint8Array) => {
+ try {
+ // on first ondata event, dataType is read from data, on subsequent ondata event, is read from buffer that is the concatanations of data received on each ondata event
+ let dataType = buffer.length === 0 ? data.subarray(0, 1).toString() : buffer.subarray(0, 1).toString('utf8')
+ buffer = Buffer.concat([buffer, data])
+ const commandLength = hasCommandLength(dataType)
+
+ if (commandLength) {
+ const commandLength = parseCommandLength(buffer)
+ const hasReceivedEntireCommand = buffer.length - buffer.indexOf(' ') - 1 >= commandLength ? true : false
+ if (hasReceivedEntireCommand) {
+ if (this.config?.verbose) {
+ let bufferString = buffer.toString('utf8')
+ if (bufferString.length > 1000) {
+ bufferString = bufferString.substring(0, 100) + '...' + bufferString.substring(bufferString.length - 40)
+ }
+ // const elapsedMs = new Date().getTime() - startedOn.getTime()
+ // console.debug(`Receive: ${bufferString} - ${elapsedMs}ms`)
+ }
+
+ // need to decompress this buffer before decoding?
+ if (dataType === CMD_COMPRESSED) {
+ ;({ buffer, dataType } = decompressBuffer(buffer))
+ }
+
+ if (dataType !== CMD_ROWSET_CHUNK) {
+ this.socket?.off('data', readData)
+ const { data } = popData(buffer)
+ finish(null, data)
+ } else {
+ // check if rowset received the ending chunk
+ if (bufferEndsWith(buffer, ROWSET_CHUNKS_END)) {
+ rowsetChunks.push(buffer)
+ const parsedData = parseRowsetChunks(rowsetChunks)
+ finish?.call(this, null, parsedData)
+ } else {
+ // no ending string? ask server for another chunk
+ rowsetChunks.push(buffer)
+ buffer = Buffer.alloc(0)
+ }
+ }
+ }
+ } else {
+ // command with no explicit len so make sure that the final character is a space
+ const lastChar = buffer.subarray(buffer.length - 1, buffer.length).toString('utf8')
+ if (lastChar == ' ') {
+ const { data } = popData(buffer)
+ finish(null, data)
+ }
+ }
+ } catch (error) {
+ console.assert(error instanceof Error)
+ if (error instanceof Error) {
+ finish(error)
+ }
+ }
+ }
+
+ this.socket?.once('close', () => {
+ finish(new SQLiteCloudError('Connection was closed', { cause: anonimizeCommand(commands) }))
+ })
+
+ this.socket?.write(commands, 'utf8', () => {
+ // @ts-ignore
+ socketTimeout = setTimeout(() => {
+ const timeoutError = new SQLiteCloudError('Request timed out', { cause: anonimizeCommand(commands) })
+ // console.debug(`Request timed out, config.timeout is ${this.config?.timeout as number}ms`, timeoutError)
+ finish(timeoutError)
+ }, this.config?.timeout)
+ this.socket?.on('data', readData)
+ })
+
+ this.socket?.once('error', (error: any) => {
+ console.error('Socket error', error)
+ this.close()
+ finish(new SQLiteCloudError('Socket error', { cause: anonimizeError(error) }))
+ })
+
+ return this
+ }
+
+ /** Disconnect from server, release connection. */
+ close(): this {
+ console.assert(this.socket !== null, 'TlsConnection.close - connection already closed')
+ this.operations.clear()
+ if (this.socket) {
+ this.socket.destroy()
+ this.socket = null
+ }
+ this.socket = undefined
+ return this
+ }
+}
+
+export default SQLiteCloudTlsConnection
diff --git a/src/transport-ws.ts b/src/drivers/connection-ws.ts
similarity index 84%
rename from src/transport-ws.ts
rename to src/drivers/connection-ws.ts
index 0371131..b11cb71 100644
--- a/src/transport-ws.ts
+++ b/src/drivers/connection-ws.ts
@@ -4,7 +4,7 @@
import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
import { SQLiteCloudRowset } from './rowset'
-import { ConnectionTransport } from './connection'
+import { SQLiteCloudConnection } from './connection'
import { io, Socket } from 'socket.io-client'
/**
@@ -13,9 +13,7 @@ import { io, Socket } from 'socket.io-client'
* requests by returning results and rowsets in json format. The gateway handles
* connect, disconnect, retries, order of operations, timeouts, etc.
*/
-export class WebSocketTransport implements ConnectionTransport {
- /** Configuration passed to connect */
- private config?: SQLiteCloudConfig
+export class SQLiteCloudWebsocketConnection extends SQLiteCloudConnection {
/** Socket.io used to communicated with SQLiteCloud server */
private socket?: Socket
@@ -25,7 +23,7 @@ export class WebSocketTransport implements ConnectionTransport {
}
/* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
- connect(config: SQLiteCloudConfig, callback?: ErrorCallback): this {
+ connectTransport(config: SQLiteCloudConfig, callback?: ErrorCallback): this {
try {
// connection established while we were waiting in line?
console.assert(!this.connected, 'Connection already established')
@@ -43,7 +41,7 @@ export class WebSocketTransport implements ConnectionTransport {
}
/** Will send a command immediately (no queueing), return the rowset/result or throw an error */
- processCommands(commands: string, callback?: ResultsCallback): this {
+ transportCommands(commands: string, callback?: ResultsCallback): this {
// connection needs to be established?
if (!this.socket) {
callback?.call(this, new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }))
@@ -58,7 +56,7 @@ export class WebSocketTransport implements ConnectionTransport {
const { data, metadata } = response
if (data && metadata) {
if (metadata.numberOfRows !== undefined && metadata.numberOfColumns !== undefined && metadata.columns !== undefined) {
- console.assert(Array.isArray(data), 'SQLiteCloudWebsocketConnection.processCommands - data is not an array')
+ console.assert(Array.isArray(data), 'SQLiteCloudWebsocketConnection.transportCommands - data is not an array')
// we can recreate a SQLiteCloudRowset from the response which we know to be an array of arrays
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const rowset = new SQLiteCloudRowset(metadata, data.flat())
@@ -75,12 +73,15 @@ export class WebSocketTransport implements ConnectionTransport {
/** Disconnect socket.io from server */
public close(): this {
- console.assert(this.socket !== null, 'WebsocketTransport.close - connection already closed')
+ console.assert(this.socket !== null, 'SQLiteCloudWebsocketConnection.close - connection already closed')
if (this.socket) {
this.socket?.close()
this.socket = undefined
}
+ this.operations.clear()
this.socket = undefined
return this
}
}
+
+export default SQLiteCloudWebsocketConnection
diff --git a/src/drivers/connection.ts b/src/drivers/connection.ts
new file mode 100644
index 0000000..524b811
--- /dev/null
+++ b/src/drivers/connection.ts
@@ -0,0 +1,101 @@
+/**
+ * connection.ts - base abstract class for sqlitecloud server connections
+ */
+
+import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback } from './types'
+import { validateConfiguration } from './utilities'
+import { OperationsQueue } from './queue'
+import { anonimizeCommand } from './utilities'
+
+/**
+ * Base class for SQLiteCloudConnection handles basics and defines methods.
+ * Actual connection management and communication with the server in concrete classes.
+ */
+export abstract class SQLiteCloudConnection {
+ /** Parse and validate provided connectionString or configuration */
+ constructor(config: SQLiteCloudConfig | string, callback?: ErrorCallback) {
+ if (typeof config === 'string') {
+ this.config = validateConfiguration({ connectionString: config })
+ } else {
+ this.config = validateConfiguration(config)
+ }
+
+ // connect transport layer to server
+ this.connect(callback)
+ }
+
+ /** Configuration passed by client or extracted from connection string */
+ protected config: SQLiteCloudConfig
+
+ /** Operations are serialized by waiting an any pending promises */
+ protected operations = new OperationsQueue()
+
+ //
+ // internal methods (some are implemented in concrete classes using different transport layers)
+ //
+
+ /** Connect will establish a tls or websocket transport to the server based on configuration and environment */
+ protected connect(callback?: ErrorCallback): this {
+ this.operations.enqueue(done => {
+ this.connectTransport(this.config, error => {
+ if (error) {
+ console.error(
+ `SQLiteCloudConnection.connect - error connecting ${this.config.host as string}:${this.config.port as number} ${error.toString()}`,
+ error
+ )
+ this.close()
+ }
+ callback?.call(this, error || null)
+ done(error)
+ })
+ })
+ return this
+ }
+
+ /* Opens a connection with the server and sends the initialization commands */
+ protected abstract connectTransport(config: SQLiteCloudConfig, callback?: ErrorCallback): this
+
+ /** Send a command, return the rowset/result or throw an error */
+ protected abstract transportCommands(commands: string, callback?: ResultsCallback): this
+
+ /** Will log to console if verbose mode is enabled */
+ protected log(message: string, ...optionalParams: any[]): void {
+ if (this.config.verbose) {
+ message = anonimizeCommand(message)
+ console.log(`${new Date().toISOString()} ${this.config.clientId as string}: ${message}`, ...optionalParams)
+ }
+ }
+
+ //
+ // public methods (some are abstract and implemented in concrete classes)
+ //
+
+ /** Returns true if connection is open */
+ public abstract get connected(): boolean
+
+ /** Enable verbose logging for debug purposes */
+ public verbose(): void {
+ this.config.verbose = true
+ }
+
+ /** Will enquee a command to be executed and callback with the resulting rowset/result/error */
+ public sendCommands(commands: string, callback?: ResultsCallback): this {
+ this.operations.enqueue(done => {
+ if (!this.connected) {
+ const error = new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' })
+ callback?.call(this, error)
+ done(error)
+ }
+
+ this.transportCommands(commands, (error, result) => {
+ callback?.call(this, error, result)
+ done(error)
+ })
+ })
+
+ return this
+ }
+
+ /** Disconnect from server, release transport. */
+ public abstract close(): this
+}
diff --git a/src/database.ts b/src/drivers/database.ts
similarity index 91%
rename from src/database.ts
rename to src/drivers/database.ts
index 658a8b0..6c42ab5 100644
--- a/src/database.ts
+++ b/src/drivers/database.ts
@@ -17,6 +17,7 @@ import { prepareSql, popCallback } from './utilities'
import { Statement } from './statement'
import { ErrorCallback, ResultsCallback, RowCallback, RowsCallback } from './types'
import EventEmitter from 'eventemitter3'
+import { isBrowser } from './utilities'
// Uses eventemitter3 instead of node events for browser compatibility
// https://github.com/primus/eventemitter3
@@ -45,7 +46,11 @@ export class Database extends EventEmitter {
// mode is ignored for now
// opens first connection to the database automatically
- this.getConnection(callback as ResultsCallback)
+ this.getConnection((error, _connection) => {
+ if (callback) {
+ callback.call(this, error)
+ }
+ })
}
/** Configuration used to open database connections */
@@ -64,17 +69,47 @@ export class Database extends EventEmitter {
if (this.connections?.length > 0) {
callback?.call(this, null, this.connections[0])
} else {
- this.connections.push(
- new SQLiteCloudConnection(this.config, error => {
- if (error) {
- this.handleError(this.connections[0], error, callback)
- } else {
- console.assert
- callback?.call(this, null, this.connections[0])
- this.emitEvent('open')
- }
- })
- )
+ // connect using websocket if tls is not supported or if explicitly requested
+ const useWebsocket = isBrowser || this.config?.useWebsocket || this.config?.gatewayUrl
+ if (useWebsocket) {
+ // socket.io transport works in both node.js and browser environments and connects via SQLite Cloud Gateway
+ import('./connection-ws')
+ .then(module => {
+ this.connections.push(
+ new module.default(this.config, error => {
+ if (error) {
+ this.handleError(this.connections[0], error, callback)
+ } else {
+ console.assert
+ callback?.call(this, null, this.connections[0])
+ this.emitEvent('open')
+ }
+ })
+ )
+ })
+ .catch(error => {
+ this.handleError(null, error, callback)
+ })
+ } else {
+ // tls sockets work only in node.js environments
+ import('./connection-tls')
+ .then(module => {
+ this.connections.push(
+ new module.default(this.config, error => {
+ if (error) {
+ this.handleError(this.connections[0], error, callback)
+ } else {
+ console.assert
+ callback?.call(this, null, this.connections[0])
+ this.emitEvent('open')
+ }
+ })
+ )
+ })
+ .catch(error => {
+ this.handleError(null, error, callback)
+ })
+ }
}
}
diff --git a/src/drivers/protocol.ts b/src/drivers/protocol.ts
new file mode 100644
index 0000000..79750ad
--- /dev/null
+++ b/src/drivers/protocol.ts
@@ -0,0 +1,321 @@
+//
+// protocol.ts - low level protocol handling for SQLiteCloud transport
+//
+
+import { SQLiteCloudError, type SQLCloudRowsetMetadata, type SQLiteCloudDataTypes } from './types'
+import { SQLiteCloudRowset } from './rowset'
+
+const lz4 = require('lz4js')
+
+// The server communicates with clients via commands defined in
+// SQLiteCloud Server Protocol (SCSP), see more at:
+// https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md
+
+export const CMD_STRING = '+'
+export const CMD_ZEROSTRING = '!'
+export const CMD_ERROR = '-'
+export const CMD_INT = ':'
+export const CMD_FLOAT = ','
+export const CMD_ROWSET = '*'
+export const CMD_ROWSET_CHUNK = '/'
+export const CMD_JSON = '#'
+export const CMD_NULL = '_'
+export const CMD_BLOB = '$'
+export const CMD_COMPRESSED = '%'
+export const CMD_COMMAND = '^'
+export const CMD_ARRAY = '='
+// const CMD_RAWJSON = '{'
+// const CMD_PUBSUB = '|'
+// const CMD_RECONNECT = '@'
+
+// To mark the end of the Rowset, the special string /LEN 0 0 0 is sent (LEN is always 6 in this case)
+// https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md#scsp-rowset-chunk
+export const ROWSET_CHUNKS_END = '/6 0 0 0 '
+
+//
+// utility functions
+//
+
+/** Analyze first character to check if corresponding data type has LEN */
+export function hasCommandLength(firstCharacter: string): boolean {
+ return firstCharacter == CMD_INT || firstCharacter == CMD_FLOAT || firstCharacter == CMD_NULL ? false : true
+}
+
+/** Analyze a command with explict LEN and extract it */
+export function parseCommandLength(data: Buffer): number {
+ return parseInt(data.subarray(1, data.indexOf(' ')).toString('utf8'))
+}
+
+/** Receive a compressed buffer, decompress with lz4, return buffer and datatype */
+export function decompressBuffer(buffer: Buffer): { buffer: Buffer; dataType: string } {
+ const spaceIndex = buffer.indexOf(' ')
+ buffer = buffer.subarray(spaceIndex + 1)
+
+ // extract compressed size
+ const compressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'))
+ buffer = buffer.subarray(buffer.indexOf(' ') + 1)
+
+ // extract decompressed size
+ const decompressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'))
+ buffer = buffer.subarray(buffer.indexOf(' ') + 1)
+
+ // extract compressed dataType
+ const dataType = buffer.subarray(0, 1).toString('utf8')
+ const decompressedBuffer = Buffer.alloc(decompressedSize)
+ const compressedBuffer = buffer.subarray(buffer.length - compressedSize)
+
+ // lz4js library is javascript and doesn't have types so we silence the type check
+ // eslint-disable-next-line
+ const decompressionResult: number = lz4.decompressBlock(compressedBuffer, decompressedBuffer, 0, compressedSize, 0)
+ buffer = Buffer.concat([buffer.subarray(0, buffer.length - compressedSize), decompressedBuffer])
+ if (decompressionResult <= 0 || decompressionResult !== decompressedSize) {
+ throw new Error(`lz4 decompression error at offset ${decompressionResult}`)
+ }
+
+ return { buffer, dataType }
+}
+
+/** Parse error message or extended error message */
+export function parseError(buffer: Buffer, spaceIndex: number): never {
+ const errorBuffer = buffer.subarray(spaceIndex + 1)
+ const errorString = errorBuffer.toString('utf8')
+ const parts = errorString.split(' ')
+
+ let errorCodeStr = parts.shift() || '0' // Default errorCode is '0' if not present
+ let extErrCodeStr = '0' // Default extended error code
+ let offsetCodeStr = '-1' // Default offset code
+
+ // Split the errorCode by ':' to check for extended error codes
+ const errorCodeParts = errorCodeStr.split(':')
+ errorCodeStr = errorCodeParts[0]
+ if (errorCodeParts.length > 1) {
+ extErrCodeStr = errorCodeParts[1]
+ if (errorCodeParts.length > 2) {
+ offsetCodeStr = errorCodeParts[2]
+ }
+ }
+
+ // Rest of the error string is the error message
+ const errorMessage = parts.join(' ')
+
+ // Parse error codes to integers safely, defaulting to 0 if NaN
+ const errorCode = parseInt(errorCodeStr)
+ const extErrCode = parseInt(extErrCodeStr)
+ const offsetCode = parseInt(offsetCodeStr)
+
+ // create an Error object and add the custom properties
+ throw new SQLiteCloudError(errorMessage, {
+ errorCode: errorCode.toString(),
+ externalErrorCode: extErrCode.toString(),
+ offsetCode
+ })
+}
+
+/** Parse an array of items (each of which will be parsed by type separately) */
+export function parseArray(buffer: Buffer, spaceIndex: number): SQLiteCloudDataTypes[] {
+ const parsedData = []
+
+ const array = buffer.subarray(spaceIndex + 1, buffer.length)
+ const numberOfItems = parseInt(array.subarray(0, spaceIndex - 2).toString('utf8'))
+ let arrayItems = array.subarray(array.indexOf(' ') + 1, array.length)
+
+ for (let i = 0; i < numberOfItems; i++) {
+ const { data, fwdBuffer: buffer } = popData(arrayItems)
+ parsedData.push(data)
+ arrayItems = buffer
+ }
+
+ return parsedData as SQLiteCloudDataTypes[]
+}
+
+/** Parse header in a rowset or chunk of a chunked rowset */
+export function parseRowsetHeader(buffer: Buffer): { index: number; metadata: SQLCloudRowsetMetadata; fwdBuffer: Buffer } {
+ const index = parseInt(buffer.subarray(0, buffer.indexOf(':') + 1).toString())
+ buffer = buffer.subarray(buffer.indexOf(':') + 1)
+
+ // extract rowset header
+ const { data, fwdBuffer } = popIntegers(buffer, 3)
+
+ return {
+ index,
+ metadata: {
+ version: data[0],
+ numberOfRows: data[1],
+ numberOfColumns: data[2],
+ columns: []
+ },
+ fwdBuffer
+ }
+}
+
+/** Extract column names and, optionally, more metadata out of a rowset's header */
+function parseRowsetColumnsMetadata(buffer: Buffer, metadata: SQLCloudRowsetMetadata): Buffer {
+ function popForward() {
+ const { data, fwdBuffer: fwdBuffer } = popData(buffer) // buffer in parent scope
+ buffer = fwdBuffer
+ return data
+ }
+
+ for (let i = 0; i < metadata.numberOfColumns; i++) {
+ metadata.columns.push({ name: popForward() as string })
+ }
+
+ // extract additional metadata if rowset has version 2
+ if (metadata.version == 2) {
+ for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].type = popForward() as string
+ for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].database = popForward() as string
+ for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].table = popForward() as string
+ for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].column = popForward() as string // original column name
+
+ for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].notNull = popForward() as number
+ for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].primaryKey = popForward() as number
+ for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].autoIncrement = popForward() as number
+ }
+
+ return buffer
+}
+
+/** Parse a regular rowset (no chunks) */
+function parseRowset(buffer: Buffer, spaceIndex: number): SQLiteCloudRowset {
+ buffer = buffer.subarray(spaceIndex + 1, buffer.length)
+
+ const { metadata, fwdBuffer } = parseRowsetHeader(buffer)
+ buffer = parseRowsetColumnsMetadata(fwdBuffer, metadata)
+
+ // decode each rowset item
+ const data = []
+ for (let j = 0; j < metadata.numberOfRows * metadata.numberOfColumns; j++) {
+ const { data: rowData, fwdBuffer } = popData(buffer)
+ data.push(rowData)
+ buffer = fwdBuffer
+ }
+
+ console.assert(data && data.length === metadata.numberOfRows * metadata.numberOfColumns, 'SQLiteCloudConnection.parseRowset - invalid rowset data')
+ return new SQLiteCloudRowset(metadata, data)
+}
+
+export function bufferStartsWith(buffer: Buffer, prefix: string): boolean {
+ return buffer.length >= prefix.length && buffer.subarray(0, prefix.length).toString('utf8') === prefix
+}
+
+export function bufferEndsWith(buffer: Buffer, suffix: string): boolean {
+ return buffer.length >= suffix.length && buffer.subarray(buffer.length - suffix.length, buffer.length).toString('utf8') === suffix
+}
+
+/**
+ * Parse a chunk of a chunked rowset command, eg:
+ * *LEN 0:VERS NROWS NCOLS DATA
+ * @see https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md#scsp-rowset-chunk
+ */
+export function parseRowsetChunks(buffers: Buffer[]): SQLiteCloudRowset {
+ let buffer = Buffer.concat(buffers)
+ if (!bufferStartsWith(buffer, CMD_ROWSET_CHUNK) || !bufferEndsWith(buffer, ROWSET_CHUNKS_END)) {
+ throw new Error('SQLiteCloudConnection.parseRowsetChunks - invalid chunks buffer')
+ }
+
+ let metadata: SQLCloudRowsetMetadata = { version: 1, numberOfColumns: 0, numberOfRows: 0, columns: [] }
+ const data: any[] = []
+
+ // validate and skip data type
+ const dataType = buffer.subarray(0, 1).toString()
+ console.assert(dataType === CMD_ROWSET_CHUNK)
+ buffer = buffer.subarray(buffer.indexOf(' ') + 1)
+
+ while (buffer.length > 0 && !bufferStartsWith(buffer, ROWSET_CHUNKS_END)) {
+ // chunk header, eg: 0:VERS NROWS NCOLS
+ const { index: chunkIndex, metadata: chunkMetadata, fwdBuffer } = parseRowsetHeader(buffer)
+ buffer = fwdBuffer
+
+ // first chunk? extract columns metadata
+ if (chunkIndex === 1) {
+ metadata = chunkMetadata
+ buffer = parseRowsetColumnsMetadata(buffer, metadata)
+ } else {
+ metadata.numberOfRows += chunkMetadata.numberOfRows
+ }
+
+ // extract single rowset row
+ for (let k = 0; k < chunkMetadata.numberOfRows * metadata.numberOfColumns; k++) {
+ const { data: itemData, fwdBuffer } = popData(buffer)
+ data.push(itemData)
+ buffer = fwdBuffer
+ }
+ }
+
+ console.assert(data && data.length === metadata.numberOfRows * metadata.numberOfColumns, 'parseRowsetChunks - invalid rowset data')
+ const rowset = new SQLiteCloudRowset(metadata, data)
+ // console.debug(`parseRowsetChunks - ${rowset.numberOfRows} rows, ${rowset.numberOfColumns} columns`)
+ return rowset
+}
+
+/** Pop one or more space separated integers from beginning of buffer, move buffer forward */
+function popIntegers(buffer: Buffer, numberOfIntegers = 1): { data: number[]; fwdBuffer: Buffer } {
+ const data: number[] = []
+ for (let i = 0; i < numberOfIntegers; i++) {
+ const spaceIndex = buffer.indexOf(' ')
+ data[i] = parseInt(buffer.subarray(0, spaceIndex).toString())
+ buffer = buffer.subarray(spaceIndex + 1)
+ }
+ return { data, fwdBuffer: buffer }
+}
+
+/** Parse command, extract its data, return the data and the buffer moved to the first byte after the command */
+export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCloudRowset; fwdBuffer: Buffer } {
+ function popResults(data: any) {
+ const fwdBuffer = buffer.subarray(commandEnd)
+ return { data, fwdBuffer }
+ }
+
+ // first character is the data type
+ console.assert(buffer && buffer instanceof Buffer)
+ const dataType: string = buffer.subarray(0, 1).toString('utf8')
+ console.assert(dataType !== CMD_COMPRESSED, "Compressed data shouldn't be decompressed before parsing")
+ console.assert(dataType !== CMD_ROWSET_CHUNK, 'Chunked data should be parsed by parseRowsetChunks')
+
+ let spaceIndex = buffer.indexOf(' ')
+ if (spaceIndex === -1) {
+ spaceIndex = buffer.length - 1
+ }
+
+ let commandEnd = -1
+ if (dataType === CMD_INT || dataType === CMD_FLOAT || dataType === CMD_NULL) {
+ commandEnd = spaceIndex + 1
+ } else {
+ const commandLength = parseInt(buffer.subarray(1, spaceIndex).toString())
+ commandEnd = spaceIndex + 1 + commandLength
+ }
+
+ switch (dataType) {
+ case CMD_INT:
+ return popResults(parseInt(buffer.subarray(1, spaceIndex).toString()))
+ case CMD_FLOAT:
+ return popResults(parseFloat(buffer.subarray(1, spaceIndex).toString()))
+ case CMD_NULL:
+ return popResults(null)
+ case CMD_STRING:
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'))
+ case CMD_ZEROSTRING:
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd - 1).toString('utf8'))
+ case CMD_COMMAND:
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'))
+ case CMD_JSON:
+ return popResults(JSON.parse(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8')))
+ case CMD_BLOB:
+ return popResults(buffer.subarray(spaceIndex + 1, commandEnd))
+ case CMD_ARRAY:
+ return popResults(parseArray(buffer, spaceIndex))
+ case CMD_ROWSET:
+ return popResults(parseRowset(buffer, spaceIndex))
+ case CMD_ERROR:
+ parseError(buffer, spaceIndex) // throws custom error
+ break
+ }
+
+ throw new TypeError(`Data type: ${dataType} is not defined in SCSP`)
+}
+
+/** Format a command to be sent via SCSP protocol */
+export function formatCommand(command: string): string {
+ const commandLength = Buffer.byteLength(command, 'utf-8')
+ return `+${commandLength} ${command}`
+}
diff --git a/src/drivers/queue.ts b/src/drivers/queue.ts
new file mode 100644
index 0000000..9a79973
--- /dev/null
+++ b/src/drivers/queue.ts
@@ -0,0 +1,45 @@
+//
+// queue.ts - simple task queue used to linearize async operations
+//
+
+export type OperationCallback = (error: Error | null) => void
+export type Operation = (done: OperationCallback) => void
+
+export class OperationsQueue {
+ private queue: Operation[] = []
+ private isProcessing = false
+
+ /** Add operations to the queue, process immediately if possible, else wait for previous operations to complete */
+ public enqueue(operation: Operation): void {
+ this.queue.push(operation)
+ if (!this.isProcessing) {
+ this.processNext()
+ }
+ }
+
+ /** Clear the queue */
+ public clear(): void {
+ this.queue = []
+ this.isProcessing = false
+ }
+
+ /** Process the next operation in the queue */
+ private processNext(): void {
+ if (this.queue.length === 0) {
+ this.isProcessing = false
+ return
+ }
+
+ this.isProcessing = true
+ const operation = this.queue.shift()
+ operation?.(() => {
+ // could receive (error) => { ...
+ // if (error) {
+ // console.warn('OperationQueue.processNext - error in operation', error)
+ // }
+
+ // process the next operation in the queue
+ this.processNext()
+ })
+ }
+}
diff --git a/src/rowset.ts b/src/drivers/rowset.ts
similarity index 100%
rename from src/rowset.ts
rename to src/drivers/rowset.ts
diff --git a/src/statement.ts b/src/drivers/statement.ts
similarity index 100%
rename from src/statement.ts
rename to src/drivers/statement.ts
diff --git a/src/types.ts b/src/drivers/types.ts
similarity index 100%
rename from src/types.ts
rename to src/drivers/types.ts
diff --git a/src/utilities.ts b/src/drivers/utilities.ts
similarity index 80%
rename from src/utilities.ts
rename to src/drivers/utilities.ts
index 458e078..a6c0ce7 100644
--- a/src/utilities.ts
+++ b/src/drivers/utilities.ts
@@ -16,6 +16,56 @@ export const isNode: boolean = typeof process !== 'undefined' && process.version
// utility methods
//
+/** Messages going to the server are sometimes logged when error conditions occour and need to be stripped of user credentials */
+export function anonimizeCommand(message: string): string {
+ // hide password in AUTH command if needed
+ message = message.replace(/USER \S+/, 'USER ******')
+ message = message.replace(/PASSWORD \S+?(?=;)/, 'PASSWORD ******')
+ message = message.replace(/HASH \S+?(?=;)/, 'HASH ******')
+ return message
+}
+
+/** Strip message code in error of user credentials */
+export function anonimizeError(error: Error): Error {
+ if (error?.message) {
+ error.message = anonimizeCommand(error.message)
+ }
+ return error
+}
+
+/** Initialization commands sent to database when connection is established */
+export function getInitializationCommands(config: SQLiteCloudConfig): string {
+ // first user authentication, then all other commands
+ let commands = `AUTH USER ${config.username || ''} ${config.passwordHashed ? 'HASH' : 'PASSWORD'} ${config.password || ''}; `
+
+ if (config.database) {
+ if (config.createDatabase && !config.dbMemory) {
+ commands += `CREATE DATABASE ${config.database} IF NOT EXISTS; `
+ }
+ commands += `USE DATABASE ${config.database}; `
+ }
+ if (config.compression) {
+ commands += 'SET CLIENT KEY COMPRESSION TO 1; '
+ }
+ if (config.nonlinearizable) {
+ commands += 'SET CLIENT KEY NONLINEARIZABLE TO 1; '
+ }
+ if (config.noBlob) {
+ commands += 'SET CLIENT KEY NOBLOB TO 1; '
+ }
+ if (config.maxData) {
+ commands += `SET CLIENT KEY MAXDATA TO ${config.maxData}; `
+ }
+ if (config.maxRows) {
+ commands += `SET CLIENT KEY MAXROWS TO ${config.maxRows}; `
+ }
+ if (config.maxRowset) {
+ commands += `SET CLIENT KEY MAXROWSET TO ${config.maxRowset}; `
+ }
+
+ return commands
+}
+
/** Takes a generic value and escapes it so it can replace ? as a binding in a prepared SQL statement */
export function escapeSqlParameter(param: SQLiteCloudDataTypes): string {
if (param === null || param === undefined) {
@@ -121,6 +171,7 @@ export function popCallback(
/** Validate configuration, apply defaults, throw if something is missing or misconfigured */
export function validateConfiguration(config: SQLiteCloudConfig): SQLiteCloudConfig {
+ console.assert(config, 'SQLiteCloudConnection.validateConfiguration - missing config')
if (config.connectionString) {
config = {
...config,
diff --git a/src/gateway/connection-bun.test.ts b/src/gateway/connection-bun.test.ts
new file mode 100644
index 0000000..a4e2576
--- /dev/null
+++ b/src/gateway/connection-bun.test.ts
@@ -0,0 +1,246 @@
+//
+// gateway.test.ts - bun tests for
+//
+
+// MUST RUN USING BUN TEST RUNNER, EG:
+// bun test connection-bun.test.ts --watch
+
+import { SQLiteCloudError } from '../drivers/types'
+import { SQLiteCloudBunConnection } from './connection-bun'
+import { expect, test, describe, beforeEach, afterEach } from 'bun:test'
+
+let CHINOOK_DATABASE_URL = process.env['CHINOOK_DATABASE_URL'] as string
+console.assert(CHINOOK_DATABASE_URL, 'CHINOOK_DATABASE_URL is required')
+
+async function getConnection(): Promise {
+ return new Promise((resolve, reject) => {
+ const connection = new SQLiteCloudBunConnection(CHINOOK_DATABASE_URL, error => {
+ if (error) {
+ reject(error)
+ }
+ resolve(connection)
+ })
+ })
+}
+
+async function sendCommands(connection: SQLiteCloudBunConnection, command: string): Promise {
+ return new Promise((resolve, reject) => {
+ connection.sendCommands(command, (error, result) => {
+ if (error) {
+ reject(error)
+ }
+ resolve(result)
+ })
+ })
+}
+
+describe('SQLiteCloudBunConnection', () => {
+ // test different ways to connect
+ describe('connecting', () => {
+ test('can connect using bun socket', done => {
+ new SQLiteCloudBunConnection(CHINOOK_DATABASE_URL, error => {
+ if (error) {
+ console.error('Error connecting to database', error)
+ }
+ done(error)
+ })
+ })
+ })
+
+ // test command exercise different data types
+ describe('test commands', () => {
+ let chinookConnection: SQLiteCloudBunConnection
+
+ beforeEach(async () => {
+ chinookConnection = await getConnection()
+ })
+
+ afterEach(() => {
+ if (chinookConnection) {
+ chinookConnection.close()
+ }
+ })
+
+ test('should test null', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST NULL')
+ expect(results).toBeNull()
+ })
+
+ test('should test integer', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST INTEGER')
+ expect(results).toBe(123456)
+ })
+
+ test('should test float', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST FLOAT')
+ expect(results).toBe(3.1415926)
+ })
+
+ test('should test string', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST STRING')
+ expect(results).toBe('Hello World, this is a test string.')
+ })
+
+ test('should test zero string', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST ZERO_STRING')
+ expect(results).toBe('Hello World, this is a zero-terminated test string.')
+ })
+
+ test('should test string0', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST STRING0')
+ expect(results).toBe('')
+ })
+
+ test('should test command', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST COMMAND')
+ expect(results).toBe('PING')
+ })
+
+ test('should test json', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST JSON')
+ expect(results).toEqual({
+ 'msg-from': { class: 'soldier', name: 'Wixilav' },
+ 'msg-to': { class: 'supreme-commander', name: '[Redacted]' },
+ 'msg-type': ['0xdeadbeef', 'irc log'],
+ 'msg-log': [
+ 'soldier: Boss there is a slight problem with the piece offering to humans',
+ 'supreme-commander: Explain yourself soldier!',
+ "soldier: Well they don't seem to move anymore...",
+ 'supreme-commander: Oh snap, I came here to see them twerk!'
+ ]
+ })
+ })
+
+ test('should test blob', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST BLOB')
+ expect(typeof results).toBe('object')
+ expect(results).toBeInstanceOf(Buffer)
+ const bufferrowset = results as Buffer
+ expect(bufferrowset.length).toBe(1000)
+ })
+
+ test('should test blob0', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST BLOB0')
+ expect(typeof results).toBe('object')
+ expect(results).toBeInstanceOf(Buffer)
+ const bufferrowset = results as Buffer
+ expect(bufferrowset.length).toBe(0)
+ })
+
+ test('should test error', done => {
+ chinookConnection.sendCommands('TEST ERROR', (error, results) => {
+ expect(error).toBeDefined()
+ expect(error).toBeInstanceOf(SQLiteCloudError)
+ expect(results).toBeNull()
+
+ const sqliteCloudError = error as SQLiteCloudError
+ expect(sqliteCloudError.message).toBe('This is a test error message with a devil error code.')
+ expect(sqliteCloudError.errorCode).toBe('66666')
+ expect(sqliteCloudError.externalErrorCode).toBe('0')
+ expect(sqliteCloudError.offsetCode).toBe(-1)
+
+ done()
+ })
+ })
+
+ test('should test exterror', done => {
+ chinookConnection.sendCommands('TEST EXTERROR', (error, results) => {
+ expect(error).toBeDefined()
+ expect(error).toBeInstanceOf(SQLiteCloudError)
+ expect(results).toBeNull()
+
+ const sqliteCloudError = error as SQLiteCloudError
+ expect(sqliteCloudError.message).toBe('This is a test error message with an extcode and a devil error code.')
+ expect(sqliteCloudError.errorCode).toBe('66666')
+ expect(sqliteCloudError.externalErrorCode).toBe('333')
+ expect(sqliteCloudError.offsetCode).toBe(-1)
+
+ done()
+ })
+ })
+
+ test('should test array', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST ARRAY')
+ expect(Array.isArray(results)).toBe(true)
+ const arrayrowset = results as Array
+ expect(arrayrowset.length).toBe(5)
+ expect(arrayrowset[0]).toBe('Hello World')
+ expect(arrayrowset[1]).toBe(123456)
+ expect(arrayrowset[2]).toBe(3.1415)
+ expect(arrayrowset[3]).toBeNull()
+ })
+
+ test('should test rowset', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST ROWSET')
+ expect(results.numberOfRows).toBe(41)
+ expect(results.numberOfColumns).toBe(2)
+ expect(results.version == 1 || results.version == 2).toBeTruthy()
+ expect(results.columnsNames).toEqual(['key', 'value'])
+ })
+
+ test.only('should test chunked rowset', async () => {
+ const results = await sendCommands(chinookConnection, 'TEST ROWSET_CHUNK')
+ expect(results.numberOfRows).toBe(147)
+ expect(results.numberOfColumns).toBe(1)
+ expect(results.columnsNames).toEqual(['key'])
+
+ expect(results[0]['key']).toBe('REINDEX')
+ expect(results[1]['key']).toBe('INDEXED')
+ expect(results[2]['key']).toBe('INDEX')
+ expect(results[3]['key']).toBe('DESC')
+ })
+ })
+
+ // various select statements
+ describe('select', () => {
+ test('can run simple select', done => {
+ const connection = new SQLiteCloudBunConnection(CHINOOK_DATABASE_URL, error => {
+ if (error) {
+ done(error)
+ }
+ connection.sendCommands("SELECT 2 'COLONNA'", (error, result) => {
+ if (!error) {
+ expect(result).toEqual([{ COLONNA: 2 }])
+ }
+ done(error)
+ })
+ })
+ })
+
+ test('can list tables', async done => {
+ const connection = await getConnection()
+ const results = await sendCommands(connection, 'LIST TABLES')
+ expect(results.numberOfColumns).toBe(6)
+ expect(results.numberOfRows).toBe(11)
+ done()
+ })
+
+ test('can repeat commands', async () => {
+ const connection = await getConnection()
+ for (let i = 0; i < 50; i++) {
+ const results = await sendCommands(connection, `SELECT ${i} 'COLONNA'`)
+ expect(results.numberOfColumns).toBe(1)
+ expect(results.numberOfRows).toBe(1)
+ expect(results).toEqual([{ COLONNA: i }])
+ }
+ connection.close()
+ })
+
+ test('can send long commands', async () => {
+ const connection = await getConnection()
+
+ let sql = ''
+ let i = 0
+ for (; i < 250; i++) {
+ sql += `SELECT ${i} AS counter; `
+ }
+
+ // receives only one result for last statement
+ const results = await sendCommands(connection, sql)
+ expect(results.numberOfColumns).toBe(1)
+ expect(results.numberOfRows).toBe(1)
+ expect(results).toEqual([{ counter: i - 1 }])
+ connection.close()
+ })
+ })
+})
diff --git a/src/gateway/connection-bun.ts b/src/gateway/connection-bun.ts
new file mode 100644
index 0000000..5703dde
--- /dev/null
+++ b/src/gateway/connection-bun.ts
@@ -0,0 +1,235 @@
+/**
+ * transport-bun.ts - handles low level communication with sqlitecloud server via specific Bun APIs for tls socket and binary protocol
+ */
+
+import { type SQLiteCloudConfig, SQLiteCloudError, type ErrorCallback, type ResultsCallback } from '../drivers/types'
+import { SQLiteCloudConnection } from '../drivers/connection'
+import { getInitializationCommands } from '../drivers/utilities'
+import {
+ formatCommand,
+ hasCommandLength,
+ parseCommandLength,
+ popData,
+ decompressBuffer,
+ parseRowsetChunks,
+ CMD_COMPRESSED,
+ CMD_ROWSET_CHUNK,
+ bufferEndsWith,
+ ROWSET_CHUNKS_END
+} from '../drivers/protocol'
+import type { Socket } from 'bun'
+
+/**
+ * Implementation of SQLiteCloudConnection that connects to the database using specific Bun APIs
+ * that connect to native sockets or tls sockets and communicates via raw, binary protocol.
+ */
+export class SQLiteCloudBunConnection extends SQLiteCloudConnection {
+ /** Currently opened bun socket used to communicated with SQLiteCloud server */
+ private socket?: Socket
+
+ /** True if connection is open */
+ get connected(): boolean {
+ return !!this.socket
+ }
+
+ /* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ connectTransport(config: SQLiteCloudConfig, callback?: ErrorCallback): this {
+ console.debug(`-> connecting ${config?.host as string}:${config?.port as number}`)
+ console.assert(!this.connected, 'BunSocketTransport.connect - connection already established')
+ this.config = config
+
+ void Bun.connect({
+ hostname: config.host as string,
+ port: config.port as number,
+ tls: config.insecure ? false : true,
+
+ socket: {
+ open: socket => {
+ // console.debug('BunSocketTransport.connect - open')
+ this.socket = socket
+
+ // send initialization commands
+ const commands = getInitializationCommands(config)
+ this.transportCommands(commands, error => {
+ // any results are ignored
+ if (error) {
+ console.error('BunSocketTransport.connect - error initializing connection', error)
+ callback?.call(this, error)
+ } else {
+ // console.debug(`<- connected ${config?.host}:${config?.port}`)
+ callback?.call(this, null)
+ }
+ })
+ },
+
+ // connection failed
+ connectError: (socket, error) => {
+ console.error('BunTransport.connect - connectError', error)
+ this.close()
+ callback?.call(this, error)
+ },
+
+ // data received is processed by onData chunk by chunk
+ data: (socket, data) => {
+ this.processCommandsData(socket, data)
+ },
+
+ // close is received when we call socket.end() or when the server closes the connection
+ close: socket => {
+ if (this.socket) {
+ this.close()
+ this.processCommandsFinish(new SQLiteCloudError('Connection was closed'))
+ }
+ },
+
+ drain: socket => {
+ // console.debug('BunTransport.connect - drain')
+ },
+
+ error: (socket, error) => {
+ this.close()
+ this.processCommandsFinish(new SQLiteCloudError('Connection error', { cause: error }))
+ },
+
+ // connection closed by server
+ end: socket => {
+ if (this.socket) {
+ this.close()
+ this.processCommandsFinish(new SQLiteCloudError('Connection ended'))
+ }
+ },
+
+ // connection timed out
+ timeout: socket => {
+ this.close()
+ this.processCommandsFinish(new SQLiteCloudError('Connection timed out'))
+ }
+ }
+ })
+ .catch(error => {
+ console.debug('BunTransport.connect - error', error)
+ this.close()
+ callback?.call(this, error)
+ })
+ .then(socket => {
+ // connection established
+ })
+
+ return this
+ }
+
+ /** Will send a command immediately (no queueing), return the rowset/result or throw an error */
+ transportCommands(commands: string, callback?: ResultsCallback): this {
+ // connection needs to be established?
+ if (!this.socket) {
+ callback?.call(this, new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }))
+ return this
+ }
+
+ // reset buffer and rowset chunks, define response callback
+ this.buffer = Buffer.alloc(0)
+ this.startedOn = new Date()
+ this.processCallback = callback
+
+ // compose commands following SCPC protocol
+ const formattedCommands = formatCommand(commands)
+ if (this.config?.verbose) {
+ console.debug(`-> ${formattedCommands}`)
+ }
+ this.socket.write(formattedCommands)
+ this.socket.flush()
+
+ return this
+ }
+
+ // processCommands sets up empty buffers, results callback then send the command to the server via socket.write
+ // onData is called when data is received, it will process the data until all data is retrieved for a response
+ // when response is complete or there's an error, finish is called to call the results callback set by processCommands...
+
+ // buffer to accumulate incoming data until an whole command is received and can be parsed
+ private buffer: Buffer = Buffer.alloc(0)
+ private startedOn: Date = new Date()
+
+ // callback to be called when a command is finished processing
+ private processCallback?: ResultsCallback
+
+ /** Handles data received in response to an outbound command sent by processCommands */
+ private processCommandsData(socket: Socket, data: Buffer) {
+ try {
+ // append data to buffer as it arrives
+ if (data.length && data.length > 0) {
+ this.buffer = Buffer.concat([this.buffer, data])
+ }
+
+ let dataType = this.buffer?.subarray(0, 1).toString()
+ if (hasCommandLength(dataType)) {
+ const commandLength = parseCommandLength(this.buffer)
+ const hasReceivedEntireCommand = this.buffer.length - this.buffer.indexOf(' ') - 1 >= commandLength ? true : false
+
+ if (hasReceivedEntireCommand) {
+ if (this.config?.verbose) {
+ let bufferString = this.buffer.toString('utf8')
+ if (bufferString.length > 1000) {
+ bufferString = bufferString.substring(0, 100) + '...' + bufferString.substring(bufferString.length - 40)
+ }
+ const elapsedMs = new Date().getTime() - this.startedOn.getTime()
+ console.debug(`<- ${bufferString} (${elapsedMs}ms)`)
+ }
+
+ // need to decompress this buffer before decoding?
+ if (dataType === CMD_COMPRESSED) {
+ ;({ buffer: this.buffer, dataType } = decompressBuffer(this.buffer))
+ }
+
+ if (dataType !== CMD_ROWSET_CHUNK) {
+ const { data } = popData(this.buffer)
+ this.processCommandsFinish?.call(this, null, data)
+ } else {
+ // check if rowset received the ending chunk in which case it can be unpacked
+ if (bufferEndsWith(this.buffer, ROWSET_CHUNKS_END)) {
+ const parsedData = parseRowsetChunks([this.buffer])
+ this.processCommandsFinish?.call(this, null, parsedData)
+ }
+ }
+ }
+ } else {
+ // command with no explicit len so make sure that the final character is a space
+ const lastChar = this.buffer.subarray(this.buffer.length - 1, this.buffer.length).toString('utf8')
+ if (lastChar == ' ') {
+ const { data } = popData(this.buffer)
+ this.processCommandsFinish?.call(this, null, data)
+ }
+ }
+ } catch (error) {
+ console.assert(error instanceof Error)
+ if (error instanceof Error) {
+ this.processCommandsFinish?.call(this, error)
+ }
+ }
+ }
+
+ /** Completes a transaction initiated by processCommands */
+ private processCommandsFinish(error: Error | null, result?: any) {
+ if (error) {
+ console.error('BunTransport.finish - error', error)
+ } else {
+ // console.debug('BunTransport.finish - result', result)
+ }
+ if (this.processCallback) {
+ // console.error(`SQLiteCloudBunConnection.processCommandsFinish - error:${error}, result: ${result}`, error, result)
+ this.processCallback(error, result)
+ }
+ }
+
+ /** Disconnect immediately, release connection. */
+ close(): this {
+ if (this.socket) {
+ const socket = this.socket
+ this.socket = undefined
+ socket.end()
+ }
+ this.operations.clear()
+ return this
+ }
+}
diff --git a/src/gateway/gateway.ts b/src/gateway/gateway.ts
new file mode 100644
index 0000000..6f5e096
--- /dev/null
+++ b/src/gateway/gateway.ts
@@ -0,0 +1,286 @@
+//
+// gateway.ts - SQLite Cloud Gateway enabling websocket connections and SQL to json queries
+//
+
+import packageJson from '../../package.json'
+
+// bun specific driver + shared classes
+import { SQLiteCloudBunConnection } from './connection-bun'
+import { SQLiteCloudRowset, SQLiteCloudError, validateConfiguration } from '../index'
+import { type ApiRequest, type ApiResponse, type SqlApiRequest, DEFAULT_PORT_HTTP, DEFAULT_PORT_SOCKET } from './shared'
+
+// external modules
+import { heapStats } from 'bun:jsc'
+import { Server } from 'socket.io'
+import express from 'express'
+import http from 'http'
+
+// port where socket.io will listen for connections
+const SOCKET_PORT = parseInt(process.env['SOCKET_PORT'] || DEFAULT_PORT_SOCKET.toString())
+// port where http server will listen for connections
+const HTTP_PORT = parseInt(process.env['HTTP_PORT'] || DEFAULT_PORT_HTTP.toString())
+// should we log verbose messages?
+const VERBOSE = process.env['VERBOSE']?.toLowerCase() === 'true'
+console.debug(`@sqlitecloud/gateway v${packageJson.version}`)
+
+//
+// express
+//
+
+// Express app for HTTP server
+const app = express()
+app.use(express.json())
+app.use(express.static('public'))
+
+// server for socket.io and http endpoints
+const server = http.createServer(app)
+
+//
+// websocket server
+//
+
+// Replacing Deno's Server with socket.io's Server
+const io = new Server(server, {
+ cors: {
+ origin: '*', // specify the client origin
+ methods: ['GET', 'POST'], // allowed HTTP methods
+ credentials: true // allow credentials (cookies, session)
+ }
+})
+
+// Establish handlers for a socket.io connection
+io.on('connection', socket => {
+ //
+ // state
+ //
+
+ // the connection string is passed in the bearer token
+ // https://socket.io/docs/v4/client-options/#auth
+ const connectionString = socket.handshake.auth.token as string
+ let connection: SQLiteCloudBunConnection | null = null
+ log(`ws | connect socket.id: ${socket.id}`)
+
+ //
+ // handlers
+ //
+
+ // received a sql query request from the client socket
+ socket.on('v1/info', (_request: ApiRequest, callback: (response: ApiResponse) => void) => {
+ const serverInfo = getServerInfo()
+ log(`ws | info <- ${JSON.stringify(serverInfo)}`)
+ return callback(serverInfo)
+ })
+
+ // received a sql query request from the client socket
+ socket.on('v1/sql', async (request: SqlApiRequest, callback: (response: ApiResponse) => void) => {
+ if (!connectionString) {
+ callback({ error: { status: '401', title: 'Unauthorized', detail: 'Provide connection string in bearer token' } })
+ return
+ }
+
+ try {
+ if (!connection) {
+ const startTime = Date.now()
+ log('ws | connecting...')
+ connection = await connectAsync(connectionString)
+ log(`ws | connected in ${Date.now() - startTime}ms`)
+ }
+
+ log(`ws | sql -> ${JSON.stringify(request)}`)
+ const response = await queryAsync(connection, request)
+ log(`ws | sql <- ${JSON.stringify(response)}`)
+ return callback(response)
+ } catch (error) {
+ callback({ error: { status: '400', title: 'Bad Request', detail: error as string } })
+ }
+ })
+
+ // received a disconnect request from the client socket
+ socket.on('disconnect', () => {
+ log(`ws | disconnect socket.id: ${socket.id}`)
+ connection?.close()
+ connection = null
+ })
+})
+
+// Run websocket server
+server.listen(SOCKET_PORT, () => {
+ console.debug(`WebSocket server is running on port ${SOCKET_PORT}`)
+})
+
+//
+// HTTP server
+//
+
+app.listen(HTTP_PORT, () => {
+ console.debug(`HTTP server is running on port ${HTTP_PORT}`)
+})
+
+app.get('/v1/info', (req, res) => {
+ res.json(getServerInfo())
+})
+
+app.post('/v1/sql', (req: express.Request, res: express.Response) => {
+ void (async () => {
+ try {
+ log('POST /v1/sql')
+ const response = await handleHttpSqlRequest(req, res)
+ res.json(response)
+ } catch (error) {
+ log('POST /v1/sql - error', error)
+ res.status(400).json({ error: { status: '400', title: 'Bad Request', detail: error as string } })
+ }
+ })
+})
+
+//
+// utilities
+//
+
+/** Handle a stateless sql query request */
+async function handleHttpSqlRequest(request: express.Request, response: express.Response) {
+ // bearer token is required to connect to sqlitecloud
+ const connectionString = getBearerToken(request)
+ if (!connectionString) {
+ return errorResponse(response, 401, 'Unauthorized')
+ }
+
+ // ?sql= or json payload with sql property is required
+ let apiRequest: SqlApiRequest
+ try {
+ apiRequest = request.body
+ } catch (_error) {
+ apiRequest = {
+ database: request.query.database as string,
+ sql: request.query.sql as string,
+ row: request.query.row as 'array' | 'dictionary'
+ }
+ }
+ if (!(apiRequest.database || apiRequest.sql)) {
+ return errorResponse(response, 400, 'Bad Request', 'Missing ?sql= query or json payload')
+ }
+
+ let connection
+ try {
+ // request is stateless so we will connect and disconnect for each request
+ log(`http | sql -> ${JSON.stringify(apiRequest)}`)
+ connection = await connectAsync(connectionString)
+ const apiResponse = await queryAsync(connection, apiRequest)
+ log(`http | sql <- ${JSON.stringify(apiResponse)}`)
+ response.json(apiResponse)
+ } catch (error) {
+ errorResponse(response, 400, 'Bad Request', (error as Error).toString())
+ } finally {
+ connection?.close()
+ }
+}
+
+/** Server info for /v1/info endpoints */
+function getServerInfo() {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { objectTypeCounts, protectedObjectTypeCounts, ...memory } = heapStats()
+ return {
+ data: {
+ name: '@sqlitecloud/gateway',
+ version: packageJson.version,
+ bun: {
+ version: Bun.version,
+ path: Bun.which('bun'),
+ main: Bun.main,
+ uptime: Math.floor(Bun.nanoseconds() / 1e9) // seconds
+ },
+ memory,
+ cpuUsage: process.cpuUsage(),
+ date: new Date().toISOString()
+ }
+ }
+}
+
+/** Extract and return bearer token from request authorization headers */
+function getBearerToken(request: express.Request): string | null {
+ const authorization = request.headers['authorization'] as string
+ // console.debug(`getBearerToken - ${authorization}`, request.headers)
+ if (authorization && authorization.startsWith('Bearer ')) {
+ return authorization.substring(7)
+ }
+ return null
+}
+
+/** Returns a json api compatibile error response */
+function errorResponse(response: express.Response, status: number, statusText: string, detail?: string) {
+ response.status(status).json({ error: { status: status.toString(), title: statusText, detail } })
+}
+
+/** Connects to given database asynchronously */
+async function connectAsync(connectionString: string): Promise {
+ return await new Promise((resolve, reject) => {
+ const config = validateConfiguration({ connectionString })
+ const connection = new SQLiteCloudBunConnection(config, (error: Error | null) => {
+ if (error) {
+ log('connectAsync | error', error)
+ reject(error)
+ } else {
+ resolve(connection)
+ }
+ })
+ })
+}
+
+/** Sends given sql commands asynchronously */
+async function sendCommandsAsync(connection: SQLiteCloudBunConnection, sql: string): Promise {
+ return await new Promise((resolve, reject) => {
+ connection.sendCommands(sql, (error: Error | null, results) => {
+ // Explicitly type the 'error' parameter as 'Error'
+ if (error) {
+ log('sendCommandsAsync | error', error)
+ reject(error)
+ } else {
+ // console.debug(JSON.stringify(results).substring(0, 140) + '...')
+ resolve(results)
+ }
+ })
+ })
+}
+
+/** Runs query on given connection and returns response payload */
+async function queryAsync(connection: SQLiteCloudBunConnection, apiRequest: SqlApiRequest): Promise {
+ let result: unknown = 'OK'
+ try {
+ if (apiRequest.database) {
+ result = await sendCommandsAsync(connection, `USE DATABASE ${apiRequest.database}`)
+ }
+
+ if (apiRequest.sql) {
+ result = await sendCommandsAsync(connection, apiRequest.sql)
+ // query returned a rowset?
+ if (result instanceof SQLiteCloudRowset) {
+ const rowset = result
+ const data = apiRequest.row === 'dictionary' ? rowset : rowset.map(rowsetRow => rowsetRow.getData()) // rows as arrays by default
+ return { data, metadata: rowset.metadata }
+ }
+ }
+ } catch (error) {
+ log('queryAsync | error', error)
+ const sqliteError = error as SQLiteCloudError
+ return {
+ error: {
+ status: '400',
+ title: 'Bad Request',
+ detail: sqliteError?.message || sqliteError?.toString(),
+ // SQLiteCloudError additional properties
+ errorCode: sqliteError?.errorCode,
+ offsetCode: sqliteError?.offsetCode,
+ externalErrorCode: sqliteError?.externalErrorCode
+ }
+ }
+ }
+
+ return { data: result }
+}
+
+/** Log only in verbose mode */
+function log(...args: unknown[]) {
+ if (VERBOSE) {
+ console.debug(...args)
+ }
+}
diff --git a/src/gateway/shared.ts b/src/gateway/shared.ts
new file mode 100644
index 0000000..a8c6ca4
--- /dev/null
+++ b/src/gateway/shared.ts
@@ -0,0 +1,37 @@
+//
+// types.ts - shared types for client and server
+//
+
+/** Generic api request as a json dictionary */
+export type ApiRequest = Record
+
+export interface ApiResponse {
+ /** Rows are returned as dictionaries or arrays */
+ data?: unknown
+ /** Additional metadata */
+ metadata?: unknown
+ /** Optional error condition */
+ error?: {
+ /** Error status as http code */
+ status: string
+ title?: string
+ detail?: string
+ // SQLiteCloudError additional properties
+ errorCode?: string
+ externalErrorCode?: string
+ offsetCode?: number
+ }
+}
+
+/** An api call to perform a query */
+export interface SqlApiRequest extends ApiRequest {
+ /** If the optional database name is specified, the connection will perform a USE DATABASE before running the query */
+ database?: string
+ /** The sql query to be executed */
+ sql: string
+ /** Rows can be returned as arrays (default) or dictionaries */
+ row?: 'array' | 'dictionary'
+}
+
+export const DEFAULT_PORT_SOCKET = 4000
+export const DEFAULT_PORT_HTTP = 8090
diff --git a/src/index.ts b/src/index.ts
index 5ea012d..50215b2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,12 +1,15 @@
//
-// index.ts - re-export public APIs
+// index.ts - export drivers classes, utilities, types
//
-export { Database } from './database'
-export { Statement } from './statement'
-export { SQLiteCloudConfig, SQLCloudRowsetMetadata, SQLiteCloudError, ErrorCallback } from './types'
-export { SQLiteCloudRowset, SQLiteCloudRow } from './rowset'
-export { SQLiteCloudConnection } from './connection'
-export { escapeSqlParameter, prepareSql, parseConnectionString, validateConfiguration } from './utilities'
-export { WebSocketTransport } from './transport-ws'
-export { TlsSocketTransport } from './transport-tls'
+// include ONLY packages used by drivers
+// do NOT include anything related to gateway or bun or express
+// connection-tls does not want/need to load on browser and is loaded dynamically by Database
+// connection-ws does not want/need to load on node and is loaded dynamically by Database
+
+export { Database } from './drivers/database'
+export { Statement } from './drivers/statement'
+export { SQLiteCloudConnection } from './drivers/connection'
+export { type SQLiteCloudConfig, type SQLCloudRowsetMetadata, SQLiteCloudError, type ErrorCallback } from './drivers/types'
+export { SQLiteCloudRowset, SQLiteCloudRow } from './drivers/rowset'
+export { escapeSqlParameter, prepareSql, parseConnectionString, validateConfiguration } from './drivers/utilities'
diff --git a/src/transport-tls.ts b/src/transport-tls.ts
deleted file mode 100644
index e1a15c5..0000000
--- a/src/transport-tls.ts
+++ /dev/null
@@ -1,532 +0,0 @@
-/**
- * transport-tls.ts - handles low level communication with sqlitecloud server via tls socket and binary protocol
- */
-
-import { SQLiteCloudConfig, SQLiteCloudError, ErrorCallback, ResultsCallback, SQLCloudRowsetMetadata, SQLiteCloudDataTypes } from './types'
-import { SQLiteCloudRowset } from './rowset'
-import { ConnectionTransport, getInitializationCommands, anonimizeError, anonimizeCommand } from './connection'
-
-import net from 'net'
-import tls from 'tls'
-const lz4 = require('lz4js')
-
-// The server communicates with clients via commands defined in
-// SQLiteCloud Server Protocol (SCSP), see more at:
-// https://github.com/sqlitecloud/sdk/blob/master/PROTOCOL.md
-
-const CMD_STRING = '+'
-const CMD_ZEROSTRING = '!'
-const CMD_ERROR = '-'
-const CMD_INT = ':'
-const CMD_FLOAT = ','
-const CMD_ROWSET = '*'
-const CMD_ROWSET_CHUNK = '/'
-const CMD_JSON = '#'
-const CMD_NULL = '_'
-const CMD_BLOB = '$'
-const CMD_COMPRESSED = '%'
-const CMD_COMMAND = '^'
-const CMD_ARRAY = '='
-// const CMD_RAWJSON = '{'
-// const CMD_PUBSUB = '|'
-// const CMD_RECONNECT = '@'
-
-/**
- * Implementation of SQLiteCloudConnection that connects directly to the database via tls socket and raw, binary protocol.
- * Connects with plain socket with no encryption is the ?insecure=1 parameter is specified.
- * SQLiteCloud low-level connection, will do messaging, handle socket, authentication, etc.
- * A connection socket is established when the connection is created and closed when the connection is closed.
- * All operations are serialized by waiting for any pending operations to complete. Once a connection is closed,
- * it cannot be reopened and you must create a new connection.
- */
-export class TlsSocketTransport implements ConnectionTransport {
- /** Configuration passed to connect */
- private config?: SQLiteCloudConfig
- /** Currently opened tls socket used to communicated with SQLiteCloud server */
- private socket?: tls.TLSSocket | net.Socket | null
-
- /** True if connection is open */
- get connected(): boolean {
- return !!this.socket
- }
-
- /* Opens a connection with the server and sends the initialization commands. Will throw in case of errors. */
- connect(config: SQLiteCloudConfig, callback?: ErrorCallback): this {
- // connection established while we were waiting in line?
- console.assert(!this.connected, 'Connection already established')
-
- // clear all listeners and call done in the operations queue
- const finish: ResultsCallback = error => {
- if (this.socket) {
- this.socket.removeAllListeners('data')
- this.socket.removeAllListeners('error')
- this.socket.removeAllListeners('close')
- if (error) {
- this.close()
- }
- }
- }
-
- this.config = config
-
- if (config.insecure) {
- // connect to plain socket, without encryption, only if insecure parameter specified
- // this option is mainly for testing purposes and is not available on production nodes
- // which would need to connect using tls and proper certificates as per code below
- const connectionOptions: net.SocketConnectOpts = {
- host: config.host,
- port: config.port as number
- }
- this.socket = net.connect(connectionOptions, () => {
- console.warn(`TlsTransport.connect - connected to ${config.host}:${config.port} using insecure protocol`)
- callback?.call(this, null)
- })
- } else {
- // connect to tls socket, initialize connection, setup event handlers
- this.socket = tls.connect(this.config.port as number, this.config.host, this.config.tlsOptions, () => {
- const tlsSocket = this.socket as tls.TLSSocket
- if (!tlsSocket?.authorized) {
- const anonimizedError = anonimizeError(tlsSocket.authorizationError)
- console.error('Connection was not authorized', anonimizedError)
- this.close()
- finish(new SQLiteCloudError('Connection was not authorized', { cause: anonimizedError }))
- } else {
- // the connection was closed before it was even opened,
- // eg. client closed the connection before the server accepted it
- if (this.socket === null) {
- finish(new SQLiteCloudError('Connection was closed before it was done opening'))
- return
- }
-
- // send initialization commands
- console.assert(this.socket, 'Connection already closed')
- const commands = getInitializationCommands(config)
- this.processCommands(commands, error => {
- if (error && this.socket) {
- this.close()
- }
- if (callback) {
- callback?.call(this, error)
- callback = undefined
- }
- finish(error)
- })
- }
- })
- }
-
- this.socket.on('close', () => {
- this.socket = null
- finish(new SQLiteCloudError('Connection was closed'))
- })
-
- this.socket.once('error', (error: any) => {
- console.error('Connection error', error)
- finish(new SQLiteCloudError('Connection error', { cause: error }))
- })
-
- return this
- }
-
- /** Will send a command immediately (no queueing), return the rowset/result or throw an error */
- processCommands(commands: string, callback?: ResultsCallback): this {
- // connection needs to be established?
- if (!this.socket) {
- callback?.call(this, new SQLiteCloudError('Connection not established', { errorCode: 'ERR_CONNECTION_NOT_ESTABLISHED' }))
- return this
- }
-
- // compose commands following SCPC protocol
- commands = formatCommand(commands)
-
- let buffer = Buffer.alloc(0)
- const rowsetChunks: Buffer[] = []
- // const startedOn = new Date()
-
- // define what to do if an answer does not arrive within the set timeout
- let socketTimeout: number
-
- // clear all listeners and call done in the operations queue
- const finish: ResultsCallback = (error, result) => {
- clearTimeout(socketTimeout)
- if (this.socket) {
- this.socket.removeAllListeners('data')
- this.socket.removeAllListeners('error')
- this.socket.removeAllListeners('close')
- }
- if (callback) {
- callback?.call(this, error, result)
- callback = undefined
- }
- }
-
- // define the Promise that waits for the server response
- const readData = (data: Uint8Array) => {
- try {
- // on first ondata event, dataType is read from data, on subsequent ondata event, is read from buffer that is the concatanations of data received on each ondata event
- let dataType = buffer.length === 0 ? data.subarray(0, 1).toString() : buffer.subarray(0, 1).toString('utf8')
- buffer = Buffer.concat([buffer, data])
- const commandLength = hasCommandLength(dataType)
-
- if (commandLength) {
- const commandLength = parseCommandLength(buffer)
- const hasReceivedEntireCommand = buffer.length - buffer.indexOf(' ') - 1 >= commandLength ? true : false
- if (hasReceivedEntireCommand) {
- if (this.config?.verbose) {
- let bufferString = buffer.toString('utf8')
- if (bufferString.length > 1000) {
- bufferString = bufferString.substring(0, 100) + '...' + bufferString.substring(bufferString.length - 40)
- }
- // const elapsedMs = new Date().getTime() - startedOn.getTime()
- // console.debug(`Receive: ${bufferString} - ${elapsedMs}ms`)
- }
-
- // need to decompress this buffer before decoding?
- if (dataType === CMD_COMPRESSED) {
- ;({ buffer, dataType } = decompressBuffer(buffer))
- }
-
- if (dataType !== CMD_ROWSET_CHUNK) {
- this.socket?.off('data', readData)
- const { data } = popData(buffer)
- finish(null, data)
- } else {
- // @ts-expect-error
- // check if rowset received the ending chunk
- if (data.subarray(data.indexOf(' ') + 1, data.length).toString() === '0 0 0 ') {
- const parsedData = parseRowsetChunks(rowsetChunks)
- finish?.call(this, null, parsedData)
- } else {
- // no ending string? ask server for another chunk
- rowsetChunks.push(buffer)
- buffer = Buffer.alloc(0)
-
- // no longer need to ack the server
- // const okCommand = formatCommand('OK')
- // this.socket?.write(okCommand)
- }
- }
- }
- } else {
- // command with no explicit len so make sure that the final character is a space
- const lastChar = buffer.subarray(buffer.length - 1, buffer.length).toString('utf8')
- if (lastChar == ' ') {
- const { data } = popData(buffer)
- finish(null, data)
- }
- }
- } catch (error) {
- console.assert(error instanceof Error)
- if (error instanceof Error) {
- finish(error)
- }
- }
- }
-
- this.socket?.once('close', () => {
- finish(new SQLiteCloudError('Connection was closed', { cause: anonimizeCommand(commands) }))
- })
-
- this.socket?.write(commands, 'utf8', () => {
- socketTimeout = setTimeout(() => {
- const timeoutError = new SQLiteCloudError('Request timed out', { cause: anonimizeCommand(commands) })
- // console.debug(`Request timed out, config.timeout is ${this.config?.timeout as number}ms`, timeoutError)
- finish(timeoutError)
- }, this.config?.timeout)
- this.socket?.on('data', readData)
- })
-
- this.socket?.once('error', (error: any) => {
- console.error('Socket error', error)
- this.close()
- finish(new SQLiteCloudError('Socket error', { cause: anonimizeError(error) }))
- })
-
- return this
- }
-
- /** Disconnect from server, release connection. */
- close(): this {
- console.assert(this.socket !== null, 'TlsSocketTransport.close - connection already closed')
- if (this.socket) {
- this.socket.destroy()
- this.socket = null
- }
- this.socket = undefined
- return this
- }
-}
-
-//
-// utility functions
-//
-
-/** Analyze first character to check if corresponding data type has LEN */
-function hasCommandLength(firstCharacter: string): boolean {
- return firstCharacter == CMD_INT || firstCharacter == CMD_FLOAT || firstCharacter == CMD_NULL ? false : true
-}
-
-/** Analyze a command with explict LEN and extract it */
-function parseCommandLength(data: Buffer) {
- return parseInt(data.subarray(1, data.indexOf(' ')).toString('utf8'))
-}
-
-/** Receive a compressed buffer, decompress with lz4, return buffer and datatype */
-function decompressBuffer(buffer: Buffer): { buffer: Buffer; dataType: string } {
- const spaceIndex = buffer.indexOf(' ')
- buffer = buffer.subarray(spaceIndex + 1)
-
- // extract compressed size
- const compressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'))
- buffer = buffer.subarray(buffer.indexOf(' ') + 1)
-
- // extract decompressed size
- const decompressedSize = parseInt(buffer.subarray(0, buffer.indexOf(' ') + 1).toString('utf8'))
- buffer = buffer.subarray(buffer.indexOf(' ') + 1)
-
- // extract compressed dataType
- const dataType = buffer.subarray(0, 1).toString('utf8')
- const decompressedBuffer = Buffer.alloc(decompressedSize)
- const compressedBuffer = buffer.subarray(buffer.length - compressedSize)
-
- // lz4js library is javascript and doesn't have types so we silence the type check
- // eslint-disable-next-line
- const decompressionResult: number = lz4.decompressBlock(compressedBuffer, decompressedBuffer, 0, compressedSize, 0)
- buffer = Buffer.concat([buffer.subarray(0, buffer.length - compressedSize), decompressedBuffer])
- if (decompressionResult <= 0 || decompressionResult !== decompressedSize) {
- throw new Error(`lz4 decompression error at offset ${decompressionResult}`)
- }
-
- return { buffer, dataType: dataType }
-}
-
-/** Parse error message or extended error message */
-function parseError(buffer: Buffer, spaceIndex: number): never {
- const errorBuffer = buffer.subarray(spaceIndex + 1)
- const errorString = errorBuffer.toString('utf8')
- const parts = errorString.split(' ')
-
- let errorCodeStr = parts.shift() || '0' // Default errorCode is '0' if not present
- let extErrCodeStr = '0' // Default extended error code
- let offsetCodeStr = '-1' // Default offset code
-
- // Split the errorCode by ':' to check for extended error codes
- const errorCodeParts = errorCodeStr.split(':')
- errorCodeStr = errorCodeParts[0]
- if (errorCodeParts.length > 1) {
- extErrCodeStr = errorCodeParts[1]
- if (errorCodeParts.length > 2) {
- offsetCodeStr = errorCodeParts[2]
- }
- }
-
- // Rest of the error string is the error message
- const errorMessage = parts.join(' ')
-
- // Parse error codes to integers safely, defaulting to 0 if NaN
- const errorCode = parseInt(errorCodeStr)
- const extErrCode = parseInt(extErrCodeStr)
- const offsetCode = parseInt(offsetCodeStr)
-
- // create an Error object and add the custom properties
- throw new SQLiteCloudError(errorMessage, {
- errorCode: errorCode.toString(),
- externalErrorCode: extErrCode.toString(),
- offsetCode
- })
-}
-
-/** Parse an array of items (each of which will be parsed by type separately) */
-function parseArray(buffer: Buffer, spaceIndex: number): SQLiteCloudDataTypes[] {
- const parsedData = []
-
- const array = buffer.subarray(spaceIndex + 1, buffer.length)
- const numberOfItems = parseInt(array.subarray(0, spaceIndex - 2).toString('utf8'))
- let arrayItems = array.subarray(array.indexOf(' ') + 1, array.length)
-
- for (let i = 0; i < numberOfItems; i++) {
- const { data, fwdBuffer: buffer } = popData(arrayItems)
- parsedData.push(data)
- arrayItems = buffer
- }
-
- return parsedData as SQLiteCloudDataTypes[]
-}
-
-/** Parse header in a rowset or chunk of a chunked rowset */
-function parseRowsetHeader(buffer: Buffer): { index: number; metadata: SQLCloudRowsetMetadata; fwdBuffer: Buffer } {
- const index = parseInt(buffer.subarray(0, buffer.indexOf(':') + 1).toString())
- buffer = buffer.subarray(buffer.indexOf(':') + 1)
-
- // extract rowset header
- const { data, fwdBuffer } = popIntegers(buffer, 3)
-
- return {
- index,
- metadata: {
- version: data[0],
- numberOfRows: data[1],
- numberOfColumns: data[2],
- columns: []
- },
- fwdBuffer
- }
-}
-
-/** Extract column names and, optionally, more metadata out of a rowset's header */
-function parseRowsetColumnsMetadata(buffer: Buffer, metadata: SQLCloudRowsetMetadata): Buffer {
- function popForward() {
- const { data, fwdBuffer: fwdBuffer } = popData(buffer) // buffer in parent scope
- buffer = fwdBuffer
- return data
- }
-
- for (let i = 0; i < metadata.numberOfColumns; i++) {
- metadata.columns.push({ name: popForward() as string })
- }
-
- // extract additional metadata if rowset has version 2
- if (metadata.version == 2) {
- for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].type = popForward() as string
- for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].database = popForward() as string
- for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].table = popForward() as string
- for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].column = popForward() as string // original column name
-
- for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].notNull = popForward() as number
- for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].primaryKey = popForward() as number
- for (let i = 0; i < metadata.numberOfColumns; i++) metadata.columns[i].autoIncrement = popForward() as number
- }
-
- return buffer
-}
-
-/** Parse a regular rowset (no chunks) */
-function parseRowset(buffer: Buffer, spaceIndex: number): SQLiteCloudRowset {
- buffer = buffer.subarray(spaceIndex + 1, buffer.length)
-
- const { metadata, fwdBuffer } = parseRowsetHeader(buffer)
- buffer = parseRowsetColumnsMetadata(fwdBuffer, metadata)
-
- // decode each rowset item
- const data = []
- for (let j = 0; j < metadata.numberOfRows * metadata.numberOfColumns; j++) {
- const { data: rowData, fwdBuffer } = popData(buffer)
- data.push(rowData)
- buffer = fwdBuffer
- }
-
- console.assert(data && data.length === metadata.numberOfRows * metadata.numberOfColumns, 'SQLiteCloudConnection.parseRowset - invalid rowset data')
- return new SQLiteCloudRowset(metadata, data)
-}
-
-/**
- * Parse a chunk of a chunked rowset command, eg:
- * *LEN 0:VERS NROWS NCOLS DATA
- */
-export function parseRowsetChunks(buffers: Buffer[]) {
- let metadata: SQLCloudRowsetMetadata = { version: 1, numberOfColumns: 0, numberOfRows: 0, columns: [] }
- const data: any[] = []
-
- for (let i = 0; i < buffers.length; i++) {
- let buffer: Buffer = buffers[i]
-
- // validate and skip data type
- const dataType = buffer.subarray(0, 1).toString()
- console.assert(dataType === CMD_ROWSET_CHUNK)
- buffer = buffer.subarray(buffer.indexOf(' ') + 1)
-
- // chunk header, eg: 0:VERS NROWS NCOLS
- const { index: chunkIndex, metadata: chunkMetadata, fwdBuffer } = parseRowsetHeader(buffer)
- buffer = fwdBuffer
-
- // first chunk? extract columns metadata
- if (chunkIndex === 1) {
- metadata = chunkMetadata
- buffer = parseRowsetColumnsMetadata(buffer, metadata)
- } else {
- metadata.numberOfRows += chunkMetadata.numberOfRows
- }
-
- // extract single rowset row
- for (let k = 0; k < chunkMetadata.numberOfRows * metadata.numberOfColumns; k++) {
- const { data: itemData, fwdBuffer } = popData(buffer)
- data.push(itemData)
- buffer = fwdBuffer
- }
- }
-
- console.assert(data && data.length === metadata.numberOfRows * metadata.numberOfColumns, 'SQLiteCloudConnection.parseRowsetChunks - invalid rowset data')
- return new SQLiteCloudRowset(metadata, data)
-}
-
-/** Pop one or more space separated integers from beginning of buffer, move buffer forward */
-function popIntegers(buffer: Buffer, numberOfIntegers = 1): { data: number[]; fwdBuffer: Buffer } {
- const data: number[] = []
- for (let i = 0; i < numberOfIntegers; i++) {
- const spaceIndex = buffer.indexOf(' ')
- data[i] = parseInt(buffer.subarray(0, spaceIndex).toString())
- buffer = buffer.subarray(spaceIndex + 1)
- }
- return { data, fwdBuffer: buffer }
-}
-
-/** Parse command, extract its data, return the data and the buffer moved to the first byte after the command */
-export function popData(buffer: Buffer): { data: SQLiteCloudDataTypes | SQLiteCloudRowset; fwdBuffer: Buffer } {
- function popResults(data: any) {
- const fwdBuffer = buffer.subarray(commandEnd)
- return { data, fwdBuffer }
- }
-
- // first character is the data type
- console.assert(buffer && buffer instanceof Buffer)
- const dataType: string = buffer.subarray(0, 1).toString('utf8')
- console.assert(dataType !== CMD_COMPRESSED, "Compressed data shouldn't be decompressed before parsing")
- console.assert(dataType !== CMD_ROWSET_CHUNK, 'Chunked data should be parsed by parseRowsetChunks')
-
- let spaceIndex = buffer.indexOf(' ')
- if (spaceIndex === -1) {
- spaceIndex = buffer.length - 1
- }
-
- let commandEnd = -1
- if (dataType === CMD_INT || dataType === CMD_FLOAT || dataType === CMD_NULL) {
- commandEnd = spaceIndex + 1
- } else {
- const commandLength = parseInt(buffer.subarray(1, spaceIndex).toString())
- commandEnd = spaceIndex + 1 + commandLength
- }
-
- switch (dataType) {
- case CMD_INT:
- return popResults(parseInt(buffer.subarray(1, spaceIndex).toString()))
- case CMD_FLOAT:
- return popResults(parseFloat(buffer.subarray(1, spaceIndex).toString()))
- case CMD_NULL:
- return popResults(null)
- case CMD_STRING:
- return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'))
- case CMD_ZEROSTRING:
- return popResults(buffer.subarray(spaceIndex + 1, commandEnd - 1).toString('utf8'))
- case CMD_COMMAND:
- return popResults(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8'))
- case CMD_JSON:
- return popResults(JSON.parse(buffer.subarray(spaceIndex + 1, commandEnd).toString('utf8')))
- case CMD_BLOB:
- return popResults(buffer.subarray(spaceIndex + 1, commandEnd))
- case CMD_ARRAY:
- return popResults(parseArray(buffer, spaceIndex))
- case CMD_ROWSET:
- return popResults(parseRowset(buffer, spaceIndex))
- case CMD_ERROR:
- parseError(buffer, spaceIndex) // throws custom error
- break
- }
-
- throw new TypeError(`Data type: ${dataType} is not defined in SCSP`)
-}
-
-/** Format a command to be sent via SCSP protocol */
-export function formatCommand(command: string): string {
- const commandLength = Buffer.byteLength(command, 'utf-8')
- return `+${commandLength} ${command}`
-}
diff --git a/test/assets/testing.sql b/test/assets/testing.sql
index aa8b9d2..94419c8 100644
--- a/test/assets/testing.sql
+++ b/test/assets/testing.sql
@@ -31,3 +31,6 @@ INSERT INTO people (name, age, hobby) VALUES
('Harper Jackson', 36, 'Bellybutton lint sculpting'),
('Ditto il Foho', 27, 'Cloud shaping'),
('Benjamin Clark', 39, 'Telepathic cooking');
+
+-- Return the number 42 so we can test the query
+SELECT 42;
diff --git a/test/compare.test.ts b/test/compare.test.ts
index acdfa4d..c34105e 100644
--- a/test/compare.test.ts
+++ b/test/compare.test.ts
@@ -111,8 +111,6 @@ describe('Database.on', () => {
describe('Database.run', () => {
it('sqlite3: insert with plain sql', done => {
- const testingFile = createTestDatabaseFile()
-
// https://github.com/TryGhost/node-sqlite3/wiki/API#runsql--param---callback
function onInsert(error: Error, results: any) {
expect(error).toBeNull()
@@ -127,12 +125,11 @@ describe('Database.run', () => {
done()
}
+ const testingFile = createTestDatabaseFile()
testingFile.run(INSERT_SQL, onInsert)
})
it('sqlitecloud: insert with plain sql', done => {
- const testingCloud = getTestingDatabase()
-
// https://github.com/TryGhost/node-sqlite3/wiki/API#runsql--param---callback
function onInsert(error: Error, results: any) {
expect(error).toBeNull()
@@ -150,8 +147,10 @@ describe('Database.run', () => {
testingCloud.close()
done()
}
-
- testingCloud.run(INSERT_SQL, onInsert)
+ const testingCloud = getTestingDatabase(error => {
+ expect(error).toBeNull()
+ testingCloud.run(INSERT_SQL, onInsert)
+ })
})
// end run
diff --git a/test/connection-tls.test.ts b/test/connection-tls.test.ts
index 8e2107b..ff42c8b 100644
--- a/test/connection-tls.test.ts
+++ b/test/connection-tls.test.ts
@@ -3,7 +3,9 @@
*/
import { SQLiteCloudError } from '../src/index'
-import { SQLiteCloudConnection, anonimizeCommand } from '../src/connection'
+import { SQLiteCloudConnection } from '../src/drivers/connection'
+import { SQLiteCloudTlsConnection } from '../src/drivers/connection-tls'
+import { anonimizeCommand } from '../src/drivers/utilities'
import {
CHINOOK_DATABASE_URL,
INSECURE_DATABASE_URL,
@@ -54,7 +56,7 @@ describe('connection-tls', () => {
it('should connect with config object string', done => {
const configObj = getChinookConfig()
- const conn = new SQLiteCloudConnection(configObj, error => {
+ const conn = new SQLiteCloudTlsConnection(configObj, error => {
expect(error).toBeNull()
expect(conn.connected).toBe(true)
@@ -73,7 +75,7 @@ describe('connection-tls', () => {
done()
}
- const conn = new SQLiteCloudConnection(CHINOOK_DATABASE_URL, error => {
+ const conn = new SQLiteCloudTlsConnection(CHINOOK_DATABASE_URL, error => {
expect(error).toBeNull()
expect(conn.connected).toBe(true)
@@ -87,9 +89,11 @@ describe('connection-tls', () => {
})
it('should connect with insecure connection string', done => {
- if (INSECURE_DATABASE_URL) {
+ if (!INSECURE_DATABASE_URL) {
+ done()
+ } else {
expect(INSECURE_DATABASE_URL).toBeDefined()
- const conn = new SQLiteCloudConnection(INSECURE_DATABASE_URL, error => {
+ const conn = new SQLiteCloudTlsConnection(INSECURE_DATABASE_URL, error => {
expect(error).toBeNull()
expect(conn.connected).toBe(true)
@@ -100,9 +104,6 @@ describe('connection-tls', () => {
})
})
expect(conn).toBeDefined()
- } else {
- console.warn(`INSECURE_DATABASE_URL is not defined, ?insecure= connection will not be tested`)
- done()
}
})
@@ -113,7 +114,7 @@ describe('connection-tls', () => {
delete testingConfig.password
try {
- const conn = new SQLiteCloudConnection(testingConfig)
+ const conn = new SQLiteCloudTlsConnection(testingConfig)
} catch (error) {
expect(error).toBeDefined()
expect(error).toBeInstanceOf(SQLiteCloudError)
@@ -296,29 +297,7 @@ describe('connection-tls', () => {
expect(results[1]['key']).toBe('INDEXED')
expect(results[2]['key']).toBe('INDEX')
expect(results[3]['key']).toBe('DESC')
-
- database.close()
- done()
- })
- },
- LONG_TIMEOUT
- )
-
- it(
- 'should test chunked rowset via ',
- done => {
- // this operation sends 150 packets, so we need to increase the timeout
- const database = getChinookTlsConnection(undefined, { timeout: 60 * 1000 })
- database.sendCommands('TEST ROWSET_CHUNK', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfRows).toBe(147)
- expect(results.numberOfColumns).toBe(1)
- expect(results.columnsNames).toEqual(['key'])
-
- expect(results[0]['key']).toBe('REINDEX')
- expect(results[1]['key']).toBe('INDEXED')
- expect(results[2]['key']).toBe('INDEX')
- expect(results[3]['key']).toBe('DESC')
+ expect(results[146]['key']).toBe('PRIMARY')
database.close()
done()
diff --git a/test/connection-ws.test.ts b/test/connection-ws.test.ts
index ad1b6e2..d398b37 100644
--- a/test/connection-ws.test.ts
+++ b/test/connection-ws.test.ts
@@ -3,8 +3,8 @@
*/
import { SQLiteCloudError } from '../src/index'
-import { SQLiteCloudConnection, anonimizeCommand } from '../src/connection'
-import { parseConnectionString } from '../src/utilities'
+import { SQLiteCloudConnection } from '../src/drivers/connection'
+import { SQLiteCloudWebsocketConnection } from '../src/drivers/connection-ws'
import {
//
CHINOOK_DATABASE_URL,
@@ -14,6 +14,7 @@ import {
WARN_SPEED_MS,
EXPECT_SPEED_MS
} from './shared'
+import { error } from 'console'
describe('connection-ws', () => {
let chinook: SQLiteCloudConnection
@@ -36,11 +37,9 @@ describe('connection-ws', () => {
it('should connect with config object string', done => {
const configObj = getChinookConfig()
configObj.useWebsocket = true
- const connection = new SQLiteCloudConnection(configObj)
- expect(connection).toBeDefined()
- connection.sendCommands('TEST STRING', (error, results) => {
- connection.close()
- expect(connection.connected).toBe(false)
+ let connection: SQLiteCloudWebsocketConnection | null = null
+ connection = new SQLiteCloudWebsocketConnection(configObj, error => {
+ expect(error).toBeNull()
done()
})
})
@@ -51,237 +50,231 @@ describe('connection-ws', () => {
configObj.password = 'wrongpassword'
configObj.useWebsocket = true
- // should attemp connection and return error
- const connection = new SQLiteCloudConnection(configObj)
- expect(connection).toBeDefined()
- connection.sendCommands('TEST STRING', (error, results) => {
+ // should attempt connection and return error
+ const connection = new SQLiteCloudWebsocketConnection(configObj, error => {
expect(error).toBeDefined()
- expect(error).toBeInstanceOf(SQLiteCloudError)
- expect((error as any).message).toBe('SQLiteCloudError: Authentication failed.')
-
- connection.close()
- expect(connection.connected).toBe(false)
done()
})
})
-
+ /* TODO RESTORE TEST
it('should connect with connection string', done => {
if (CHINOOK_DATABASE_URL.indexOf('localhost') > 0) {
// skip this test when running locally since it requires a self-signed certificate
done()
}
- const conn = new SQLiteCloudConnection(CHINOOK_DATABASE_URL, error => {
+ let conn: SQLiteCloudWebsocketConnection | null = null
+ conn = new SQLiteCloudWebsocketConnection(CHINOOK_DATABASE_URL, error => {
expect(error).toBeNull()
- expect(conn.connected).toBe(true)
-
- chinook.sendCommands('TEST STRING', (error, results) => {
- conn.close()
- expect(conn.connected).toBe(false)
+ // @ts-ignore
+ this.sendCommands('TEST STRING', (error, results) => {
+ expect(error).toBeNull()
+ conn?.close()
+ expect(conn?.connected).toBe(false)
done()
})
})
expect(conn).toBeDefined()
})
})
-
- describe('send test commands', () => {
- it('should test integer', done => {
- chinook.sendCommands('TEST INTEGER', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBe(123456)
- done()
+*/
+ describe('send test commands', () => {
+ it('should test integer', done => {
+ chinook.sendCommands('TEST INTEGER', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBe(123456)
+ done()
+ })
})
- })
- it('should test null', done => {
- chinook.sendCommands('TEST NULL', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBeNull()
- done()
+ it('should test null', done => {
+ chinook.sendCommands('TEST NULL', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBeNull()
+ done()
+ })
})
- })
- it('should test float', done => {
- chinook.sendCommands('TEST FLOAT', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBe(3.1415926)
- done()
+ it('should test float', done => {
+ chinook.sendCommands('TEST FLOAT', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBe(3.1415926)
+ done()
+ })
})
- })
- it('should test string', done => {
- chinook.sendCommands('TEST STRING', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBe('Hello World, this is a test string.')
- done()
+ it('should test string', done => {
+ chinook.sendCommands('TEST STRING', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBe('Hello World, this is a test string.')
+ done()
+ })
})
- })
- it('should test zero string', done => {
- chinook.sendCommands('TEST ZERO_STRING', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBe('Hello World, this is a zero-terminated test string.')
- done()
+ it('should test zero string', done => {
+ chinook.sendCommands('TEST ZERO_STRING', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBe('Hello World, this is a zero-terminated test string.')
+ done()
+ })
})
- })
- it('should test string0', done => {
- chinook.sendCommands('TEST STRING0', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBe('')
- done()
+ it('should test string0', done => {
+ chinook.sendCommands('TEST STRING0', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBe('')
+ done()
+ })
})
- })
- it('should test command', done => {
- chinook.sendCommands('TEST COMMAND', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBe('PING')
- done()
+ it('should test command', done => {
+ chinook.sendCommands('TEST COMMAND', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBe('PING')
+ done()
+ })
})
- })
- it('should test json', done => {
- chinook.sendCommands('TEST JSON', (error, results) => {
- expect(error).toBeNull()
- expect(results).toEqual({
- 'msg-from': { class: 'soldier', name: 'Wixilav' },
- 'msg-to': { class: 'supreme-commander', name: '[Redacted]' },
- 'msg-type': ['0xdeadbeef', 'irc log'],
- 'msg-log': [
- 'soldier: Boss there is a slight problem with the piece offering to humans',
- 'supreme-commander: Explain yourself soldier!',
- "soldier: Well they don't seem to move anymore...",
- 'supreme-commander: Oh snap, I came here to see them twerk!'
- ]
+ it('should test json', done => {
+ chinook.sendCommands('TEST JSON', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toEqual({
+ 'msg-from': { class: 'soldier', name: 'Wixilav' },
+ 'msg-to': { class: 'supreme-commander', name: '[Redacted]' },
+ 'msg-type': ['0xdeadbeef', 'irc log'],
+ 'msg-log': [
+ 'soldier: Boss there is a slight problem with the piece offering to humans',
+ 'supreme-commander: Explain yourself soldier!',
+ "soldier: Well they don't seem to move anymore...",
+ 'supreme-commander: Oh snap, I came here to see them twerk!'
+ ]
+ })
+ done()
})
- done()
})
- })
- it('should test blob', done => {
- chinook.sendCommands('TEST BLOB', (error, results) => {
- expect(error).toBeNull()
- expect(typeof results).toBe('object')
- expect(results).toBeInstanceOf(Buffer)
- const bufferrowset = results as any as Buffer
- expect(bufferrowset.length).toBe(1000)
- done()
+ it('should test blob', done => {
+ chinook.sendCommands('TEST BLOB', (error, results) => {
+ expect(error).toBeNull()
+ expect(typeof results).toBe('object')
+ expect(results).toBeInstanceOf(Buffer)
+ const bufferrowset = results as any as Buffer
+ expect(bufferrowset.length).toBe(1000)
+ done()
+ })
})
- })
- it('should test blob0', done => {
- chinook.sendCommands('TEST BLOB0', (error, results) => {
- expect(error).toBeNull()
- expect(typeof results).toBe('object')
- expect(results).toBeInstanceOf(Buffer)
- const bufferrowset = results as any as Buffer
- expect(bufferrowset.length).toBe(0)
- done()
+ it('should test blob0', done => {
+ chinook.sendCommands('TEST BLOB0', (error, results) => {
+ expect(error).toBeNull()
+ expect(typeof results).toBe('object')
+ expect(results).toBeInstanceOf(Buffer)
+ const bufferrowset = results as any as Buffer
+ expect(bufferrowset.length).toBe(0)
+ done()
+ })
})
- })
- it('should test error', done => {
- chinook.sendCommands('TEST ERROR', (error, results) => {
- expect(error).toBeDefined()
- expect(error).toBeInstanceOf(SQLiteCloudError)
+ it('should test error', done => {
+ chinook.sendCommands('TEST ERROR', (error, results) => {
+ expect(error).toBeDefined()
+ expect(error).toBeInstanceOf(SQLiteCloudError)
- const sqliteCloudError = error as SQLiteCloudError
- expect(sqliteCloudError.message).toBe('This is a test error message with a devil error code.')
- expect(sqliteCloudError.errorCode).toBe('66666')
- expect(sqliteCloudError.externalErrorCode).toBe('0')
- expect(sqliteCloudError.offsetCode).toBe(-1)
+ const sqliteCloudError = error as SQLiteCloudError
+ expect(sqliteCloudError.message).toBe('This is a test error message with a devil error code.')
+ expect(sqliteCloudError.errorCode).toBe('66666')
+ expect(sqliteCloudError.externalErrorCode).toBe('0')
+ expect(sqliteCloudError.offsetCode).toBe(-1)
- done()
+ done()
+ })
})
- })
-
- it('should test exterror', done => {
- chinook.sendCommands('TEST EXTERROR', (error, results) => {
- expect(error).toBeDefined()
- expect(error).toBeInstanceOf(SQLiteCloudError)
- const sqliteCloudError = error as SQLiteCloudError
- expect(sqliteCloudError.message).toBe('This is a test error message with an extcode and a devil error code.')
- expect(sqliteCloudError.errorCode).toBe('66666')
- expect(sqliteCloudError.externalErrorCode).toBe('333')
- expect(sqliteCloudError.offsetCode).toBe(-1)
+ it('should test exterror', done => {
+ chinook.sendCommands('TEST EXTERROR', (error, results) => {
+ expect(error).toBeDefined()
+ expect(error).toBeInstanceOf(SQLiteCloudError)
- done()
- })
- })
+ const sqliteCloudError = error as SQLiteCloudError
+ expect(sqliteCloudError.message).toBe('This is a test error message with an extcode and a devil error code.')
+ expect(sqliteCloudError.errorCode).toBe('66666')
+ expect(sqliteCloudError.externalErrorCode).toBe('333')
+ expect(sqliteCloudError.offsetCode).toBe(-1)
- it('should test array', done => {
- chinook.sendCommands('TEST ARRAY', (error, results) => {
- expect(error).toBeNull()
- expect(Array.isArray(results)).toBe(true)
-
- const arrayrowset = results as any as Array
- expect(arrayrowset.length).toBe(5)
- expect(arrayrowset[0]).toBe('Hello World')
- expect(arrayrowset[1]).toBe(123456)
- expect(arrayrowset[2]).toBe(3.1415)
- expect(arrayrowset[3]).toBeNull()
- done()
+ done()
+ })
})
- })
- it('should test rowset', done => {
- chinook.sendCommands('TEST ROWSET', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfRows).toBe(41)
- expect(results.numberOfColumns).toBe(2)
- expect(results.version == 1 || results.version == 2).toBeTruthy()
- expect(results.columnsNames).toEqual(['key', 'value'])
- done()
+ it('should test array', done => {
+ chinook.sendCommands('TEST ARRAY', (error, results) => {
+ expect(error).toBeNull()
+ expect(Array.isArray(results)).toBe(true)
+
+ const arrayrowset = results as any as Array
+ expect(arrayrowset.length).toBe(5)
+ expect(arrayrowset[0]).toBe('Hello World')
+ expect(arrayrowset[1]).toBe(123456)
+ expect(arrayrowset[2]).toBe(3.1415)
+ expect(arrayrowset[3]).toBeNull()
+ done()
+ })
})
- })
- it(
- 'should test chunked rowset',
- done => {
- // this operation sends 150 packets, so we need to increase the timeout
- const database = getChinookWebsocketConnection(undefined, { timeout: 60 * 1000 })
- database.sendCommands('TEST ROWSET_CHUNK', (error, results) => {
+ it('should test rowset', done => {
+ chinook.sendCommands('TEST ROWSET', (error, results) => {
expect(error).toBeNull()
- expect(results.numberOfRows).toBe(147)
- expect(results.numberOfColumns).toBe(1)
- expect(results.columnsNames).toEqual(['key'])
-
- database.close()
+ expect(results.numberOfRows).toBe(41)
+ expect(results.numberOfColumns).toBe(2)
+ expect(results.version == 1 || results.version == 2).toBeTruthy()
+ expect(results.columnsNames).toEqual(['key', 'value'])
done()
})
- },
- LONG_TIMEOUT
- )
- })
-
- describe('operations', () => {
- it(
- 'should serialize operations',
- done => {
- const numQueries = 20
- let completed = 0
+ })
- for (let i = 0; i < numQueries; i++) {
- chinook.sendCommands(`select ${i} as "count", 'hello' as 'string'`, (error, results) => {
+ it(
+ 'should test chunked rowset',
+ done => {
+ // this operation sends 150 packets, so we need to increase the timeout
+ const database = getChinookWebsocketConnection(undefined, { timeout: 60 * 1000 })
+ database.sendCommands('TEST ROWSET_CHUNK', (error, results) => {
expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(2)
- expect(results.numberOfRows).toBe(1)
- expect(results.version == 1 || results.version == 2).toBeTruthy()
- expect(results.columnsNames).toEqual(['count', 'string'])
- expect(results.getItem(0, 0)).toBe(i)
-
- if (++completed >= numQueries) {
- done()
- }
+ expect(results.numberOfRows).toBe(147)
+ expect(results.numberOfColumns).toBe(1)
+ expect(results.columnsNames).toEqual(['key'])
+
+ database.close()
+ done()
})
- }
- },
- LONG_TIMEOUT
- )
- /* TODO RESTORE TEST
+ },
+ LONG_TIMEOUT
+ )
+ })
+
+ describe('operations', () => {
+ it(
+ 'should serialize operations',
+ done => {
+ const numQueries = 20
+ let completed = 0
+
+ for (let i = 0; i < numQueries; i++) {
+ chinook.sendCommands(`select ${i} as "count", 'hello' as 'string'`, (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBe(2)
+ expect(results.numberOfRows).toBe(1)
+ expect(results.version == 1 || results.version == 2).toBeTruthy()
+ expect(results.columnsNames).toEqual(['count', 'string'])
+ expect(results.getItem(0, 0)).toBe(i)
+
+ if (++completed >= numQueries) {
+ done()
+ }
+ })
+ }
+ },
+ LONG_TIMEOUT
+ )
+ /* TODO RESTORE TEST
it('should apply short timeout', done => {
// apply shorter timeout
const configObj = parseConnectionString(CHINOOK_DATABASE_URL + '?timeout=20')
@@ -304,163 +297,164 @@ describe('connection-ws', () => {
})
})
*/
- })
+ })
- describe('send select commands', () => {
- it('should LIST METADATA', done => {
- chinook.sendCommands('LIST METADATA;', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(8)
- expect(results.numberOfRows).toBe(64)
- done()
+ describe('send select commands', () => {
+ it('should LIST METADATA', done => {
+ chinook.sendCommands('LIST METADATA;', (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBe(8)
+ expect(results.numberOfRows).toBe(64)
+ done()
+ })
})
- })
- it('should select results with no colum names', done => {
- chinook.sendCommands("select 42, 'hello'", (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(2)
- expect(results.numberOfRows).toBe(1)
- expect(results.version == 1 || results.version == 2).toBeTruthy()
- expect(results.columnsNames).toEqual(['42', "'hello'"]) // column name should be hello, not 'hello'
- expect(results.getItem(0, 0)).toBe(42)
- expect(results.getItem(0, 1)).toBe('hello')
+ it('should select results with no colum names', done => {
+ chinook.sendCommands("select 42, 'hello'", (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBe(2)
+ expect(results.numberOfRows).toBe(1)
+ expect(results.version == 1 || results.version == 2).toBeTruthy()
+ expect(results.columnsNames).toEqual(['42', "'hello'"]) // column name should be hello, not 'hello'
+ expect(results.getItem(0, 0)).toBe(42)
+ expect(results.getItem(0, 1)).toBe('hello')
- done()
+ done()
+ })
})
- })
- it('should select long formatted string', done => {
- chinook.sendCommands("USE DATABASE :memory:; select printf('%.*c', 1000, 'x') AS DDD", (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(1)
- expect(results.numberOfRows).toBe(1)
- expect(results.version == 1 || results.version == 2).toBeTruthy()
+ it('should select long formatted string', done => {
+ chinook.sendCommands("USE DATABASE :memory:; select printf('%.*c', 1000, 'x') AS DDD", (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBe(1)
+ expect(results.numberOfRows).toBe(1)
+ expect(results.version == 1 || results.version == 2).toBeTruthy()
- const stringrowset = results.getItem(0, 0) as string
- expect(stringrowset.startsWith('xxxxxxxxxxxxx')).toBeTruthy()
- expect(stringrowset).toHaveLength(1000)
+ const stringrowset = results.getItem(0, 0) as string
+ expect(stringrowset.startsWith('xxxxxxxxxxxxx')).toBeTruthy()
+ expect(stringrowset).toHaveLength(1000)
- done()
+ done()
+ })
})
- })
- it('should select database', done => {
- chinook.sendCommands('USE DATABASE chinook.db;', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBeUndefined()
- expect(results.numberOfRows).toBeUndefined()
- expect(results.version).toBeUndefined()
- done()
+ it('should select database', done => {
+ chinook.sendCommands('USE DATABASE chinook.db;', (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBeUndefined()
+ expect(results.numberOfRows).toBeUndefined()
+ expect(results.version).toBeUndefined()
+ done()
+ })
})
- })
- it('should select * from tracks limit 10 (no chunks)', done => {
- chinook.sendCommands('SELECT * FROM tracks LIMIT 10;', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(9)
- expect(results.numberOfRows).toBe(10)
- done()
+ it('should select * from tracks limit 10 (no chunks)', done => {
+ chinook.sendCommands('SELECT * FROM tracks LIMIT 10;', (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBe(9)
+ expect(results.numberOfRows).toBe(10)
+ done()
+ })
})
- })
- it('should select * from tracks (with chunks)', done => {
- chinook.sendCommands('SELECT * FROM tracks;', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(9)
- expect(results.numberOfRows).toBe(3503)
- done()
+ it('should select * from tracks (with chunks)', done => {
+ chinook.sendCommands('SELECT * FROM tracks;', (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBe(9)
+ expect(results.numberOfRows).toBe(3503)
+ done()
+ })
})
- })
- it('should select * from albums', done => {
- chinook.sendCommands('SELECT * FROM albums;', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(3)
- expect(results.numberOfRows).toBe(347)
- expect(results.version == 1 || results.version == 2).toBeTruthy()
- done()
+ it('should select * from albums', done => {
+ chinook.sendCommands('SELECT * FROM albums;', (error, results) => {
+ expect(error).toBeNull()
+ expect(results.numberOfColumns).toBe(3)
+ expect(results.numberOfRows).toBe(347)
+ expect(results.version == 1 || results.version == 2).toBeTruthy()
+ done()
+ })
})
})
- })
- describe('connection stress testing', () => {
- it(
- '20x test string',
- done => {
- const numQueries = 20
- let completed = 0
- const startTime = Date.now()
- for (let i = 0; i < numQueries; i++) {
- chinook.sendCommands('TEST STRING', (error, results) => {
- expect(error).toBeNull()
- expect(results).toBe('Hello World, this is a test string.')
- if (++completed >= numQueries) {
- const queryMs = (Date.now() - startTime) / numQueries
- if (queryMs > WARN_SPEED_MS) {
- console.log(`${numQueries}x test string, ${queryMs.toFixed(0)}ms per query`)
- expect(queryMs).toBeLessThan(EXPECT_SPEED_MS)
- }
- done()
- }
- })
- }
- },
- LONG_TIMEOUT
- )
-
- it(
- '20x individual selects',
- done => {
- const numQueries = 20
- let completed = 0
- const startTime = Date.now()
- for (let i = 0; i < numQueries; i++) {
- chinook.sendCommands('SELECT * FROM albums ORDER BY RANDOM() LIMIT 4;', (error, results) => {
- expect(error).toBeNull()
- expect(results.numberOfColumns).toBe(3)
- expect(results.numberOfRows).toBe(4)
- if (++completed >= numQueries) {
- const queryMs = (Date.now() - startTime) / numQueries
- if (queryMs > WARN_SPEED_MS) {
- console.log(`${numQueries}x individual selects, ${queryMs.toFixed(0)}ms per query`)
- expect(queryMs).toBeLessThan(EXPECT_SPEED_MS)
+ describe('connection stress testing', () => {
+ it(
+ '20x test string',
+ done => {
+ const numQueries = 20
+ let completed = 0
+ const startTime = Date.now()
+ for (let i = 0; i < numQueries; i++) {
+ chinook.sendCommands('TEST STRING', (error, results) => {
+ expect(error).toBeNull()
+ expect(results).toBe('Hello World, this is a test string.')
+ if (++completed >= numQueries) {
+ const queryMs = (Date.now() - startTime) / numQueries
+ if (queryMs > WARN_SPEED_MS) {
+ console.log(`${numQueries}x test string, ${queryMs.toFixed(0)}ms per query`)
+ expect(queryMs).toBeLessThan(EXPECT_SPEED_MS)
+ }
+ done()
}
- done()
- }
- })
- }
- },
- LONG_TIMEOUT
- )
-
- it(
- '20x batched selects',
- done => {
- const numQueries = 20
- let completed = 0
- const startTime = Date.now()
- for (let i = 0; i < numQueries; i++) {
- chinook.sendCommands(
- 'SELECT * FROM albums ORDER BY RANDOM() LIMIT 16; SELECT * FROM albums ORDER BY RANDOM() LIMIT 12; SELECT * FROM albums ORDER BY RANDOM() LIMIT 8; SELECT * FROM albums ORDER BY RANDOM() LIMIT 4;',
- (error, results) => {
+ })
+ }
+ },
+ LONG_TIMEOUT
+ )
+
+ it(
+ '20x individual selects',
+ done => {
+ const numQueries = 20
+ let completed = 0
+ const startTime = Date.now()
+ for (let i = 0; i < numQueries; i++) {
+ chinook.sendCommands('SELECT * FROM albums ORDER BY RANDOM() LIMIT 4;', (error, results) => {
expect(error).toBeNull()
- // server only returns the last rowset?
expect(results.numberOfColumns).toBe(3)
expect(results.numberOfRows).toBe(4)
if (++completed >= numQueries) {
const queryMs = (Date.now() - startTime) / numQueries
if (queryMs > WARN_SPEED_MS) {
- console.log(`${numQueries}x batched selects, ${queryMs.toFixed(0)}ms per query`)
+ console.log(`${numQueries}x individual selects, ${queryMs.toFixed(0)}ms per query`)
expect(queryMs).toBeLessThan(EXPECT_SPEED_MS)
}
done()
}
- }
- )
- }
- },
- LONG_TIMEOUT
- )
+ })
+ }
+ },
+ LONG_TIMEOUT
+ )
+
+ it(
+ '20x batched selects',
+ done => {
+ const numQueries = 20
+ let completed = 0
+ const startTime = Date.now()
+ for (let i = 0; i < numQueries; i++) {
+ chinook.sendCommands(
+ 'SELECT * FROM albums ORDER BY RANDOM() LIMIT 16; SELECT * FROM albums ORDER BY RANDOM() LIMIT 12; SELECT * FROM albums ORDER BY RANDOM() LIMIT 8; SELECT * FROM albums ORDER BY RANDOM() LIMIT 4;',
+ (error, results) => {
+ expect(error).toBeNull()
+ // server only returns the last rowset?
+ expect(results.numberOfColumns).toBe(3)
+ expect(results.numberOfRows).toBe(4)
+ if (++completed >= numQueries) {
+ const queryMs = (Date.now() - startTime) / numQueries
+ if (queryMs > WARN_SPEED_MS) {
+ console.log(`${numQueries}x batched selects, ${queryMs.toFixed(0)}ms per query`)
+ expect(queryMs).toBeLessThan(EXPECT_SPEED_MS)
+ }
+ done()
+ }
+ }
+ )
+ }
+ },
+ LONG_TIMEOUT
+ )
+ })
})
})
diff --git a/test/database.test.ts b/test/database.test.ts
index bc3d4a4..cfff5bc 100644
--- a/test/database.test.ts
+++ b/test/database.test.ts
@@ -4,8 +4,8 @@
import { SQLiteCloudRowset, SQLiteCloudRow, SQLiteCloudError } from '../src/index'
import { getTestingDatabase, getTestingDatabaseAsync, getChinookDatabase, removeDatabase, removeDatabaseAsync, LONG_TIMEOUT } from './shared'
-import { RowCountCallback } from '../src/types'
-import { finished } from 'stream'
+import { RowCountCallback } from '../src/drivers/types'
+import e from 'express'
//
// utility methods to setup and destroy temporary test databases
@@ -38,8 +38,10 @@ describe('Database.run', () => {
})
}
- const database = getTestingDatabase()
- database.run(updateSql, plainCallbackNotALambda)
+ const database = getTestingDatabase(error => {
+ expect(error).toBeNull()
+ database.run(updateSql, plainCallbackNotALambda)
+ })
},
LONG_TIMEOUT
)
@@ -85,8 +87,10 @@ describe('Database.run', () => {
})
}
- const database = getTestingDatabase()
- database.run(insertSql, plainCallbackNotALambdaOne)
+ const database = getTestingDatabase(error => {
+ expect(error).toBeNull()
+ database.run(insertSql, plainCallbackNotALambdaOne)
+ })
},
LONG_TIMEOUT
)
@@ -269,7 +273,7 @@ describe('Database.sql (async)', () => {
it('should work with regular function parameters', async () => {
let database
try {
- database = await getTestingDatabase()
+ database = await getTestingDatabaseAsync()
const results = await database.sql('SELECT * FROM people WHERE name = ?', 'Emma Johnson')
expect(results).toHaveLength(1)
} finally {
@@ -280,7 +284,7 @@ describe('Database.sql (async)', () => {
it('should select and return multiple rows', async () => {
let database
try {
- database = await getTestingDatabase()
+ database = await getTestingDatabaseAsync()
const results = await database.sql('SELECT * FROM people ORDER BY id')
expect(results).toBeDefined()
diff --git a/test/protocol.test.ts b/test/protocol.test.ts
new file mode 100644
index 0000000..c332c0d
--- /dev/null
+++ b/test/protocol.test.ts
@@ -0,0 +1,31 @@
+//
+// protocol.test.ts
+//
+
+import { parseRowsetChunks } from '../src/drivers/protocol'
+
+// response sent by the server when we TEST ROWSET_CHUNK
+const CHUNKED_RESPONSE = Buffer.from(
+ '/24 1:1 1 1 +3 key+7 REINDEX/18 2:1 1 1 +7 INDEXED/16 3:1 1 1 +5 INDEX/15 4:1 1 1 +4 DESC/17 5:1 1 1 +6 ESCAPE/15 6:1 1 1 +4 EACH/16 7:1 1 1 +5 CHECK/14 8:1 1 1 +3 KEY/17 9:1 1 1 +6 BEFORE/19 10:1 1 1 +7 FOREIGN/15 11:1 1 1 +3 FOR/18 12:1 1 1 +6 IGNORE/18 13:1 1 1 +6 REGEXP/19 14:1 1 1 +7 EXPLAIN/19 15:1 1 1 +7 INSTEAD/15 16:1 1 1 +3 ADD/20 17:1 1 1 +8 DATABASE/14 18:1 1 1 +2 AS/18 19:1 1 1 +6 SELECT/17 20:1 1 1 +5 TABLE/16 21:1 1 1 +4 LEFT/16 22:1 1 1 +4 THEN/15 23:1 1 1 +3 END/23 24:1 1 1 +10 DEFERRABLE/16 25:1 1 1 +4 ELSE/19 26:1 1 1 +7 EXCLUDE/18 27:1 1 1 +6 DELETE/21 28:1 1 1 +9 TEMPORARY/16 29:1 1 1 +4 TEMP/14 30:1 1 1 +2 OR/18 31:1 1 1 +6 ISNULL/17 32:1 1 1 +5 NULLS/21 33:1 1 1 +9 SAVEPOINT/21 34:1 1 1 +9 INTERSECT/16 35:1 1 1 +4 TIES/19 36:1 1 1 +7 NOTNULL/15 37:1 1 1 +3 NOT/14 38:1 1 1 +2 NO/16 39:1 1 1 +4 NULL/16 40:1 1 1 +4 LIKE/18 41:1 1 1 +6 EXCEPT/24 42:1 1 1 +11 TRANSACTION/18 43:1 1 1 +6 ACTION/14 44:1 1 1 +2 ON/19 45:1 1 1 +7 NATURAL/17 46:1 1 1 +5 ALTER/17 47:1 1 1 +5 RAISE/21 48:1 1 1 +9 EXCLUSIVE/18 49:1 1 1 +6 EXISTS/23 50:1 1 1 +10 CONSTRAINT/16 51:1 1 1 +4 INTO/18 52:1 1 1 +6 OFFSET/14 53:1 1 1 +2 OF/15 54:1 1 1 +3 SET/19 55:1 1 1 +7 TRIGGER/17 56:1 1 1 +5 RANGE/21 57:1 1 1 +9 GENERATED/18 58:1 1 1 +6 DETACH/18 59:1 1 1 +6 HAVING/16 60:1 1 1 +4 GLOB/17 61:1 1 1 +5 BEGIN/17 62:1 1 1 +5 INNER/23 63:1 1 1 +10 REFERENCES/18 64:1 1 1 +6 UNIQUE/17 65:1 1 1 +5 QUERY/19 66:1 1 1 +7 WITHOUT/16 67:1 1 1 +4 WITH/17 68:1 1 1 +5 OUTER/19 69:1 1 1 +7 RELEASE/18 70:1 1 1 +6 ATTACH/19 71:1 1 1 +7 BETWEEN/19 72:1 1 1 +7 NOTHING/18 73:1 1 1 +6 GROUPS/17 74:1 1 1 +5 GROUP/19 75:1 1 1 +7 CASCADE/15 76:1 1 1 +3 ASC/19 77:1 1 1 +7 DEFAULT/16 78:1 1 1 +4 CASE/19 79:1 1 1 +7 COLLATE/18 80:1 1 1 +6 CREATE/25 81:1 1 1 +12 CURRENT_DATE/21 82:1 1 1 +9 IMMEDIATE/16 83:1 1 1 +4 JOIN/18 84:1 1 1 +6 INSERT/17 85:1 1 1 +5 MATCH/16 86:1 1 1 +4 PLAN/19 87:1 1 1 +7 ANALYZE/18 88:1 1 1 +6 PRAGMA/25 89:1 1 1 +12 MATERIALIZED/20 90:1 1 1 +8 DEFERRED/20 91:1 1 1 +8 DISTINCT/14 92:1 1 1 +2 IS/18 93:1 1 1 +6 UPDATE/18 94:1 1 1 +6 VALUES/19 95:1 1 1 +7 VIRTUAL/18 96:1 1 1 +6 ALWAYS/16 97:1 1 1 +4 WHEN/17 98:1 1 1 +5 WHERE/21 99:1 1 1 +9 RECURSIVE/18 100:1 1 1 +5 ABORT/18 101:1 1 1 +5 AFTER/19 102:1 1 1 +6 RENAME/16 103:1 1 1 +3 AND/17 104:1 1 1 +4 DROP/22 105:1 1 1 +9 PARTITION/27 106:1 1 1 +13 AUTOINCREMENT/15 107:1 1 1 +2 TO/15 108:1 1 1 +2 IN/17 109:1 1 1 +4 CAST/19 110:1 1 1 +6 COLUMN/19 111:1 1 1 +6 COMMIT/21 112:1 1 1 +8 CONFLICT/18 113:1 1 1 +5 CROSS/31 114:1 1 1 +17 CURRENT_TIMESTAMP/26 115:1 1 1 +12 CURRENT_TIME/20 116:1 1 1 +7 CURRENT/22 117:1 1 1 +9 PRECEDING/17 118:1 1 1 +4 FAIL/17 119:1 1 1 +4 LAST/19 120:1 1 1 +6 FILTER/20 121:1 1 1 +7 REPLACE/18 122:1 1 1 +5 FIRST/22 123:1 1 1 +9 FOLLOWING/17 124:1 1 1 +4 FROM/17 125:1 1 1 +4 FULL/18 126:1 1 1 +5 LIMIT/15 127:1 1 1 +2 IF/18 128:1 1 1 +5 ORDER/21 129:1 1 1 +8 RESTRICT/19 130:1 1 1 +6 OTHERS/17 131:1 1 1 +4 OVER/22 132:1 1 1 +9 RETURNING/18 133:1 1 1 +5 RIGHT/21 134:1 1 1 +8 ROLLBACK/17 135:1 1 1 +4 ROWS/16 136:1 1 1 +3 ROW/22 137:1 1 1 +9 UNBOUNDED/18 138:1 1 1 +5 UNION/18 139:1 1 1 +5 USING/19 140:1 1 1 +6 VACUUM/17 141:1 1 1 +4 VIEW/19 142:1 1 1 +6 WINDOW/15 143:1 1 1 +2 DO/15 144:1 1 1 +2 BY/22 145:1 1 1 +9 INITIALLY/16 146:1 1 1 +3 ALL/20 147:1 1 1 +7 PRIMARY/6 0 0 0 '
+)
+
+describe('parseRowsetChunks', () => {
+ it('should extract rowset from single buffer', () => {
+ const rowset = parseRowsetChunks([CHUNKED_RESPONSE])
+ expect(rowset.length).toBe(147)
+ expect(rowset[0]['key']).toBe('REINDEX')
+ expect(rowset[146]['key']).toBe('PRIMARY')
+ })
+
+ it('should extract rowset from segmented buffers', () => {
+ // split CHUNKED_RESPONSE into 3 random sized buffers
+ const buffer1 = CHUNKED_RESPONSE.slice(0, 100)
+ const buffer2 = CHUNKED_RESPONSE.slice(100, 200)
+ const buffer3 = CHUNKED_RESPONSE.slice(200)
+
+ const rowset = parseRowsetChunks([buffer1, buffer2, buffer3])
+ expect(rowset.length).toBe(147)
+ expect(rowset[0]['key']).toBe('REINDEX')
+ expect(rowset[146]['key']).toBe('PRIMARY')
+ })
+})
diff --git a/test/shared.ts b/test/shared.ts
index c3a17c6..3b1e7a4 100644
--- a/test/shared.ts
+++ b/test/shared.ts
@@ -4,12 +4,16 @@
import { join } from 'path'
import { readFileSync } from 'fs'
-import { Database } from '../src/database'
-import { ResultsCallback, SQLiteCloudConfig } from '../src/types'
-import { parseConnectionString } from '../src/utilities'
+import { Database } from '../src/drivers/database'
+import { ResultsCallback, SQLiteCloudConfig, SQLiteCloudError } from '../src/drivers/types'
+import { parseConnectionString } from '../src/drivers/utilities'
+
+import { SQLiteCloudTlsConnection } from '../src/drivers/connection-tls'
+import { SQLiteCloudWebsocketConnection } from '../src/drivers/connection-ws'
import * as dotenv from 'dotenv'
-import { SQLiteCloudConnection } from '../src'
+import { SQLiteCloudConnection, SQLiteCloudRowset } from '../src'
+import e from 'express'
dotenv.config()
export const LONG_TIMEOUT = 1 * 60 * 1000 // 1 minute
@@ -21,7 +25,7 @@ export const WARN_SPEED_MS = 500
export const EXPECT_SPEED_MS = 6 * 1000
/** Number of times or size of stress (when repeated in sequence) */
-export const SEQUENCE_TEST_SIZE = 75
+export const SEQUENCE_TEST_SIZE = 150
/** Concurrency size for multiple connection tests */
export const SIMULTANEOUS_TEST_SIZE = 150
@@ -100,13 +104,13 @@ export function getChinookWebsocketConnection(callback?: ResultsCallback, extraC
useWebsocket: true,
gatewayUrl: GATEWAY_URL
}
- const chinookConnection = new SQLiteCloudConnection(chinookConfig, callback)
+ const chinookConnection = new SQLiteCloudWebsocketConnection(chinookConfig, callback)
return chinookConnection
}
export function getChinookTlsConnection(callback?: ResultsCallback, extraConfig?: Partial): SQLiteCloudConnection {
const chinookConfig = getChinookConfig(CHINOOK_DATABASE_URL, extraConfig)
- return new SQLiteCloudConnection(chinookConfig, callback)
+ return new SQLiteCloudTlsConnection(chinookConfig, callback)
}
/** Returns a chinook.db connection, caller is responsible for closing the database */
@@ -157,18 +161,42 @@ export function getTestingConfig(url = TESTING_DATABASE_URL): SQLiteCloudConfig
export function getTestingDatabase(callback?: ResultsCallback): Database {
const testingConfig = getTestingConfig()
- const database = new Database(testingConfig)
+ const database = new Database(testingConfig, error => {
+ if (error) {
+ console.error(`getTestingDatabase - connection error: ${error}`)
+ callback?.call(database, error)
+ }
+ database.run(TESTING_SQL, (error: SQLiteCloudError, results: SQLiteCloudRowset) => {
+ if (error) {
+ console.error(`getTestingDatabase - setup error: ${error}`)
+ callback?.call(database, error)
+ }
+ expect(results).toBeDefined()
+ expect(results[0][42]).toBe(42)
+ callback?.call(database, null)
+ })
+ })
+
// database.verbose()
- database.exec(TESTING_SQL, callback)
return database
}
export async function getTestingDatabaseAsync(): Promise {
const testingConfig = getTestingConfig()
- const database = new Database(testingConfig)
- // database.verbose()
- await database.sql(TESTING_SQL)
- return database
+ return new Promise((resolve, reject) => {
+ const database = new Database(testingConfig, error => {
+ if (error) {
+ reject(error)
+ }
+ database.run(TESTING_SQL, (error: SQLiteCloudError, results: SQLiteCloudRowset) => {
+ if (error) {
+ reject(error)
+ }
+ expect(results[0]['42']).toBe(42)
+ resolve(database)
+ })
+ })
+ })
}
/** Drop databases that are no longer in use */
diff --git a/test/statement.test.ts b/test/statement.test.ts
index 4797270..ae7d621 100644
--- a/test/statement.test.ts
+++ b/test/statement.test.ts
@@ -3,7 +3,7 @@
*/
import { SQLiteCloudRowset } from '../src'
-import { RowCallback, RowCountCallback, SQLiteCloudError } from '../src/types'
+import { RowCallback, RowCountCallback, SQLiteCloudError } from '../src/drivers/types'
import { getChinookDatabase } from './shared'
describe('Database.prepare', () => {
diff --git a/test/utilities.test.ts b/test/utilities.test.ts
index be9d78c..5036a27 100644
--- a/test/utilities.test.ts
+++ b/test/utilities.test.ts
@@ -3,7 +3,7 @@
//
import { SQLiteCloudError } from '../src/index'
-import { prepareSql, parseConnectionString } from '../src/utilities'
+import { prepareSql, parseConnectionString } from '../src/drivers/utilities'
describe('prepareSql', () => {
it('should replace single ? parameter', () => {
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 8a42a1a..439689a 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -1,4 +1,4 @@
{
"extends": "./tsconfig.json",
- "exclude": ["test/"]
+ "exclude": ["test/", "src/gateway/"]
}
diff --git a/tsconfig.json b/tsconfig.json
index 084a1ef..92a9299 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -66,7 +66,10 @@
/* Advanced Options */
"skipLibCheck": true /* Skip type checking of declaration files. */,
- "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
+
+ /** Loaders for json files */
+ "resolveJsonModule": true
},
"include": ["src/**/*.ts", "test/**/*.ts"],
diff --git a/webpack.config.js b/webpack.config.js
index b9df8df..0fa2435 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -11,7 +11,7 @@ const productionConfig = {
// Output configuration
output: {
path: path.resolve(__dirname, 'lib'),
- filename: `sqlitecloud.v${packageJson.version}.js`,
+ filename: `sqlitecloud.drivers.js`,
library: 'sqlitecloud',
libraryTarget: 'umd',
globalObject: 'this'
@@ -35,6 +35,7 @@ const productionConfig = {
const devConfig = JSON.parse(JSON.stringify(productionConfig))
devConfig.mode = 'development'
devConfig.optimization.minimize = false
-devConfig.output.filename = `sqlitecloud.v${packageJson.version}.dev.js`
+devConfig.output.filename = `sqlitecloud.drivers.dev.js`
+// devConfig.output.filename = `sqlitecloud.v${packageJson.version}.dev.js`
module.exports = [productionConfig, devConfig]