sonic-ws 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- var e,t=this&&this.__createBinding||(Object.create?function(e,t,s,r){void 0===r&&(r=s);var n=Object.getOwnPropertyDescriptor(t,s);n&&!("get"in n?!t.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,r,n)}:function(e,t,s,r){void 0===r&&(r=s),e[r]=t[s]}),s=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),r=this&&this.__importStar||(e=function(t){return e=Object.getOwnPropertyNames||function(e){var t=[];for(var s in e)Object.prototype.hasOwnProperty.call(e,s)&&(t[t.length]=s);return t},e(t)},function(r){if(r&&r.__esModule)return r;var n={};if(null!=r)for(var a=e(r),i=0;i<a.length;i++)"default"!==a[i]&&t(n,r,a[i]);return s(n,r),n});Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSServer=void 0;const n=r(require("ws")),a=require("node:fs/promises"),i=require("node:path"),o=require("./SonicWSConnection"),c=require("../util/packets/PacketHolder"),d=require("../util/packets/CompressionUtil"),h=require("../../native/wrapper"),l=require("../../version"),u=require("../util/packets/PacketUtils"),p=require("../PacketProcessor"),g=require("../util/packets/HashUtil"),v=require("../Connection"),w=require("../debug/DebugServer"),f=require("node:crypto"),b=require("../util/packets/ControlProtocol"),k=e=>{if(!Number.isFinite(e)||e<0)throw new Error("Rate limit must be a non-negative finite number.");return(e=Math.floor(e))>d.MAX_USHORT?(console.warn(`A rate limit above ${d.MAX_USHORT} is considered infinite.`),0):e};class m extends p.MiddlewareHolder{wss;availableIds=[];lastId=0;connectListeners=[];recoveredListeners=[];clientPackets;serverPackets;connections=[];connectionMap={};clientRateLimit=500;serverRateLimit=500;handshakePacket=null;tags=new Map;tagsInv=new Map;serverwideSendQueue=[!1,[],void 0];sendErrorHandler;adapter;serverId=(0,f.randomUUID)();sessions=new Map;recoveryMaxDisconnectionMs;recoveryMaxPackets;constructor(e){super();const{clientPackets:t=[],serverPackets:s=[],websocketOptions:r={}}=e;if(this.sendErrorHandler=e.onSendError,this.adapter=e.adapter,this.recoveryMaxDisconnectionMs=e.recovery?.maxDisconnectionMs??12e4,this.recoveryMaxPackets=e.recovery?.maxPackets??1e3,!Number.isFinite(this.recoveryMaxDisconnectionMs)||this.recoveryMaxDisconnectionMs<0)throw new Error("recovery.maxDisconnectionMs must be a non-negative finite number");if(!Number.isInteger(this.recoveryMaxPackets)||this.recoveryMaxPackets<0)throw new Error("recovery.maxPackets must be a non-negative integer");if(this.wss=new n.WebSocketServer(r),e.sonicServerSettings?.serveBrowserClient??1){const e=r.server??this.wss._server;e&&this.installBrowserAssetRoutes(e)}this.clientPackets=new c.PacketHolder(t),this.serverPackets=new c.PacketHolder(s),Promise.resolve(this.adapter?.start?.(this.serverId,e=>this.receiveAdapterBroadcast(e))).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",operation:"broadcast"}));const a=this.clientPackets.serialize(),i=this.serverPackets.serialize(),u=[...l.SERVER_SUFFIX_NUMS,l.VERSION],p=[...(0,d.convertVarInt)(a.length),...a,...i];(0,g.setHashFunc)(e.sonicServerSettings?.bit64Hash??!0),this.wss.on("connection",async e=>{const t=(0,f.randomUUID)(),s={state:{},rooms:new Set,sequence:0,frames:[],expiresAt:1/0};this.sessions.set(t,s);const r=new o.SonicWSConnection(e,this,this.generateSocketID(),this.handshakePacket,this.clientRateLimit,this.serverRateLimit,t,s.state);if(await this.callMiddleware("onClientConnect",r))return r.close(v.CloseCodes.MIDDLEWARE,"Connection blocked by middleware."),this.callMiddleware("onClientDisconnect",r,v.CloseCodes.MIDDLEWARE,Buffer.from("Connection blocked by middleware.")),this.availableIds.push(r.id),void this.sessions.delete(t);const n=(new TextEncoder).encode(t),a=new Uint8Array([...(0,d.convertVarInt)(r.id),...(0,d.convertVarInt)(n.length),...n,...p]);e.send([...u,...(0,h.deflateNative)(a)]),this.connections.push(r),this.connectionMap[r.id]=r,this.connectListeners.forEach(e=>e(r)),e.on("close",(e,t)=>{const s=new Set(this.tags.get(r)??[]);if(this.connections.splice(this.connections.indexOf(r),1),delete this.connectionMap[r.id],this.availableIds.push(r.id),this.tags.has(r)){for(const e of this.tags.get(r))this.tagsInv.get(e)?.delete(r);this.tags.delete(r)}Promise.resolve(this.adapter?.disconnect(r.id)).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",connection:r}));const n=this.sessions.get(r.sessionId);if(n){n.rooms=s,n.expiresAt=Date.now()+this.recoveryMaxDisconnectionMs;const e=setTimeout(()=>{const e=this.sessions.get(r.sessionId);e===n&&e.expiresAt<=Date.now()&&this.sessions.delete(r.sessionId)},this.recoveryMaxDisconnectionMs+1);e.unref?.()}this.callMiddleware("onClientDisconnect",r,e,t)})}),(e.sonicServerSettings?.checkForUpdates??1)&&fetch("https://raw.githubusercontent.com/liwybloc/sonic-ws/refs/heads/main/release/version").then(e=>e.text()).then(e=>{parseInt(e)>l.VERSION&&console.warn(`SonicWS is currently running outdated! (current: ${l.VERSION}, latest: ${e}) Update with "npm update sonic-ws"`)}).catch(e=>{console.error(e),console.warn("Could not check SonicWS version.")})}installBrowserAssetRoutes(e){const t=e.listeners("request");e.removeAllListeners("request"),e.on("request",(s,r)=>{const n=new URL(s.url??"/","http://localhost").pathname,o="/SonicWS/bundle.js"===n?["bundle.js","text/javascript; charset=utf-8"]:"/SonicWS/bundle.wasm"===n?["bundle.wasm","application/wasm"]:void 0;if(!o){for(const n of t)Reflect.apply(n,e,[s,r]);return}const c=(0,i.resolve)(__dirname,"../../../bundled",o[0]);(0,a.readFile)(c).then(e=>{r.writeHead(200,{"content-type":o[1],"content-length":e.byteLength,"cache-control":"public, max-age=3600"}),r.end(e)}).catch(e=>{r.writeHead(500,{"content-type":"text/plain; charset=utf-8"}),r.end(`Unable to load SonicWS browser asset: ${e instanceof Error?e.message:String(e)}`)})})}generateSocketID(){return 0==this.availableIds.length&&this.availableIds.push(this.lastId+1),this.lastId=this.availableIds.shift(),this.lastId}requireHandshake(e){if(!this.clientPackets.hasTag(e))throw new Error(`The client does not send "${e}" and so it cannot use it as a handshake!`);if(0!=this.clientPackets.getPacket(e).dataBatching)throw new Error(`The packet "${e}" is a batched packet, and cannot be used as a handshake!`);this.handshakePacket=e}setClientRateLimit(e){this.clientRateLimit=k(e)}setServerRateLimit(e){this.serverRateLimit=k(e)}enablePacket(e){this.clientPackets.getPacket(e).defaultEnabled=!0,this.connections.forEach(t=>t.enablePacket(e))}disablePacket(e){this.clientPackets.getPacket(e).defaultEnabled=!1,this.connections.forEach(t=>t.disablePacket(e))}on_connect(e){this.connectListeners.push(e)}on_recovered(e){this.recoveredListeners.push(e)}on_ready(e){this.wss.on("listening",e)}shutdown(e){this.sessions.clear(),Promise.resolve(this.adapter?.close?.()).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",operation:"broadcast"})),this.wss.close(e)}async receiveAdapterBroadcast(e){e.origin!==this.serverId&&await this.broadcastInternal(e.packetTag,{type:"filter",filter:t=>!0===this.tags.get(t)?.has(e.room)&&t.id!==e.exceptConnectionId},e.values)}replayFrame(e,t){const s=this.sessions.get(e.sessionId);if(!s)return t;const r=++s.sequence,n=(0,b.encodeReplay)(r,t);return s.frames.push({sequence:r,data:n}),s.frames.length>this.recoveryMaxPackets&&s.frames.splice(0,s.frames.length-this.recoveryMaxPackets),n}async resumeSession(e,t,s){const r=this.sessions.get(t);if(!r||r.expiresAt<Date.now())return void e.raw_send((0,b.encodeResumed)(!1,0));this.sessions.delete(e.sessionId),e.sessionId=t,e.state=r.state,r.expiresAt=1/0,this.sessions.set(t,r);for(const t of r.rooms)this.join(e,t);const n=r.frames.filter(e=>e.sequence>s);for(const t of n)e.raw_send(t.data);e.raw_send((0,b.encodeResumed)(!0,n.length));for(const t of this.recoveredListeners)await t(e,n.length)}async broadcastInternal(e,t,s){let r;if("all"===t.type)r=this.connections;else if("tagged"===t.type){if(!this.tagsInv.has(t.tag))return;r=Array.from(this.tagsInv.get(t.tag))}else r=this.connections.filter(t.filter);if(await this.callMiddleware("onPacketBroadcast_pre",e,{recipients:r,...t},s))return;if(0===r.length)return;const[n,a,i]=await(0,u.processPacket)(this.serverPackets,e,s,this.serverwideSendQueue,-1);await this.callMiddleware("onPacketBroadcast_post",e,{recipients:r,...t},a,a.length)||r.forEach(e=>e.send_processed(n,a,i))}async broadcastTagged(e,t,...s){await this.broadcastInternal(t,{type:"tagged",tag:e},s)}async broadcastRoom(e,t,...s){await this.broadcastInternal(t,{type:"tagged",tag:e},s),await(this.adapter?.publish({origin:this.serverId,room:e,packetTag:t,values:s}))}async broadcastRoomExcept(e,t,s,...r){await this.broadcastInternal(s,{type:"filter",filter:s=>s!==e&&!0===this.tags.get(s)?.has(t)},r),await(this.adapter?.publish({origin:this.serverId,room:t,packetTag:s,values:r,exceptConnectionId:e.id}))}async broadcastFiltered(e,t,...s){await this.broadcastInternal(e,{type:"filter",filter:t},s)}async broadcast(e,...t){await this.broadcastInternal(e,{type:"all"},t)}async broadcastSafe(e,...t){try{return await this.broadcast(e,...t),!0}catch(t){return this.handleSendError(t,{packetTag:e,operation:"broadcast"}),!1}}broadcastVariant(e,t,...s){return this.broadcast(this.serverPackets.getVariantTag(e,t),...s)}handleSendError(e,t){this.sendErrorHandler?this.sendErrorHandler(e,t):console.error(`Failed to send packet "${t.packetTag}"`,e)}getConnected(){return this.connections}getSocket(e){return this.connectionMap[e]}closeSocket(e,t=1e3,s){this.getSocket(e).close(t,s)}tag(e,t,s=!0){this.tags.get(e)||this.tags.set(e,new Set),this.tagsInv.get(t)||this.tagsInv.set(t,new Set),s&&(this.tags.get(e).forEach(t=>{this.tagsInv.get(t)?.delete(e),Promise.resolve(this.adapter?.leave(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}),this.tags.get(e).clear()),this.tags.get(e).add(t),this.tagsInv.get(t).add(e),Promise.resolve(this.adapter?.join(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}join(e,t){if(!t)throw new Error("Room name cannot be empty");this.tag(e,t,!1)}leave(e,t){this.tags.get(e)?.delete(t);const s=this.tagsInv.get(t);s?.delete(e),0===s?.size&&this.tagsInv.delete(t),Promise.resolve(this.adapter?.leave(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}rooms(e){return this.tags.get(e)??new Set}debugServer=null;OpenDebug(e={}){if(null!=this.debugServer)throw new Error("Attempted to open a debug server when one has already been opened.");this.debugServer=new w.DebugServer(this,e)}}exports.SonicWSServer=m;
7
+ var e,t=this&&this.__createBinding||(Object.create?function(e,t,s,r){void 0===r&&(r=s);var n=Object.getOwnPropertyDescriptor(t,s);n&&!("get"in n?!t.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,r,n)}:function(e,t,s,r){void 0===r&&(r=s),e[r]=t[s]}),s=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),r=this&&this.__importStar||(e=function(t){return e=Object.getOwnPropertyNames||function(e){var t=[];for(var s in e)Object.prototype.hasOwnProperty.call(e,s)&&(t[t.length]=s);return t},e(t)},function(r){if(r&&r.__esModule)return r;var n={};if(null!=r)for(var a=e(r),i=0;i<a.length;i++)"default"!==a[i]&&t(n,r,a[i]);return s(n,r),n});Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSServer=void 0;const n=r(require("ws")),a=require("node:fs/promises"),i=require("node:path"),o=require("./SonicWSConnection"),c=require("../util/packets/PacketHolder"),d=require("../util/packets/CompressionUtil"),h=require("../../native/wrapper"),l=require("../../version"),u=require("../util/packets/PacketUtils"),v=require("../PacketProcessor"),g=require("../util/packets/HashUtil"),p=require("../Connection"),f=require("../debug/DebugServer"),w=require("node:crypto"),b=require("../util/packets/ControlProtocol");function m(e){if(!Number.isFinite(e)||e<0)throw new Error("Rate limit must be a non-negative finite number");return(e=Math.floor(e))>d.MAX_USHORT?(console.warn(`A rate limit above ${d.MAX_USHORT} is considered infinite`),0):e}class S extends v.MiddlewareHolder{wss;availableIds=[];lastId=0;connectListeners=[];recoveredListeners=[];clientPackets;serverPackets;connections=[];connectionMap={};clientRateLimit=500;serverRateLimit=500;handshakePacket=null;tags=new Map;tagsInv=new Map;serverwideSendQueue=[!1,[],void 0];sendErrorHandler;adapter;serverId=(0,w.randomUUID)();sessions=new Map;recoveryMaxDisconnectionMs;recoveryMaxPackets;recoveryAuthorize;constructor(e){super();const{clientPackets:t=[],serverPackets:s=[],websocketOptions:r={}}=e;if(this.sendErrorHandler=e.onSendError,this.adapter=e.adapter,this.recoveryMaxDisconnectionMs=e.recovery?.maxDisconnectionMs??12e4,this.recoveryMaxPackets=e.recovery?.maxPackets??1e3,this.recoveryAuthorize=e.recovery?.authorize,!Number.isFinite(this.recoveryMaxDisconnectionMs)||this.recoveryMaxDisconnectionMs<0)throw new Error("recovery.maxDisconnectionMs must be a non-negative finite number");if(!Number.isInteger(this.recoveryMaxPackets)||this.recoveryMaxPackets<0)throw new Error("recovery.maxPackets must be a non-negative integer");const a=e.sonicServerSettings?.handshakeTimeoutMs??1e4,i=e.sonicServerSettings?.heartbeatIntervalMs??3e4,u=e.sonicServerSettings?.heartbeatTimeoutMs??1e4;if(!Number.isFinite(a)||a<=0)throw new Error("handshakeTimeoutMs must be positive");if(!Number.isFinite(i)||i<0||!Number.isFinite(u)||u<=0)throw new Error("Invalid heartbeat timing options");if(this.wss=new n.WebSocketServer({maxPayload:8388608,...r}),e.sonicServerSettings?.serveBrowserClient??1){const e=r.server??this.wss._server;e&&this.installBrowserAssetRoutes(e)}this.clientPackets=new c.PacketHolder(t),this.serverPackets=new c.PacketHolder(s),Promise.resolve(this.adapter?.start?.(this.serverId,e=>this.receiveAdapterBroadcast(e))).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",operation:"broadcast"}));const v=this.clientPackets.serialize(),f=this.serverPackets.serialize(),b=[...l.SERVER_SUFFIX_NUMS,l.VERSION],m=[...(0,d.convertVarInt)(v.length),...v,...f];(0,g.setHashFunc)(e.sonicServerSettings?.bit64Hash??!0),this.wss.on("connection",async(t,s)=>{const r=e.sonicServerSettings?.heartbeatIntervalMs??3e4,a=e.sonicServerSettings?.heartbeatTimeoutMs??1e4;let i;const c=r>0?setInterval(()=>{t.readyState===n.WebSocket.OPEN&&(t.ping(),i&&clearTimeout(i),i=setTimeout(()=>t.terminate(),a),i.unref?.())},r):void 0;c?.unref?.(),t.on("pong",()=>{i&&clearTimeout(i),i=void 0});const l=(0,w.randomUUID)(),u={state:{},rooms:new Set,sequence:0,frames:[],expiresAt:1/0};this.sessions.set(l,u);const v=new o.SonicWSConnection(t,this,this.generateSocketID(),this.handshakePacket,this.clientRateLimit,this.serverRateLimit,l,u.state,e.sonicServerSettings?.handshakeTimeoutMs??1e4,s);if(await this.callMiddleware("onClientConnect",v)){const e="Connection blocked by middleware";return v.close(p.CloseCodes.MIDDLEWARE,e),this.callMiddleware("onClientDisconnect",v,p.CloseCodes.MIDDLEWARE,Buffer.from(e)),this.availableIds.push(v.id),void this.sessions.delete(l)}const g=(new TextEncoder).encode(l),f=new Uint8Array([...(0,d.convertVarInt)(v.id),...(0,d.convertVarInt)(g.length),...g,...m]);t.send([...b,...(0,h.deflateNative)(f)]),this.connections.push(v),this.connectionMap[v.id]=v,this.connectListeners.forEach(e=>e(v)),t.on("close",(e,t)=>{c&&clearInterval(c),i&&clearTimeout(i);const s=new Set(this.tags.get(v)??[]);if(this.connections.splice(this.connections.indexOf(v),1),delete this.connectionMap[v.id],this.availableIds.push(v.id),this.tags.has(v)){for(const e of this.tags.get(v))this.tagsInv.get(e)?.delete(v);this.tags.delete(v)}Promise.resolve(this.adapter?.disconnect(v.id)).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",connection:v}));const r=this.sessions.get(v.sessionId);if(r){r.rooms=s,r.expiresAt=Date.now()+this.recoveryMaxDisconnectionMs;const e=setTimeout(()=>{const e=this.sessions.get(v.sessionId);e===r&&e.expiresAt<=Date.now()&&this.sessions.delete(v.sessionId)},this.recoveryMaxDisconnectionMs+1);e.unref?.()}this.callMiddleware("onClientDisconnect",v,e,t)})}),(e.sonicServerSettings?.checkForUpdates??1)&&fetch("https://raw.githubusercontent.com/liwybloc/sonic-ws/refs/heads/main/release/version").then(e=>e.text()).then(e=>{parseInt(e)>l.VERSION&&console.warn(`SonicWS protocol ${l.VERSION} is outdated, latest is ${e}. Update with "npm update sonic-ws"`)}).catch(e=>{console.error(e),console.warn("Could not check the SonicWS protocol version")})}installBrowserAssetRoutes(e){const t=e.listeners("request");e.removeAllListeners("request"),e.on("request",(s,r)=>{const n=new URL(s.url??"/","http://localhost").pathname,o="/SonicWS/bundle.js"===n?["bundle.js","text/javascript; charset=utf-8"]:"/SonicWS/bundle.wasm"===n?["bundle.wasm","application/wasm"]:void 0;if(!o){for(const n of t)Reflect.apply(n,e,[s,r]);return}const c=(0,i.resolve)(__dirname,"../../../bundled",o[0]);(0,a.readFile)(c).then(e=>{r.writeHead(200,{"content-type":o[1],"content-length":e.byteLength,"cache-control":"public, max-age=3600"}),r.end(e)}).catch(e=>{r.writeHead(500,{"content-type":"text/plain; charset=utf-8"}),r.end(`Unable to load SonicWS browser asset: ${e instanceof Error?e.message:String(e)}`)})})}generateSocketID(){return 0===this.availableIds.length&&this.availableIds.push(this.lastId+1),this.lastId=this.availableIds.shift(),this.lastId}requireHandshake(e){if(!this.clientPackets.hasTag(e))throw new Error(`The client does not define "${e}" so it cannot be used as a handshake`);if(0!==this.clientPackets.getPacket(e).dataBatching)throw new Error(`The batched packet "${e}" cannot be used as a handshake`);this.handshakePacket=e}setClientRateLimit(e){this.clientRateLimit=m(e)}setServerRateLimit(e){this.serverRateLimit=m(e)}enablePacket(e){this.clientPackets.getPacket(e).defaultEnabled=!0,this.connections.forEach(t=>t.enablePacket(e))}disablePacket(e){this.clientPackets.getPacket(e).defaultEnabled=!1,this.connections.forEach(t=>t.disablePacket(e))}on_connect(e){this.connectListeners.push(e)}on_recovered(e){this.recoveredListeners.push(e)}on_ready(e){this.wss.on("listening",e)}shutdown(e){this.sessions.clear(),Promise.resolve(this.adapter?.close?.()).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",operation:"broadcast"})),this.wss.close(e)}async receiveAdapterBroadcast(e){e.origin!==this.serverId&&await this.broadcastInternal(e.packetTag,{type:"filter",filter:t=>!0===this.tags.get(t)?.has(e.room)&&t.id!==e.exceptConnectionId},e.values)}replayFrame(e,t){const s=this.sessions.get(e.sessionId);if(!s)return t;const r=++s.sequence,n=(0,b.encodeReplay)(r,t);return s.frames.push({sequence:r,data:n}),s.frames.length>this.recoveryMaxPackets&&s.frames.splice(0,s.frames.length-this.recoveryMaxPackets),n}async resumeSession(e,t,s){const r=this.sessions.get(t);if(!r||r.expiresAt<Date.now())return void e.raw_send((0,b.encodeResumed)(!1,0));if(!(this.recoveryAuthorize?await this.recoveryAuthorize(r.state,e.state,e):void 0===r.state.userId||r.state.userId===e.state.userId))return void e.raw_send((0,b.encodeResumed)(!1,0));this.sessions.delete(e.sessionId),e.sessionId=t,e.state=r.state,r.expiresAt=1/0,this.sessions.set(t,r);for(const t of r.rooms)this.join(e,t);const n=r.frames.filter(e=>e.sequence>s);for(const t of n)e.raw_send(t.data);e.raw_send((0,b.encodeResumed)(!0,n.length));for(const t of this.recoveredListeners)await t(e,n.length)}async broadcastInternal(e,t,s){let r;if("all"===t.type)r=this.connections;else if("tagged"===t.type){if(!this.tagsInv.has(t.tag))return;r=Array.from(this.tagsInv.get(t.tag))}else r=this.connections.filter(t.filter);if(await this.callMiddleware("onPacketBroadcast_pre",e,{recipients:r,...t},s))return;if(0===r.length)return;const[n,a,i]=await(0,u.processPacket)(this.serverPackets,e,s,this.serverwideSendQueue,-1);await this.callMiddleware("onPacketBroadcast_post",e,{recipients:r,...t},a,a.length)||r.forEach(e=>e.send_processed(n,a,i))}async broadcastTagged(e,t,...s){await this.broadcastInternal(t,{type:"tagged",tag:e},s)}async broadcastRoom(e,t,...s){await this.broadcastInternal(t,{type:"tagged",tag:e},s),await(this.adapter?.publish({origin:this.serverId,room:e,packetTag:t,values:s}))}async broadcastRoomExcept(e,t,s,...r){await this.broadcastInternal(s,{type:"filter",filter:s=>s!==e&&!0===this.tags.get(s)?.has(t)},r),await(this.adapter?.publish({origin:this.serverId,room:t,packetTag:s,values:r,exceptConnectionId:e.id}))}async broadcastFiltered(e,t,...s){await this.broadcastInternal(e,{type:"filter",filter:t},s)}async broadcast(e,...t){await this.broadcastInternal(e,{type:"all"},t)}async broadcastSafe(e,...t){try{return await this.broadcast(e,...t),!0}catch(t){return this.handleSendError(t,{packetTag:e,operation:"broadcast"}),!1}}broadcastVariant(e,t,...s){return this.broadcast(this.serverPackets.getVariantTag(e,t),...s)}handleSendError(e,t){this.sendErrorHandler?this.sendErrorHandler(e,t):console.error(`Failed to send packet "${t.packetTag}"`,e)}getConnected(){return this.connections}getSocket(e){return this.connectionMap[e]}closeSocket(e,t=1e3,s){this.getSocket(e).close(t,s)}tag(e,t,s=!0){this.tags.has(e)||this.tags.set(e,new Set),this.tagsInv.has(t)||this.tagsInv.set(t,new Set),s&&(this.tags.get(e).forEach(t=>{this.tagsInv.get(t)?.delete(e),Promise.resolve(this.adapter?.leave(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}),this.tags.get(e).clear()),this.tags.get(e).add(t),this.tagsInv.get(t).add(e),Promise.resolve(this.adapter?.join(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}join(e,t){if(!t)throw new Error("Room name cannot be empty");this.tag(e,t,!1)}leave(e,t){this.tags.get(e)?.delete(t);const s=this.tagsInv.get(t);s?.delete(e),0===s?.size&&this.tagsInv.delete(t),Promise.resolve(this.adapter?.leave(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}rooms(e){return this.tags.get(e)??new Set}debugServer=null;OpenDebug(e={}){if(null!=this.debugServer)throw new Error("A debug server is already running");this.debugServer=new f.DebugServer(this,e)}}exports.SonicWSServer=S;
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.BatchHelper=void 0;const t=require("../BufferUtil"),e=require("../../../native/wrapper");exports.BatchHelper=class{batchInfo={};batchTimeouts={};batchedData={};conn;registerSendPackets(t,e){this.conn=e,t.getTags().forEach(e=>{const a=t.getPacket(e);if(0==a.dataBatching)return;const c=t.getKey(e);this.initiateBatch(c,a.dataBatching,a.gzipCompression)})}initiateBatch(t,e,a){this.batchedData[t]=[],this.batchInfo[t]=[e,a]}startBatch(a){const[c,s]=this.batchInfo[a];this.batchTimeouts[a]=this.conn.setInterval(()=>{if(0==this.batchedData[a].length)return;const c=(0,e.encodeNativeBatch)(this.batchedData[a],s);this.conn.raw_send((0,t.toPacketBuffer)(a,c)),this.batchedData[a]=[],delete this.batchTimeouts[a]},c)}batchPacket(t,e){this.batchedData[t].push(e),this.batchTimeouts[t]||this.startBatch(t)}static async unravelBatch(t,a,c){let s;try{s=(0,e.decodeNativeBatch)(a,t.gzipCompression,t.maxBatchSize)}catch(t){return"Invalid batch: "+t}const i=[];for(const e of s){const a=await t.listen(e,c);if("string"==typeof a)return"Batched packet: "+a;i.push(a)}return i}};
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.BatchHelper=void 0;const t=require("../BufferUtil"),e=require("../../../native/wrapper");exports.BatchHelper=class{batchInfo={};batchTimeouts={};batchedData={};conn;registerSendPackets(t,e){this.conn=e,t.getTags().forEach(e=>{const a=t.getPacket(e);if(0===a.dataBatching)return;const c=t.getKey(e);this.initiateBatch(c,a.dataBatching,a.gzipCompression)})}initiateBatch(t,e,a){this.batchedData[t]=[],this.batchInfo[t]=[e,a]}startBatch(a){const[c,i]=this.batchInfo[a];this.batchTimeouts[a]=this.conn.setInterval(()=>{if(0===this.batchedData[a].length)return;const c=(0,e.encodeNativeBatch)(this.batchedData[a],i);this.conn.raw_send((0,t.toPacketBuffer)(a,c)),this.batchedData[a]=[],delete this.batchTimeouts[a]},c)}batchPacket(t,e){this.batchedData[t].push(e),this.batchTimeouts[t]||this.startBatch(t)}static async unravelBatch(t,a,c){let i;try{i=(0,e.decodeNativeBatch)(a,t.gzipCompression,t.maxBatchSize)}catch(t){return`Invalid batch: ${String(t)}`}const s=[];for(const e of i){const a=await t.listen(e,c);if("string"==typeof a)return`Batched packet: ${a}`;s.push(a)}return s}};
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketHolder=void 0;exports.PacketHolder=class{key;keys;tags;packetMap;packets;variants={};parents=new Set;constructor(t){this.key=1,this.keys={},this.tags={},this.packetMap={},t&&this.holdPackets(t)}createKey(t){this.keys[t]=this.key,this.tags[this.key]=t,this.key++}holdPackets(t){this.packets=t;for(const e of t)this.createKey(e.tag),this.packetMap[e.tag]=e,e.parent&&e.variant&&(this.variants[`${e.parent}.${e.variant}`]=e.tag,this.parents.add(e.parent)),e.isParent&&this.parents.add(e.tag)}getKey(t){if(!((t=this.resolveTag(t))in this.keys))throw new Error(`Not a valid tag: ${t}`);return this.keys[t]}getTag(t){if(t in this.tags)return this.tags[t]}getPacket(t){if(!((t=this.resolveTag(t))in this.packetMap))throw new Error("Unknown packet tag: "+t);return this.packetMap[t]}hasKey(t){return t in this.tags}hasTag(t){return this.resolveTag(t)in this.keys||this.parents.has(t)}resolveTag(t){return this.variants[t]??t}getVariantTag(t,e){const s=this.variants[`${t}.${e}`];if(!s)throw new Error(`Unknown packet variant: ${t}.${e}`);return s}getKeys(){return this.keys}getTagMap(){return this.tags}getTags(){return Object.values(this.tags)}getPackets(){return this.packets}serialize(){return this.packets.map(t=>t.serialize()).flat()}};
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketHolder=void 0;const t=require("./SchemaValidation");exports.PacketHolder=class{key;keys;tags;packetMap;packets;variants={};parents=new Set;constructor(t){this.key=1,this.keys={},this.tags={},this.packetMap={},t&&this.holdPackets(t)}createKey(t){this.keys[t]=this.key,this.tags[this.key]=t,this.key++}holdPackets(e){(0,t.AssertPacketSchema)(e),this.packets=e;for(const t of e)this.createKey(t.tag),this.packetMap[t.tag]=t,t.parent&&t.variant&&(this.variants[`${t.parent}.${t.variant}`]=t.tag,this.parents.add(t.parent)),t.isParent&&this.parents.add(t.tag)}getKey(t){if(!((t=this.resolveTag(t))in this.keys))throw new Error(`Not a valid tag: ${t}`);return this.keys[t]}getTag(t){if(t in this.tags)return this.tags[t]}getPacket(t){if(!((t=this.resolveTag(t))in this.packetMap))throw new Error("Unknown packet tag: "+t);return this.packetMap[t]}hasKey(t){return t in this.tags}hasTag(t){return this.resolveTag(t)in this.keys||this.parents.has(t)}resolveTag(t){return this.variants[t]??t}getVariantTag(t,e){const a=this.variants[`${t}.${e}`];if(!a)throw new Error(`Unknown packet variant: ${t}.${e}`);return a}getKeys(){return this.keys}getTagMap(){return this.tags}getTags(){return Object.values(this.tags)}getPackets(){return this.packets}serialize(){return this.packets.map(t=>t.serialize()).flat()}};
@@ -25,7 +25,10 @@ export type SharedPacketSettings = {
25
25
  * Each batched packet is counted towards the rate limit.
26
26
  */
27
27
  dataBatching?: number;
28
- /** If data batching is on, this will limit the amount of packets that can be batched into one (only effects the client). Defaults to 10. 0 for unlimited. */
28
+ /**
29
+ * Limits packets in one batch on the receiving client.
30
+ * Defaults to 10. Zero disables the limit.
31
+ */
29
32
  maxBatchSize?: number;
30
33
  /** The amount of times this packet can be sent every second, or 0 for infinite. */
31
34
  rateLimit?: number;
@@ -37,7 +40,10 @@ export type SharedPacketSettings = {
37
40
  enabled?: boolean;
38
41
  /** A validation function that is called whenever data is received. Return true for success, return false to kick socket. */
39
42
  validator?: ValidatorFunction;
40
- /** If this is true, other packets will be processed even if this one isn't finished; it'll still prevent it from calling twice before this finishes though. Defaults to false. */
43
+ /**
44
+ * Allows other packet types to run while this packet is processing.
45
+ * Repeated calls for this packet remain serialized.
46
+ */
41
47
  async?: boolean;
42
48
  /** If this is true, the packet will be Gzip compressed. Defaults to false on all types but JSON. */
43
49
  gzipCompression?: boolean;
@@ -52,7 +58,10 @@ export type SinglePacketSettings = SharedPacketSettings & {
52
58
  dataMax?: number;
53
59
  /** The minimum amount of values that can be sent through this packet; defaults to the max */
54
60
  dataMin?: number;
55
- /** If this is true, it will save the last received of this value, and if no data is sent, it'll re-use the previous value. This is not compatible with dataMin: 0. Defaults to false. */
61
+ /**
62
+ * Reuses the previous value when an empty payload arrives.
63
+ * This is incompatible with a zero data minimum.
64
+ */
56
65
  rereference?: boolean;
57
66
  /** Field names used to map positional values to and from an object. */
58
67
  schema?: readonly string[];
@@ -67,18 +76,21 @@ export type SinglePacketSettings = SharedPacketSettings & {
67
76
  min?: number;
68
77
  /** Inclusive application-level numeric maximum. */
69
78
  max?: number;
70
- /** Local class constructed from decoded schema fields. The class name is exchanged; peers must register their own matching class. */
79
+ /**
80
+ * Constructs decoded schema fields with a local class.
81
+ * Peers exchange the class name and register their own matching class.
82
+ */
71
83
  constructor?: Function;
72
84
  };
73
85
  /** Settings for multi-typed packets */
74
86
  export type MultiPacketSettings = SharedPacketSettings & {
75
87
  /** The data types of the packet */
76
88
  readonly types: readonly ArguableType[];
77
- /** The maximum amount of values that can be sent through each type of packet; defaults to 1 for each. Non-array will fill all for that amount */
89
+ /** Sets the maximum value count per field. A scalar applies to every field. */
78
90
  dataMaxes?: number[] | number;
79
- /** The minimum amount of values that can be sent through each type of packet; defaults to the max for each Non-array will fill all for that amount */
91
+ /** Sets the minimum value count per field. A scalar applies to every field. */
80
92
  dataMins?: number[] | number;
81
- /** Will automatically run FlattenData() and UnFlattenData() on values; this will optimize [[x,y,z],[x,y,z]...] for wire transfer */
93
+ /** Transposes repeated rows into columns before encoding. */
82
94
  autoFlatten?: boolean;
83
95
  /** Field names for records transposed into object-packet columns. */
84
96
  schema?: readonly string[];
@@ -108,7 +120,7 @@ export type EnumPacketSettings = SharedPacketSettings & {
108
120
  /**
109
121
  * Creates a structure for a simple single-typed packet.
110
122
  * This packet can be sent and received with the specified tag, type, and data cap.
111
- * @param settings The settings object containing `tag`, `type`, `dataMax`, `dataMin`, `noDataRange`, `dontSpread`, `validator`, `dataBatching`, and/or `maxBatchSize`.
123
+ * @param settings Packet type, limits, mapping, validation, and batching settings.
112
124
  * @returns The constructed packet structure data.
113
125
  * @throws {Error} If the `type` is invalid.
114
126
  */
@@ -119,7 +131,7 @@ export declare function CreatePacket<T extends ArguableType>(settings: SinglePac
119
131
  /**
120
132
  * Creates a structure for an object (multi-typed) packet.
121
133
  * This packet allows multiple types and their associated data caps.
122
- * @param settings The settings object containing `tag`, `types`, `dataMaxes`, `dataMins`, `noDataRange`, `dontSpread`, `autoFlatten`, `largePacket`, `validator`, `dataBatching`, and/or `maxBatchSize`.
134
+ * @param settings Field types, limits, transposition, validation, and batching settings.
123
135
  * @returns The constructed packet structure data.
124
136
  * @throws {Error} If any type in `types` is invalid.
125
137
  */
@@ -143,7 +155,7 @@ export declare function CreatePacketGroup(settings: PacketGroupSettings): Packet
143
155
  /**
144
156
  * Creates and defines an enum packet. This can be used to create an enum-based packet
145
157
  * with a specific tag and possible values.
146
- * @param settings The settings object containing `tag`, `enumTag`, `values`, `dataMax`, `dataMin`, `noDataRange`, `dontSpread`, `validator`, `dataBatching`, and/or `maxBatchSize`.
158
+ * @param settings Enum package, limits, validation, and batching settings.
147
159
  * @returns The constructed packet structure data.
148
160
  */
149
161
  export declare function CreateEnumPacket(settings: EnumPacketSettings): Packet<PacketType.ENUMS>;
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.processPacket=i,exports.listenPacket=async function(e,t,a){if("string"==typeof e)return a(e);const[r,n]=e;try{if(n&&Array.isArray(r))for(const e of t)await e(...r);else for(const e of t)await e(r)}catch(e){console.error(e),a(e)}},exports.CreatePacket=p,exports.CreateObjPacket=function(a){const r=Object.prototype.hasOwnProperty.call(a,"constructor")?a.constructor:void 0;let{tag:n,types:i=[],dataMaxes:p,dataMins:m,noDataRange:w=!1,dontSpread:d=!1,autoFlatten:y=!1,autoTranspose:g,schema:k,validator:P=null,dataBatching:E=0,maxBatchSize:v=10,rateLimit:S=0,enabled:T=!0,async:$=!1,gzipCompression:b=i&&i.includes(t.PacketType.JSON)}=a;if(!n)throw new Error("Tag not selected!");if(!i||0==i.length)throw new Error("Types is set to 0 length");if(a.replay&&E)throw new Error(`Packet "${n}" cannot combine replay with batching`);if(f(k,n),r&&!k)throw new Error(`Packet "${n}" constructor requires schema`);r&&(0,o.RegisterPacketConstructor)(r);if(k&&k.length!==i.length)throw new Error(`Packet "${n}" schema length must match types length`);if(void 0!==g&&y&&g!==y)throw new Error(`Packet "${n}" has conflicting autoFlatten and autoTranspose options`);const x=g??y;for(const e of i)if(c(e))throw new Error(`Invalid packet type in "${n}" packet: ${e}`);w?(p=Array.from({length:i.length}).map(()=>s),m=Array.from({length:i.length}).map(()=>0)):(null==p?p=Array.from({length:i.length}).map(()=>1):Array.isArray(p)||(p=Array.from({length:i.length}).map(()=>p)),null==m?m=Array.from({length:i.length}).map((e,t)=>p[t]):Array.isArray(m)||(m=Array.from({length:i.length}).map(()=>m)));const A=p.map(l),M=m.map((e,a)=>i[a]==t.PacketType.NONE?0:u(e,A[a])),O=new e.PacketSchema(!0,i,$,M,A,h(S),d,x,!1,E,v,b,k,void 0,void 0,void 0,void 0,r?.name,a.replay??!1);return new e.Packet(n,O,P,T,!1)},exports.CreatePacketGroup=function(e){if(!e.tag||e.tag.includes("$"))throw new Error("Packet group tag is required and cannot contain '$'");const a=Object.entries(e.variants);if(!a.length)throw new Error(`Packet group "${e.tag}" requires at least one variant`);const r=p({tag:e.tag,type:t.PacketType.NONE,dataMin:0,dataMax:0,_group:{parent:e.tag,variant:"",isParent:!0}}),n=a.map(([t,a])=>{if(!t||t.includes("$"))throw new Error("Packet variant names cannot be empty or contain '$'");return p({...a,tag:`${e.tag}.${t}`,_group:{parent:e.tag,variant:t,isParent:!1}})});return[r,...n]},exports.CreateEnumPacket=function(e){const{tag:t,enumData:a,dataMax:r=1,dataMin:n=0,noDataRange:o=!1,dontSpread:i=!1,validator:c=null,dataBatching:s=0,maxBatchSize:l=10,rateLimit:u=0,enabled:h=!0,async:f=!1}=e;return p({tag:t,type:a,dataMax:r,dataMin:n,noDataRange:o,dontSpread:i,validator:c,dataBatching:s,maxBatchSize:l,rateLimit:u,enabled:h,async:f})},exports.FlattenData=w,exports.UnFlattenData=function(e){return e[0]?.map((t,a)=>e.map(e=>e[a]))??[]};const e=require("../../packets/Packets"),t=require("../../packets/PacketType"),a=require("../enums/EnumType"),r=require("./CompressionUtil"),n=require("./HashUtil"),o=require("./ConstructorRegistry");async function i(e,a,o,c,s,l=!1){const u=e.getKey(a),h=e.getPacket(a);return async function(e,t,a,r,n,o,c){if(e[0]&&!o)return new Promise(t=>e[1].push([t,r,n]));e[0]=!0;const s=await c();if(e[1].length>0){const[r,n,o]=e[1].shift();queueMicrotask(async()=>{r(await i(t,n,o,e,a,!0))})}else e[0]=!1;return s}(c,e,s,a,o,l,async()=>{if(h.rereference){if(-1===s)throw new Error("Cannot send a re-referenced packet from the server-wide sender!");const e=(0,n.hashValue)(o);if(h.lastSent[s]===e)return[u,r.EMPTY_UINT8,h];h.lastSent[s]=e}if(o=h.prepareSend(o,s),h.autoFlatten&&h.object&&!h.fields)o=w(o[0]);else{if(o.length>h.maxSize)throw new Error(`Packet "${a}" only allows ${h.maxSize} values!`);if(o.length<h.minSize)throw new Error(`Packet "${a}" requires at least ${h.minSize} values!`)}if(h.object){if(o=o.map(e=>Array.isArray(e)?e:[e]),!h.autoFlatten){const e=h.dataMin,t=h.dataMax;for(let r=0;r<e.length;r++){if(o[r].length<e[r])throw new Error(`Section ${r+1} of packet "${a}" requires at least ${e[r]} values!`);if(o[r].length>t[r])throw new Error(`Section ${r+1} of packet "${a}" only allows ${t[r]} values!`)}}}else if(h.type!==t.PacketType.JSON){const e=o.find(e=>"object"==typeof e&&null!=e);e&&console.warn(`Passing an array will result in undefined behavior (${JSON.stringify(e)}). Spread the array with ...arr`)}const e=o.length>0?await h.processSend(o):r.EMPTY_UINT8;return[u,e,h]})}function c(e){return!("number"==typeof e&&e in t.PacketType||e instanceof a.EnumPackage)}const s=2048383;function l(e){return e<0?(console.warn("Having a data maximum below 0 does not do anything!"),0):e>s?(console.warn(`Only ${s} values can be sent on a type! Uhh make an issue if you want to send more.`),s):e}function u(e,t){return e<0?(console.warn("Having a data minimum below 0 does not do anything!"),0):e>t?(console.warn("Data minimum can not be higher than the data maximum!"),t):e}function h(e){if(!Number.isFinite(e)||e<0)throw new Error("Rate limit must be a non-negative finite number");return(e=Math.floor(e))>r.MAX_USHORT?(console.warn(`A rate limit above ${r.MAX_USHORT} is considered infinite.`),0):e}function p(a){const r=!0===a.autoFlatten&&void 0===a.dataMax&&void 0===a.dataMin,n=Object.prototype.hasOwnProperty.call(a,"constructor")?a.constructor:void 0;let{tag:i,type:p=t.PacketType.NONE,dataMax:w=1,dataMin:d,noDataRange:y=!1,dontSpread:g=!1,validator:k=null,dataBatching:P=0,maxBatchSize:E=10,rateLimit:v=0,enabled:S=!0,async:T=!1,gzipCompression:$=p==t.PacketType.JSON,rereference:b=!1,schema:x,autoFlatten:A=!1,quantized:M,min:O,max:q}=a;if(!i)throw new Error("Tag not selected!");if(y||r?(d=b?1:0,w=s):null==d&&(d=p==t.PacketType.NONE?0:w),b&&0==d)throw new Error("Rereference cannot be true if the dataMin is 0");if(a.replay&&P)throw new Error(`Packet "${i}" cannot combine replay with batching`);if(c(p))throw new Error(`Invalid packet type: ${p}`);if(f(x,i),n&&!x)throw new Error(`Packet "${i}" constructor requires schema`);if(n&&(0,o.RegisterPacketConstructor)(n),A&&(!x||0===x.length))throw new Error(`Packet "${i}" autoFlatten requires schema`);if(x&&!A&&d===w&&x.length!==w)throw new Error(`Packet "${i}" schema length must match its fixed value count (${w})`);!function(e,t,a,r,n){if(void 0!==a&&void 0!==r&&a>r)throw new Error(`Packet "${n}" min cannot exceed max`);if((t||void 0!==a||void 0!==r)&&!m.has(e))throw new Error(`Packet "${n}" numeric options require a numeric packet type`);if(t&&(!Number.isFinite(t.scale)||t.scale<=0))throw new Error(`Packet "${n}" quantization scale must be positive and finite`)}(p,M,O,q,i);const N=new e.PacketSchema(!1,p,T,u(d,w),l(w),h(v),g,A,b,P,E,$,x,M,O,q,a._group,n?.name,a.replay??!1);return new e.Packet(i,N,k,S,!1)}function f(e,t){if(e){if(!e.length||e.some(e=>"string"!=typeof e||!e))throw new Error(`Packet "${t}" schema must contain non-empty field names`);if(new Set(e).size!==e.length)throw new Error(`Packet "${t}" schema fields must be unique`)}}const m=new Set([t.PacketType.BYTES,t.PacketType.UBYTES,t.PacketType.SHORTS,t.PacketType.USHORTS,t.PacketType.VARINT,t.PacketType.UVARINT,t.PacketType.DELTAS,t.PacketType.FLOATS,t.PacketType.DOUBLES]);function w(e){if(null==e)return[];const t=e[0];if(null==t)return[];if(!Array.isArray(t))throw new Error(`Cannot flatten array: ${e}`);return t.map((t,a)=>e.map(e=>e[a]))??[]}
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.processPacket=i,exports.listenPacket=async function(e,t,a){if("string"==typeof e)return void a(e);const[r,n]=e;try{if(n&&Array.isArray(r))for(const e of t)await e(...r);else for(const e of t)await e(r)}catch(e){console.error(e),a(String(e))}},exports.CreatePacket=f,exports.CreateObjPacket=function(a){const r=Object.prototype.hasOwnProperty.call(a,"constructor")?a.constructor:void 0;let{tag:n,types:i=[],dataMaxes:f,dataMins:m,noDataRange:d=!1,dontSpread:w=!1,autoFlatten:y=!1,autoTranspose:g,schema:k,validator:P=null,dataBatching:v=0,maxBatchSize:E=10,rateLimit:S=0,enabled:$=!0,async:T=!1,gzipCompression:b=i&&i.includes(t.PacketType.JSON)}=a;if(!n)throw new Error("Packet tag is required");if(!i||0===i.length)throw new Error(`Packet "${n}" requires at least one type`);if(a.replay&&v)throw new Error(`Packet "${n}" cannot combine replay with batching`);if(h(k,n),r&&!k)throw new Error(`Packet "${n}" constructor requires schema`);r&&(0,o.RegisterPacketConstructor)(r);if(k&&k.length!==i.length)throw new Error(`Packet "${n}" schema length must match types length`);if(void 0!==g&&y&&g!==y)throw new Error(`Packet "${n}" has conflicting autoFlatten and autoTranspose options`);const x=g??y;for(const e of i)if(c(e))throw new Error(`Invalid packet type in "${n}" packet: ${e}`);if(d)f=Array.from({length:i.length},()=>s),m=Array.from({length:i.length},()=>0);else if(void 0===f?f=Array.from({length:i.length},()=>1):Array.isArray(f)||(f=Array.from({length:i.length},()=>f)),void 0===m){const e=f;m=Array.from({length:i.length},(t,a)=>e[a])}else Array.isArray(m)||(m=Array.from({length:i.length},()=>m));const A=m,q=f.map(l),M=A.map((e,a)=>i[a]===t.PacketType.NONE?0:u(e,q[a])),O=new e.PacketSchema(!0,i,T,M,q,p(S),w,x,!1,v,E,b,k,void 0,void 0,void 0,void 0,r?.name,a.replay??!1);return new e.Packet(n,O,P,$,!1)},exports.CreatePacketGroup=function(e){if(!e.tag||e.tag.includes("$"))throw new Error("Packet group tag is required and cannot contain '$'");const a=Object.entries(e.variants);if(!a.length)throw new Error(`Packet group "${e.tag}" requires at least one variant`);const r=f({tag:e.tag,type:t.PacketType.NONE,dataMin:0,dataMax:0,_group:{parent:e.tag,variant:"",isParent:!0}}),n=a.map(([t,a])=>{if(!t||t.includes("$"))throw new Error("Packet variant names cannot be empty or contain '$'");return f({...a,tag:`${e.tag}.${t}`,_group:{parent:e.tag,variant:t,isParent:!1}})});return[r,...n]},exports.CreateEnumPacket=function(e){const{tag:t,enumData:a,dataMax:r=1,dataMin:n=0,noDataRange:o=!1,dontSpread:i=!1,validator:c=null,dataBatching:s=0,maxBatchSize:l=10,rateLimit:u=0,enabled:p=!0,async:h=!1}=e;return f({tag:t,type:a,dataMax:r,dataMin:n,noDataRange:o,dontSpread:i,validator:c,dataBatching:s,maxBatchSize:l,rateLimit:u,enabled:p,async:h})},exports.FlattenData=d,exports.UnFlattenData=function(e){return e[0]?.map((t,a)=>e.map(e=>e[a]))??[]};const e=require("../../packets/Packets"),t=require("../../packets/PacketType"),a=require("../enums/EnumType"),r=require("./CompressionUtil"),n=require("./HashUtil"),o=require("./ConstructorRegistry");async function i(e,a,o,c,s,l=!1){const u=e.getKey(a),p=e.getPacket(a);return async function(e,t,a,r,n,o,c){if(e[0]&&!o)return new Promise(t=>e[1].push([t,r,n]));e[0]=!0;const s=await c();if(e[1].length>0){const[r,n,o]=e[1].shift();queueMicrotask(async()=>{r(await i(t,n,o,e,a,!0))})}else e[0]=!1;return s}(c,e,s,a,o,l,async()=>{if(p.rereference){if(-1===s)throw new Error("Cannot send a re-referenced packet from the server-wide sender");const e=(0,n.hashValue)(o);if(p.lastSent[s]===e)return[u,r.EMPTY_UINT8,p];p.lastSent[s]=e}if(o=p.prepareSend(o,s),p.autoFlatten&&p.object&&!p.fields)o=d(o[0]);else{if(o.length>p.maxSize)throw new Error(`Packet "${a}" only allows ${p.maxSize} values`);if(o.length<p.minSize)throw new Error(`Packet "${a}" requires at least ${p.minSize} values`)}if(p.object||p.type===t.PacketType.JSON){if(p.object&&(o=o.map(e=>Array.isArray(e)?e:[e]),!p.autoFlatten)){const e=p.dataMin,t=p.dataMax;for(let r=0;r<e.length;r++){if(o[r].length<e[r])throw new Error(`Section ${r+1} of packet "${a}" requires at least ${e[r]} values`);if(o[r].length>t[r])throw new Error(`Section ${r+1} of packet "${a}" only allows ${t[r]} values`)}}}else{const e=o.find(e=>"object"==typeof e&&null!=e);e&&console.warn(`Passing a nested value may produce undefined behavior (${JSON.stringify(e)}). Spread arrays into positional packet arguments`)}let e=r.EMPTY_UINT8;if(o.length>0){const t=p.processSend(o);e=t instanceof Promise?await t:t}return[u,e,p]})}function c(e){return!("number"==typeof e&&e in t.PacketType||e instanceof a.EnumPackage)}const s=2048383;function l(e){return e<0?(console.warn("A data maximum below zero is treated as zero"),0):e>s?(console.warn(`A packet type can contain at most ${s} values`),s):e}function u(e,t){return e<0?(console.warn("A data minimum below zero is treated as zero"),0):e>t?(console.warn("A data minimum above the data maximum is clamped to the maximum"),t):e}function p(e){if(!Number.isFinite(e)||e<0)throw new Error("Rate limit must be a non-negative finite number");return(e=Math.floor(e))>r.MAX_USHORT?(console.warn(`A rate limit above ${r.MAX_USHORT} is considered infinite.`),0):e}function f(a){const r=!0===a.autoFlatten&&void 0===a.dataMax&&void 0===a.dataMin,n=Object.prototype.hasOwnProperty.call(a,"constructor")?a.constructor:void 0;let{tag:i,type:f=t.PacketType.NONE,dataMax:d=1,dataMin:w,noDataRange:y=!1,dontSpread:g=!1,validator:k=null,dataBatching:P=0,maxBatchSize:v=10,rateLimit:E=0,enabled:S=!0,async:$=!1,gzipCompression:T=f===t.PacketType.JSON,rereference:b=!1,schema:x,autoFlatten:A=!1,quantized:q,min:M,max:O}=a;if(!i)throw new Error("Packet tag is required");if(y||r?(w=b?1:0,d=s):void 0===w&&(w=f===t.PacketType.NONE?0:d),b&&0===w)throw new Error("Rereference requires a data minimum above zero");if(a.replay&&P)throw new Error(`Packet "${i}" cannot combine replay with batching`);if(c(f))throw new Error(`Invalid packet type: ${f}`);if(h(x,i),n&&!x)throw new Error(`Packet "${i}" constructor requires schema`);if(n&&(0,o.RegisterPacketConstructor)(n),A&&(!x||0===x.length))throw new Error(`Packet "${i}" autoFlatten requires schema`);if(x&&!A&&w===d&&x.length!==d)throw new Error(`Packet "${i}" schema length must match its fixed value count (${d})`);!function(e,t,a,r,n){if(void 0!==a&&void 0!==r&&a>r)throw new Error(`Packet "${n}" min cannot exceed max`);if((t||void 0!==a||void 0!==r)&&!m.has(e))throw new Error(`Packet "${n}" numeric options require a numeric packet type`);if(t&&(!Number.isFinite(t.scale)||t.scale<=0))throw new Error(`Packet "${n}" quantization scale must be positive and finite`)}(f,q,M,O,i);const z=new e.PacketSchema(!1,f,$,u(w,d),l(d),p(E),g,A,b,P,v,T,x,q,M,O,a._group,n?.name,a.replay??!1);return new e.Packet(i,z,k,S,!1)}function h(e,t){if(e){if(!e.length||e.some(e=>"string"!=typeof e||!e))throw new Error(`Packet "${t}" schema must contain non-empty field names`);if(new Set(e).size!==e.length)throw new Error(`Packet "${t}" schema fields must be unique`)}}const m=new Set([t.PacketType.BYTES,t.PacketType.UBYTES,t.PacketType.SHORTS,t.PacketType.USHORTS,t.PacketType.VARINT,t.PacketType.UVARINT,t.PacketType.DELTAS,t.PacketType.FLOATS,t.PacketType.DOUBLES]);function d(e){if(null==e)return[];const t=e[0];if(null==t)return[];if(!Array.isArray(t))throw new Error(`Cannot flatten array: ${e}`);return t.map((t,a)=>e.map(e=>e[a]))}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Copyright (c) 2026 Lily (liwybloc)
3
+ * License-Identifier: LicenseRef-Lily-Personal-NonCommercial-2026
4
+ * See LICENSE for personal, non-commercial license terms.
5
+ */
6
+ import { Packet } from "../../packets/Packets";
7
+ export type SchemaValidationResult = {
8
+ errors: string[];
9
+ warnings: string[];
10
+ };
11
+ /** Performs whole-table checks that individual CreatePacket calls cannot see. */
12
+ export declare function ValidatePacketSchema(packets: readonly Packet<any>[], options?: {
13
+ direction?: "client" | "server";
14
+ warnUnbounded?: boolean;
15
+ }): SchemaValidationResult;
16
+ /** Throws when a packet table contains structural schema errors. */
17
+ export declare function AssertPacketSchema(packets: readonly Packet<any>[], options?: Parameters<typeof ValidatePacketSchema>[1]): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2026 Lily (liwybloc)
3
+ * License-Identifier: LicenseRef-Lily-Personal-NonCommercial-2026
4
+ * See LICENSE for personal, non-commercial license terms.
5
+ */
6
+
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.ValidatePacketSchema=n,exports.AssertPacketSchema=function(e,a){const t=n(e,a);if(t.errors.length)throw new Error(`Invalid SonicWS packet schema:\n- ${t.errors.join("\n- ")}`);for(const e of t.warnings)console.warn(`SonicWS schema warning: ${e}`)};const e=require("../../packets/PacketType"),a=new Set([e.PacketType.UBYTES,e.PacketType.USHORTS,e.PacketType.UVARINT]),t=new Set([e.PacketType.BYTES,e.PacketType.UBYTES,e.PacketType.SHORTS,e.PacketType.USHORTS,e.PacketType.VARINT,e.PacketType.UVARINT,e.PacketType.DELTAS,e.PacketType.FLOATS,e.PacketType.DOUBLES]);function n(n,c={}){const s=[],r=[],p=new Set,o=new Map,i=new Set;n.length>254&&s.push(`Packet table contains ${n.length} packets; the maximum is 254`);for(const e of n){p.has(e.tag)&&s.push(`Duplicate packet tag "${e.tag}"`),p.add(e.tag),e.isParent&&o.set(e.tag,e),e.replay&&e.dataBatching&&s.push(`Packet "${e.tag}" combines replay with batching`);const n=Array.isArray(e.type)?e.type:[e.type];e.quantized&&n.some(e=>!t.has(e))&&s.push(`Packet "${e.tag}" quantizes a non-numeric type`),void 0!==e.valueMin&&e.valueMin<0&&n.some(e=>a.has(e))&&s.push(`Packet "${e.tag}" has a negative minimum for an unsigned type`),!e.fields||e.autoFlatten||e.object||e.dataMin!==e.dataMax||e.fields.length===e.dataMax||s.push(`Packet "${e.tag}" schema length does not match its fixed value count`);const u=Array.isArray(e.dataMax)?e.dataMax.some(e=>e>=2048383):e.dataMax>=2048383;if(c.warnUnbounded&&"client"===c.direction&&u&&r.push(`Client packet "${e.tag}" has an effectively unbounded value count`),e.parent&&e.variant){const a=`${e.parent}.${e.variant}`;i.has(a)&&s.push(`Duplicate packet-group variant "${a}"`),i.add(a)}}for(const a of n){if(!a.parent)continue;const t=o.get(a.parent);t?t.type!==e.PacketType.NONE&&s.push(`Packet-group parent "${a.parent}" must use PacketType.NONE`):s.push(`Packet "${a.tag}" references missing group parent "${a.parent}"`)}return{errors:s,warnings:r}}
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "sonic-ws",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Ultra-lightweight, high-performance, and bandwidth efficient websocket library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "sonicws": "bin/sonicws.mjs"
9
+ },
7
10
  "files": [
8
11
  "dist/**/*",
12
+ "bin/sonicws.mjs",
9
13
  "bundled/bundle.js",
10
14
  "bundled/bundle.wasm",
11
15
  "README.md",
@@ -13,18 +17,20 @@
13
17
  ],
14
18
  "scripts": {
15
19
  "webpack": "webpack --mode production",
16
-
20
+
17
21
  "build_wasm_browser": "wasm-pack build --release --target bundler --out-dir ../ts/src/native/wasm/pkg --out-name sonic_ws_core ../core --features wasm --no-default-features",
18
22
  "build_wasm_node": "wasm-pack build --release --target nodejs --out-dir ../ts/src/native/wasm/node --out-name sonic_ws_core ../core --features wasm --no-default-features",
19
23
  "build_wasm": "npm run build_wasm_browser && npm run build_wasm_node",
20
24
  "build_web": "npm run build_wasm_browser && tsc --moduleResolution nodenext --module nodenext --outDir ./dist src/ws/client/browser/ClientBrowser.ts && cpy \"src/native/wasm/pkg/*\" dist/native/wasm/pkg --flat && rimraf ./dist/native/wasm/pkg/.gitignore && npm run webpack && node package-files.mjs",
21
25
  "build_node": "npm run build_wasm_node && rimraf ./dist && tsc -d && cpy \"src/native/wasm/node/*\" dist/native/wasm/node --flat && rimraf ./dist/native/wasm/node/.gitignore",
22
26
  "build": "npm run build_node && npm run build_web && rimraf ./dist/native/wasm/pkg ./dist/ws/client/browser && rimraf ./dist/ws/debug/DebugClient.d.ts && rimraf ./dist/ws/debug/DebugClient.js && node minify.mjs && node package-files.mjs",
23
-
27
+
24
28
  "test_web": "npm run build && node tests/test_web.mjs",
25
29
  "test_node": "npm run build_node && node tests/test_node.mjs",
30
+ "test_conformance": "npm run build_node && node tests/test_conformance.mjs",
31
+ "benchmark": "npm run build_node && node ../../benchmarks/run.mjs && node ../../benchmarks/transport.mjs",
26
32
  "prepack": "npm run build",
27
-
33
+
28
34
  "stage_package": "node package-files.mjs",
29
35
  "publish:assets": "rimraf ../../release/SonicWS_bundle.js ../../release/SonicWS_core.wasm && cp ../../bundled/bundle.js ../../release/bundle.js && cp ../../bundled/bundle.wasm ../../release/bundle.wasm"
30
36
  },