sonic-ws 2.3.0 → 2.4.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.
- package/README.md +17 -5
- package/bin/sonicws.mjs +2 -2
- package/bundled/bundle.js +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -4
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/ws/Connection.d.ts +2 -1
- package/dist/ws/Connection.js +1 -1
- package/dist/ws/client/core/ClientCore.d.ts +1 -0
- package/dist/ws/client/core/ClientCore.js +1 -1
- package/dist/ws/packets/Packets.d.ts +5 -0
- package/dist/ws/packets/Packets.js +1 -1
- package/dist/ws/server/SonicWSConnection.d.ts +1 -0
- package/dist/ws/server/SonicWSConnection.js +1 -1
- package/dist/ws/server/SonicWSServer.d.ts +5 -2
- package/dist/ws/server/SonicWSServer.js +1 -1
- package/dist/ws/util/JSONUtil.js +7 -0
- package/dist/ws/util/packets/ControlProtocol.d.ts +7 -1
- package/dist/ws/util/packets/ControlProtocol.js +1 -1
- package/dist/ws/util/packets/PacketHolder.js +1 -1
- package/dist/ws/util/packets/PacketUtils.d.ts +28 -2
- package/dist/ws/util/packets/PacketUtils.js +1 -1
- package/dist/ws/util/packets/VariantPermutation.d.ts +83 -0
- package/dist/ws/util/packets/VariantPermutation.js +7 -0
- package/dist/ws/util/packets/{PacketManifest.d.ts → metadata/PacketManifest.d.ts} +1 -1
- package/dist/ws/util/packets/{PacketManifest.js → metadata/PacketManifest.js} +1 -1
- package/dist/ws/util/packets/{SchemaValidation.d.ts → metadata/SchemaValidation.d.ts} +1 -1
- package/dist/ws/util/packets/metadata/SchemaValidation.js +7 -0
- package/package.json +1 -1
- package/dist/ws/util/packets/JSONUtil.js +0 -7
- package/dist/ws/util/packets/SchemaValidation.js +0 -7
- /package/dist/ws/util/{packets/JSONUtil.d.ts → JSONUtil.d.ts} +0 -0
- /package/dist/ws/util/packets/{ConstructorRegistry.d.ts → metadata/ConstructorRegistry.d.ts} +0 -0
- /package/dist/ws/util/packets/{ConstructorRegistry.js → metadata/ConstructorRegistry.js} +0 -0
package/dist/ws/Connection.js
CHANGED
|
@@ -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.CloseCodes=exports.Connection=void 0,exports.getClosureCause=function(e){if(e>=4e3)return i[e]??"UNKNOWN";switch(e){case 1e3:return"NORMAL_CLOSURE";case 1001:return"GOING_AWAY";case 1002:return"PROTOCOL_ERROR";case 1003:return"UNSUPPORTED_DATA";case 1006:return"ABNORMAL_CLOSURE";default:return"UNKNOWN"}};const e=require("./PacketProcessor"),t=require("./util/packets/BatchHelper");class s extends e.MiddlewareHolder{listeners;rawSendListeners=[];name;closed=!1;socket;_timers={};batcher;_on;_off;id;state={};volatileAtBytes=1048576;closeAtBytes=16777216;constructor(e,s,i,o,r){super(),this.id=s,this.listeners={},this.name=i,this.socket=e,this.batcher=new t.BatchHelper,this._on=o,this._off=r,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0;for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0)}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}replaceTransport(e,t,s){this.socket=e,this._on=t,this._off=s,this.closed=!1,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}setTimeout(e,t,s=!1){const i=setTimeout(()=>{e(),this.clearTimeout(i)},t);return this._timers[i]=[i,e,s],i}setInterval(e,t,s=!1){const i=setInterval(e,t);return this._timers[i]=[i,e,s],i}clearTimeout(e){clearTimeout(e),delete this._timers[e]}clearInterval(e){this.clearTimeout(e)}raw_send(e){if(this.getBufferedAmount()>=this.closeAtBytes)throw this.close(i.BACKPRESSURE,"Outbound buffer exceeded the configured limit"),new Error("SonicWS outbound backpressure limit exceeded");this.socket.send(e);for(const t of this.rawSendListeners)t(e)}getBufferedAmount(){const e=this.socket.bufferedAmount;return"number"==typeof e&&Number.isFinite(e)?e:0}setBackpressureLimits(e){const t=e.volatileAtBytes??this.volatileAtBytes,s=e.closeAtBytes??this.closeAtBytes;if(!Number.isFinite(t)||!Number.isFinite(s)||t<0||s<=0||t>s)throw new Error("Invalid SonicWS backpressure limits");this.volatileAtBytes=t,this.closeAtBytes=s}canSendVolatile(){return this.getBufferedAmount()<this.volatileAtBytes}raw_onmessage(e){this._on("message",e)}raw_onsend(e){this.rawSendListeners.push(e)}close(e=1e3,t){this.closed=!0,this.socket.close(e,t?.toString())}isClosed(){return this.closed||this.socket.readyState===WebSocket.CLOSED}async setName(e){await this.callMiddleware("onNameChange",e)||(this.name=e)}getName(){return this.name}}var i;exports.Connection=s,function(e){e[e.RATELIMIT=4e3]="RATELIMIT",e[e.SMALL=4001]="SMALL",e[e.INVALID_KEY=4002]="INVALID_KEY",e[e.INVALID_PACKET=4003]="INVALID_PACKET",e[e.INVALID_DATA=4004]="INVALID_DATA",e[e.REPEATED_HANDSHAKE=4005]="REPEATED_HANDSHAKE",e[e.DISABLED_PACKET=4006]="DISABLED_PACKET",e[e.MIDDLEWARE=4007]="MIDDLEWARE",e[e.MANUAL_SHUTDOWN=4008]="MANUAL_SHUTDOWN",e[e.BACKPRESSURE=4009]="BACKPRESSURE"}(i||(exports.CloseCodes=i={}));
|
|
7
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.CloseCodes=exports.Connection=void 0,exports.getClosureCause=function(e){if(e>=4e3)return i[e]??"UNKNOWN";switch(e){case 1e3:return"NORMAL_CLOSURE";case 1001:return"GOING_AWAY";case 1002:return"PROTOCOL_ERROR";case 1003:return"UNSUPPORTED_DATA";case 1006:return"ABNORMAL_CLOSURE";default:return"UNKNOWN"}};const e=require("./PacketProcessor"),t=require("./util/packets/BatchHelper");class s extends e.MiddlewareHolder{listeners;rawSendListeners=[];name;closed=!1;socket;_timers={};batcher;_on;_off;id;state={};volatileAtBytes=1048576;closeAtBytes=16777216;constructor(e,s,i,o,r){super(),this.id=s,this.listeners={},this.name=i,this.socket=e,this.batcher=new t.BatchHelper,this._on=o,this._off=r,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0;for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0)}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}replaceTransport(e,t,s){this.socket=e,this._on=t,this._off=s,this.closed=!1,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}setTimeout(e,t,s=!1){const i=setTimeout(()=>{e(),this.clearTimeout(i)},t);return this._timers[i]=[i,e,s],i}setInterval(e,t,s=!1){const i=setInterval(e,t);return this._timers[i]=[i,e,s],i}clearTimeout(e){clearTimeout(e),delete this._timers[e]}clearInterval(e){this.clearTimeout(e)}raw_send(e){if(this.getBufferedAmount()>=this.closeAtBytes)throw this.close(i.BACKPRESSURE,"Outbound buffer exceeded the configured limit"),new Error("SonicWS outbound backpressure limit exceeded");this.socket.send(e);for(const t of this.rawSendListeners)t(e)}getBufferedAmount(){const e=this.socket.bufferedAmount;return"number"==typeof e&&Number.isFinite(e)?e:0}setBackpressureLimits(e){const t=e.volatileAtBytes??this.volatileAtBytes,s=e.closeAtBytes??this.closeAtBytes;if(!Number.isFinite(t)||!Number.isFinite(s)||t<0||s<=0||t>s)throw new Error("Invalid SonicWS backpressure limits");this.volatileAtBytes=t,this.closeAtBytes=s}canSendVolatile(){return this.getBufferedAmount()<this.volatileAtBytes}raw_onmessage(e){this._on("message",e)}raw_onsend(e){this.rawSendListeners.push(e)}close(e=1e3,t){this.closed=!0,this.socket.close(e,t?.toString())}isClosed(){return this.closed||this.socket.readyState===WebSocket.CLOSED}async setName(e){await this.callMiddleware("onNameChange",e)||(this.name=e)}getName(){return this.name}}var i;exports.Connection=s,function(e){e[e.RATELIMIT=4e3]="RATELIMIT",e[e.SMALL=4001]="SMALL",e[e.INVALID_KEY=4002]="INVALID_KEY",e[e.INVALID_PACKET=4003]="INVALID_PACKET",e[e.INVALID_DATA=4004]="INVALID_DATA",e[e.REPEATED_HANDSHAKE=4005]="REPEATED_HANDSHAKE",e[e.DISABLED_PACKET=4006]="DISABLED_PACKET",e[e.MIDDLEWARE=4007]="MIDDLEWARE",e[e.MANUAL_SHUTDOWN=4008]="MANUAL_SHUTDOWN",e[e.BACKPRESSURE=4009]="BACKPRESSURE",e[e.HEARTBEAT_TIMEOUT=4010]="HEARTBEAT_TIMEOUT"}(i||(exports.CloseCodes=i={}));
|
|
@@ -81,6 +81,7 @@ export declare abstract class SonicWSCore<T extends ClientTransport, K> extends
|
|
|
81
81
|
*/
|
|
82
82
|
send(tag: string, ...values: any[]): Promise<void>;
|
|
83
83
|
sendVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
|
|
84
|
+
sendPermutation(parent: string, selection: readonly boolean[] | Record<string, boolean>, ...values: any[]): Promise<void>;
|
|
84
85
|
/** Sends a validated packet as an RPC request and waits for its response. */
|
|
85
86
|
request(tag: string, ...valuesAndOptions: any[]): Promise<any>;
|
|
86
87
|
/** Registers the client-side responder for server requests using this packet tag. */
|
|
@@ -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.SonicWSCore=void 0;const e=require("../../util/packets/PacketHolder"),t=require("../../util/packets/CompressionUtil"),s=require("../../../native/wrapper"),n=require("../../util/packets/PacketUtils"),i=require("../../../version"),r=require("../../packets/Packets"),a=require("../../util/packets/BatchHelper"),o=require("../../util/BufferUtil"),c=require("../../Connection"),h=require("../../util/StringUtil"),l=require("../../util/packets/ControlProtocol");class d extends c.Connection{preListen;clientPackets=new e.PacketHolder;serverPackets=new e.PacketHolder;pastKeys=!1;readyListeners=[];bufferHandler;_timers={};asyncData={};asyncMap={};reconnectFactory;reconnectOptions={enabled:!1,attempts:1/0,minDelayMs:500,maxDelayMs:1e4,jitter:.25};reconnectAttempt=0;reconnectTimer;intentionalClose=!1;connectedOnce=!1;reconnectingListeners=[];reconnectListeners=[];reconnectFailedListeners=[];nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;lastReplaySequence=0;recoveredListeners=[];pendingResumeSession;handshakeTimeoutMs=1e4;handshakeTimer;constructor(e,t,s,n){super(e,-1,"LocalSocket",s,n),this.socket=e,this.preListen={},this.invalidPacket=this.invalidPacket.bind(this),this.serverKeyHandler=this.serverKeyHandler.bind(this),this.messageHandler=this.messageHandler.bind(this),this.attachClientTransport(),this.armHandshakeTimeout(),this.bufferHandler=t}configureHandshakeTimeout(e){if(!Number.isFinite(e)||e<=0)throw new Error("Handshake timeout must be positive");this.handshakeTimeoutMs=e,this.armHandshakeTimeout()}armHandshakeTimeout(){this.handshakeTimer&&clearTimeout(this.handshakeTimer),this.handshakeTimer=setTimeout(()=>{this.pastKeys||this.socket.close(c.CloseCodes.INVALID_DATA,"SonicWS schema handshake timed out")},this.handshakeTimeoutMs)}configureReconnect(e,t={}){if(this.reconnectFactory=e,this.reconnectOptions={enabled:t.enabled??!0,attempts:t.attempts??1/0,minDelayMs:t.minDelayMs??500,maxDelayMs:t.maxDelayMs??1e4,jitter:t.jitter??.25},this.reconnectOptions.attempts<0||this.reconnectOptions.minDelayMs<0||this.reconnectOptions.maxDelayMs<this.reconnectOptions.minDelayMs)throw new Error("Invalid reconnect timing options");if(this.reconnectOptions.jitter<0||this.reconnectOptions.jitter>1)throw new Error("Reconnect jitter must be between 0 and 1")}attachClientTransport(){this._on("message",this.serverKeyHandler),this._on("close",(...e)=>{for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0);for(const e of this.clientPackets.getPackets())delete e.lastSent[0],e.clearQuantizationState(0);for(const e of this.serverPackets.getPackets())delete e.lastReceived[0];const t=e[0],s="number"==typeof t?t:t?.code;this.intentionalClose||1e3===s||this.scheduleReconnect()})}scheduleReconnect(){if(!this.reconnectFactory||!this.reconnectOptions.enabled||this.reconnectTimer)return;if(this.reconnectAttempt>=this.reconnectOptions.attempts)return void this.reconnectFailedListeners.forEach(e=>e());const e=++this.reconnectAttempt,t=Math.min(this.reconnectOptions.maxDelayMs,this.reconnectOptions.minDelayMs*2**(e-1)),s=t*this.reconnectOptions.jitter,n=Math.max(0,Math.round(t-s+Math.random()*s*2));this.reconnectingListeners.forEach(t=>t({attempt:e,delayMs:n})),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0,this.beginReconnect()},n)}beginReconnect(){try{const t=this.reconnectFactory();this.clientPackets=new e.PacketHolder,this.serverPackets=new e.PacketHolder,this.asyncData={},this.asyncMap={},this.reading=!1,this.readQueue=[],this.pastKeys=!1,this.preListen={},this.bufferHandler=t.bufferHandler,this.batcher=new a.BatchHelper,this.replaceTransport(t.socket,t.on,t.off),this.attachClientTransport(),this.armHandshakeTimeout()}catch{this.scheduleReconnect()}}reading=!1;readQueue=[];async serverKeyHandler(e){if(this.reading)return void this.readQueue.push(e);this.reading=!0;const n=await this.bufferHandler(e);if(n.length<3||(0,h.as8String)(n.slice(0,3))!==i.SERVER_SUFFIX)throw this.close(1e3),new Error("The requested server does not use the SonicWS protocol");const a=n[3];if(a!==i.VERSION)throw this.close(1e3),new Error(`Protocol mismatch: ${a>i.VERSION?"client":"server"} is outdated (server: ${a}, client: ${i.VERSION})`);const o=(0,s.inflateNative)(n.subarray(4)),c=this.sessionId,[d,u]=(0,t.readVarInt)(o,0);this.id=u;const[p,k]=(0,t.readVarInt)(o,d);this.sessionId=(new TextDecoder).decode(o.slice(p,p+k));const[m,v]=(0,t.readVarInt)(o,p+k),y=o.subarray(m,m+v);this.clientPackets.holdPackets(r.Packet.deserializeAll(y,!0));const f=o.subarray(m+v);this.serverPackets.holdPackets(r.Packet.deserializeAll(f,!0)),this.batcher.registerSendPackets(this.clientPackets,this);for(const e of this.serverPackets.getPackets()){const t=this.serverPackets.getKey(e.tag);this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}for(const[e,t]of Object.entries(this.preListen??{}))if(this.serverPackets.hasTag(e))for(const s of t)this.listen(e,s);else console.error(new Error(`The server does not define packet tag "${e}"`));this.preListen=null,this.pastKeys=!0,this.handshakeTimer&&clearTimeout(this.handshakeTimer),this.connectedOnce&&(this.reconnectAttempt=0,this.reconnectListeners.forEach(e=>e())),this.connectedOnce=!0,this.readyListeners?.forEach(e=>e()),this.readyListeners=null,this._off("message",this.serverKeyHandler),this._on("message",this.messageHandler),c&&c!==this.sessionId&&(this.pendingResumeSession=c,this.raw_send((0,l.encodeResume)(c,this.lastReplaySequence))),this.readQueue.forEach(e=>{this.messageHandler(e)}),this.readQueue=[]}invalidPacket(e){throw console.error(e),new Error("SonicWS rejected data from the server. Report reproducible codec failures at https://github.com/liwybloc/sonic-ws")}listenLock=!1;packetQueue=[];async deliverPacket(e,t,s,i,r){if(this.closed)return void this.releasePacketLock(s,i,r);const a=this.listeners[t],o=this.serverPackets.getPacket(t),c=o.parent?this.listeners[o.parent]:void 0;if(!a&&!c)return console.warn(`No listener is registered for packet "${t}"`),void this.releasePacketLock(s,i,r);if(a&&await(0,n.listenPacket)(e,a,this.invalidPacket),"string"!=typeof e&&o.parent&&o.variant)for(const t of c??[])await t({variant:o.variant,payload:e[0]});await this.callMiddleware("onReceive_post",t,"string"==typeof e?[e]:e[0]),this.releasePacketLock(s,i,r)}releasePacketLock(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.dataHandler(e.shift())}isAsync(e){return this.asyncMap[e]}async dataHandler(e){if(0===e[0])return void await this.handleControl(e);const t=e[0],s=e.slice(1),n=this.isAsync(t);let i,r,o;if(n?(o=this.asyncData[t],i=o[0],r=o[1]):(i=this.listenLock,r=this.packetQueue),i)return void r.push(e);n?o[0]=!0:this.listenLock=!0;const c=this.serverPackets.getTag(t),h=this.serverPackets.getPacket(c);if(await this.callMiddleware("onReceive_pre",h.tag,e,e.length))return void this.releasePacketLock(r,n,o);if(h.rereference&&0===s.length)return void 0===h.lastReceived[0]&&this.invalidPacket("No previous value to rereference"),void this.deliverPacket(h.lastReceived[0],c,r,n,o);if(0===h.dataBatching){const e=h.lastReceived[0]=await h.listen(s,null);return void this.deliverPacket(e,c,r,n,o)}const l=await a.BatchHelper.unravelBatch(h,s,null);"string"==typeof l&&this.invalidPacket(l),l.forEach(e=>{this.deliverPacket(e,c,r,n,o)})}async handleControl(e){let t;try{t=(0,l.decodeControl)(e)}catch{return void this.close(c.CloseCodes.INVALID_DATA,"Malformed SonicWS control frame")}switch(t.type){case l.ControlType.REPLAY:if(t.sequence<=this.lastReplaySequence)return;return this.lastReplaySequence=t.sequence,void await this.dataHandler(t.payload);case l.ControlType.RESUMED:return t.recovered&&this.pendingResumeSession&&(this.sessionId=this.pendingResumeSession),this.pendingResumeSession=void 0,this.recoveredListeners.forEach(e=>e({recovered:t.recovered,replayed:t.replayed})),void(t.recovered||(this.lastReplaySequence=0));case l.ControlType.RESUME:throw new Error("A client cannot receive a recovery request");case l.ControlType.RESPONSE:{const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}default:try{const e=this.serverPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const n=await this.serverPackets.getPacket(e).listen(t.payload,null);if("string"==typeof n)throw new Error(n);const[i,r]=n,a=r?await s(...i):await s(i);this.raw_send((0,l.encodeControlResponse)(t.id,!0,a??null))}catch(e){this.raw_send((0,l.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}}async messageHandler(e){const t=await this.bufferHandler(e);t.length<1||await this.dataHandler(t)}listen(e,t){if(!this.serverPackets.hasTag(e))return void console.warn(`Packet tag "${e}" is not available on the server`);const s=this.serverPackets.resolveTag(e);(this.listeners[s]??=[]).push(t)}sendQueue=[!1,[],void 0];async send(e,...t){if(await this.callMiddleware("onSend_pre",e,t,Date.now(),performance.now()))return;const[s,i,r]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0);0===r.dataBatching?this.raw_send((0,o.toPacketBuffer)(s,i)):this.batcher.batchPacket(s,i),await this.callMiddleware("onSend_post",e,i,i.length)}sendVariant(e,t,...s){return this.send(this.clientPackets.getVariantTag(e,t),...s)}async request(e,...t){const s=t.at(-1),i=t.length>1&&s&&"object"==typeof s&&!Array.isArray(s)&&Object.keys(s).every(e=>"timeoutMs"===e)?t.pop():{},[r,a]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0),o=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((t,s)=>{const n=setTimeout(()=>{this.pendingRequests.delete(o),s(new Error(`RPC request "${e}" timed out`))},i.timeoutMs??5e3);this.pendingRequests.set(o,{resolve:t,reject:s,timer:n}),this.raw_send((0,l.encodeControlRequest)(o,r,a))})}respond(e,t){const s=this.serverPackets.resolveTag(e);this.responders.set(s,t)}async sendSafe(e,...t){try{return await this.send(e,...t),!0}catch(t){return console.error(`Failed to send packet "${e}"`,t),!1}}async sendVolatile(e,...t){return!!this.canSendVolatile()&&(await this.send(e,...t),!0)}sendReliable(e,...t){return this.send(e,...t)}on_ready(e){this.pastKeys?e():(this.readyListeners??=[]).push(e)}on_close(e){this._on("close",e)}on_reconnecting(e){this.reconnectingListeners.push(e)}on_reconnect(e){this.reconnectListeners.push(e)}on_reconnect_failed(e){this.reconnectFailedListeners.push(e)}on_recovered(e){this.recoveredListeners.push(e)}close(e=1e3,t){this.intentionalClose=!0,this.reconnectTimer&&clearTimeout(this.reconnectTimer),super.close(e,t)}on(e,t){if(this.socket.readyState!==WebSocket.OPEN){const s=this.preListen??={};return s[e]??=[],void s[e].push(t)}this.listen(e,t)}}exports.SonicWSCore=d;
|
|
7
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSCore=void 0;const e=require("../../util/packets/PacketHolder"),t=require("../../util/packets/CompressionUtil"),s=require("../../../native/wrapper"),n=require("../../util/packets/PacketUtils"),i=require("../../../version"),r=require("../../packets/Packets"),a=require("../../util/packets/BatchHelper"),o=require("../../util/BufferUtil"),c=require("../../Connection"),h=require("../../util/StringUtil"),l=require("../../util/packets/ControlProtocol");class d extends c.Connection{preListen;clientPackets=new e.PacketHolder;serverPackets=new e.PacketHolder;pastKeys=!1;readyListeners=[];bufferHandler;_timers={};asyncData={};asyncMap={};reconnectFactory;reconnectOptions={enabled:!1,attempts:1/0,minDelayMs:500,maxDelayMs:1e4,jitter:.25};reconnectAttempt=0;reconnectTimer;intentionalClose=!1;connectedOnce=!1;reconnectingListeners=[];reconnectListeners=[];reconnectFailedListeners=[];nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;lastReplaySequence=0;recoveredListeners=[];pendingResumeSession;handshakeTimeoutMs=1e4;handshakeTimer;constructor(e,t,s,n){super(e,-1,"LocalSocket",s,n),this.socket=e,this.preListen={},this.invalidPacket=this.invalidPacket.bind(this),this.serverKeyHandler=this.serverKeyHandler.bind(this),this.messageHandler=this.messageHandler.bind(this),this.attachClientTransport(),this.armHandshakeTimeout(),this.bufferHandler=t}configureHandshakeTimeout(e){if(!Number.isFinite(e)||e<=0)throw new Error("Handshake timeout must be positive");this.handshakeTimeoutMs=e,this.armHandshakeTimeout()}armHandshakeTimeout(){this.handshakeTimer&&clearTimeout(this.handshakeTimer),this.handshakeTimer=setTimeout(()=>{this.pastKeys||this.socket.close(c.CloseCodes.INVALID_DATA,"SonicWS schema handshake timed out")},this.handshakeTimeoutMs)}configureReconnect(e,t={}){if(this.reconnectFactory=e,this.reconnectOptions={enabled:t.enabled??!0,attempts:t.attempts??1/0,minDelayMs:t.minDelayMs??500,maxDelayMs:t.maxDelayMs??1e4,jitter:t.jitter??.25},this.reconnectOptions.attempts<0||this.reconnectOptions.minDelayMs<0||this.reconnectOptions.maxDelayMs<this.reconnectOptions.minDelayMs)throw new Error("Invalid reconnect timing options");if(this.reconnectOptions.jitter<0||this.reconnectOptions.jitter>1)throw new Error("Reconnect jitter must be between 0 and 1")}attachClientTransport(){this._on("message",this.serverKeyHandler),this._on("close",(...e)=>{for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0);for(const e of this.clientPackets.getPackets())delete e.lastSent[0],e.clearQuantizationState(0);for(const e of this.serverPackets.getPackets())delete e.lastReceived[0];const t=e[0],s="number"==typeof t?t:t?.code;this.intentionalClose||1e3===s||this.scheduleReconnect()})}scheduleReconnect(){if(!this.reconnectFactory||!this.reconnectOptions.enabled||this.reconnectTimer)return;if(this.reconnectAttempt>=this.reconnectOptions.attempts)return void this.reconnectFailedListeners.forEach(e=>e());const e=++this.reconnectAttempt,t=Math.min(this.reconnectOptions.maxDelayMs,this.reconnectOptions.minDelayMs*2**(e-1)),s=t*this.reconnectOptions.jitter,n=Math.max(0,Math.round(t-s+Math.random()*s*2));this.reconnectingListeners.forEach(t=>t({attempt:e,delayMs:n})),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0,this.beginReconnect()},n)}beginReconnect(){try{const t=this.reconnectFactory();this.clientPackets=new e.PacketHolder,this.serverPackets=new e.PacketHolder,this.asyncData={},this.asyncMap={},this.reading=!1,this.readQueue=[],this.pastKeys=!1,this.preListen={},this.bufferHandler=t.bufferHandler,this.batcher=new a.BatchHelper,this.replaceTransport(t.socket,t.on,t.off),this.attachClientTransport(),this.armHandshakeTimeout()}catch{this.scheduleReconnect()}}reading=!1;readQueue=[];async serverKeyHandler(e){if(this.reading)return void this.readQueue.push(e);this.reading=!0;const n=await this.bufferHandler(e);if(1===n.length&&0===n[0])return this.raw_send(Uint8Array.of(0)),void(this.reading=!1);if(n.length<3||(0,h.as8String)(n.slice(0,3))!==i.SERVER_SUFFIX)throw this.close(1e3),new Error("The requested server does not use the SonicWS protocol");const a=n[3];if(a!==i.VERSION)throw this.close(1e3),new Error(`Protocol mismatch: ${a>i.VERSION?"client":"server"} is outdated (server: ${a}, client: ${i.VERSION})`);const o=(0,s.inflateNative)(n.subarray(4)),c=this.sessionId,[d,u]=(0,t.readVarInt)(o,0);this.id=u;const[p,k]=(0,t.readVarInt)(o,d);this.sessionId=(new TextDecoder).decode(o.slice(p,p+k));const[m,v]=(0,t.readVarInt)(o,p+k),f=o.subarray(m,m+v);this.clientPackets.holdPackets(r.Packet.deserializeAll(f,!0));const y=o.subarray(m+v);this.serverPackets.holdPackets(r.Packet.deserializeAll(y,!0)),this.batcher.registerSendPackets(this.clientPackets,this);for(const e of this.serverPackets.getPackets()){const t=this.serverPackets.getKey(e.tag);this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}for(const[e,t]of Object.entries(this.preListen??{}))if(this.serverPackets.hasTag(e))for(const s of t)this.listen(e,s);else console.error(new Error(`The server does not define packet tag "${e}"`));this.preListen=null,this.pastKeys=!0,this.handshakeTimer&&clearTimeout(this.handshakeTimer),this.connectedOnce&&(this.reconnectAttempt=0,this.reconnectListeners.forEach(e=>e())),this.connectedOnce=!0,this.readyListeners?.forEach(e=>e()),this.readyListeners=null,this._off("message",this.serverKeyHandler),this._on("message",this.messageHandler),c&&c!==this.sessionId&&(this.pendingResumeSession=c,this.raw_send((0,l.encodeResume)(c,this.lastReplaySequence))),this.readQueue.forEach(e=>{this.messageHandler(e)}),this.readQueue=[]}invalidPacket(e){throw console.error(e),new Error("SonicWS rejected data from the server. Report reproducible codec failures at https://github.com/liwybloc/sonic-ws")}listenLock=!1;packetQueue=[];async deliverPacket(e,t,s,i,r){if(this.closed)return void this.releasePacketLock(s,i,r);const a=this.listeners[t],o=this.serverPackets.getPacket(t),c=o.parent?this.listeners[o.parent]:void 0;if(!a&&!c)return console.warn(`No listener is registered for packet "${t}"`),void this.releasePacketLock(s,i,r);if(a&&await(0,n.listenPacket)(e,a,this.invalidPacket),"string"!=typeof e&&o.parent&&o.variant)for(const t of c??[]){const s=o.permutation();await t({variant:o.variant,payload:e[0],...s&&{permutation:s}})}await this.callMiddleware("onReceive_post",t,"string"==typeof e?[e]:e[0]),this.releasePacketLock(s,i,r)}releasePacketLock(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.dataHandler(e.shift())}isAsync(e){return this.asyncMap[e]}async dataHandler(e){if(0===e[0])return void await this.handleControl(e);const t=e[0],s=e.slice(1),n=this.isAsync(t);let i,r,o;if(n?(o=this.asyncData[t],i=o[0],r=o[1]):(i=this.listenLock,r=this.packetQueue),i)return void r.push(e);n?o[0]=!0:this.listenLock=!0;const c=this.serverPackets.getTag(t),h=this.serverPackets.getPacket(c);if(await this.callMiddleware("onReceive_pre",h.tag,e,e.length))return void this.releasePacketLock(r,n,o);if(h.rereference&&0===s.length)return void 0===h.lastReceived[0]&&this.invalidPacket("No previous value to rereference"),void this.deliverPacket(h.lastReceived[0],c,r,n,o);if(0===h.dataBatching){const e=h.lastReceived[0]=await h.listen(s,null);return void this.deliverPacket(e,c,r,n,o)}const l=await a.BatchHelper.unravelBatch(h,s,null);"string"==typeof l&&this.invalidPacket(l),l.forEach(e=>{this.deliverPacket(e,c,r,n,o)})}async handleControl(e){if(1===e.length)return void this.raw_send(Uint8Array.of(0));let t;try{t=(0,l.decodeControl)(e)}catch{return void this.close(c.CloseCodes.INVALID_DATA,"Malformed SonicWS control frame")}switch(t.type){case l.ControlType.HEARTBEAT:return void this.raw_send(Uint8Array.of(0));case l.ControlType.REPLAY:if(t.sequence<=this.lastReplaySequence)return;return this.lastReplaySequence=t.sequence,void await this.dataHandler(t.payload);case l.ControlType.RESUMED:return t.recovered&&this.pendingResumeSession&&(this.sessionId=this.pendingResumeSession),this.pendingResumeSession=void 0,this.recoveredListeners.forEach(e=>e({recovered:t.recovered,replayed:t.replayed})),void(t.recovered||(this.lastReplaySequence=0));case l.ControlType.RESUME:throw new Error("A client cannot receive a recovery request");case l.ControlType.RESPONSE:{const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}default:try{const e=this.serverPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const n=await this.serverPackets.getPacket(e).listen(t.payload,null);if("string"==typeof n)throw new Error(n);const[i,r]=n,a=r?await s(...i):await s(i);this.raw_send((0,l.encodeControlResponse)(t.id,!0,a??null))}catch(e){this.raw_send((0,l.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}}async messageHandler(e){const t=await this.bufferHandler(e);t.length<1||await this.dataHandler(t)}listen(e,t){if(!this.serverPackets.hasTag(e))return void console.warn(`Packet tag "${e}" is not available on the server`);const s=this.serverPackets.resolveTag(e);(this.listeners[s]??=[]).push(t)}sendQueue=[!1,[],void 0];async send(e,...t){if(await this.callMiddleware("onSend_pre",e,t,Date.now(),performance.now()))return;const[s,i,r]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0);0===r.dataBatching?this.raw_send((0,o.toPacketBuffer)(s,i)):this.batcher.batchPacket(s,i),await this.callMiddleware("onSend_post",e,i,i.length)}sendVariant(e,t,...s){return this.send(this.clientPackets.getVariantTag(e,t),...s)}sendPermutation(e,t,...s){return this.send(this.clientPackets.getPermutationVariant(e,t),...s)}async request(e,...t){const s=t.at(-1),i=t.length>1&&s&&"object"==typeof s&&!Array.isArray(s)&&Object.keys(s).every(e=>"timeoutMs"===e)?t.pop():{},[r,a]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0),o=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((t,s)=>{const n=setTimeout(()=>{this.pendingRequests.delete(o),s(new Error(`RPC request "${e}" timed out`))},i.timeoutMs??5e3);this.pendingRequests.set(o,{resolve:t,reject:s,timer:n}),this.raw_send((0,l.encodeControlRequest)(o,r,a))})}respond(e,t){const s=this.serverPackets.resolveTag(e);this.responders.set(s,t)}async sendSafe(e,...t){try{return await this.send(e,...t),!0}catch(t){return console.error(`Failed to send packet "${e}"`,t),!1}}async sendVolatile(e,...t){return!!this.canSendVolatile()&&(await this.send(e,...t),!0)}sendReliable(e,...t){return this.send(e,...t)}on_ready(e){this.pastKeys?e():(this.readyListeners??=[]).push(e)}on_close(e){this._on("close",e)}on_reconnecting(e){this.reconnectingListeners.push(e)}on_reconnect(e){this.reconnectListeners.push(e)}on_reconnect_failed(e){this.reconnectFailedListeners.push(e)}on_recovered(e){this.recoveredListeners.push(e)}close(e=1e3,t){this.intentionalClose=!0,this.reconnectTimer&&clearTimeout(this.reconnectTimer),super.close(e,t)}on(e,t){if(this.socket.readyState!==WebSocket.OPEN){const s=this.preListen??={};return s[e]??=[],void s[e].push(t)}this.listen(e,t)}}exports.SonicWSCore=d;
|
|
@@ -33,6 +33,7 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
|
|
|
33
33
|
readonly parent?: string;
|
|
34
34
|
readonly variant?: string;
|
|
35
35
|
readonly isParent: boolean;
|
|
36
|
+
readonly permutationValues?: readonly string[];
|
|
36
37
|
readonly constructorName?: string;
|
|
37
38
|
readonly replay: boolean;
|
|
38
39
|
readonly rateLimit: number;
|
|
@@ -59,6 +60,8 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
|
|
|
59
60
|
prepareSend(values: any[], stateKey?: number): any[];
|
|
60
61
|
/** Clears error-feedback state for a disconnected sender. */
|
|
61
62
|
clearQuantizationState(stateKey: number): void;
|
|
63
|
+
/** Expands this group variant into its negotiated boolean permutation. */
|
|
64
|
+
permutation(): Record<string, boolean> | undefined;
|
|
62
65
|
/** Converts decoded positional data into schema objects and application-level numbers. */
|
|
63
66
|
finishReceive(decoded: any): any;
|
|
64
67
|
listen(value: Uint8Array, socket: SonicWSConnection | null): Promise<[processed: any, flatten: boolean] | string>;
|
|
@@ -93,6 +96,7 @@ export declare class PacketSchema<T extends (PacketType | readonly PacketType[])
|
|
|
93
96
|
parent: string;
|
|
94
97
|
variant: string;
|
|
95
98
|
isParent: boolean;
|
|
99
|
+
permutation?: string[];
|
|
96
100
|
};
|
|
97
101
|
constructorName?: string;
|
|
98
102
|
replay: boolean;
|
|
@@ -103,6 +107,7 @@ export declare class PacketSchema<T extends (PacketType | readonly PacketType[])
|
|
|
103
107
|
parent: string;
|
|
104
108
|
variant: string;
|
|
105
109
|
isParent: boolean;
|
|
110
|
+
permutation?: string[];
|
|
106
111
|
}, constructorName?: string, replay?: boolean);
|
|
107
112
|
testObject(packet: Packet<PacketType | readonly PacketType[]>): packet is Packet<PacketType[]>;
|
|
108
113
|
}
|
|
@@ -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.PacketSchema=exports.Packet=void 0;const t=require("../util/enums/EnumHandler"),e=require("../util/enums/EnumType"),i=require("../util/packets/CompressionUtil"),a=require("../util/packets/PacketUtils"),s=require("./PacketType"),r=require("../util/StringUtil"),n=require("../../native/wrapper"),o=require("../util/packets/JSONUtil"),c=require("../util/packets/ConstructorRegistry");class h{defaultEnabled;tag;maxSize;minSize;type;enumData;dataMax;dataMin;dataBatching;maxBatchSize;dontSpread;autoFlatten;fields;quantized;valueMin;valueMax;parent;variant;isParent;constructorName;replay;rateLimit;async;rereference;gzipCompression;object;client;processReceive;processSend;validate;customValidator;lastReceived={};lastSent={};quantizationErrors={};recordValues;constructor(t,e,i,a,r){if(this.tag=t,this.defaultEnabled=a,this.client=r,this.async=e.async,this.enumData=e.enumData,this.rateLimit=e.rateLimit,this.dontSpread=e.dontSpread,this.autoFlatten=e.autoFlatten,this.rereference=e.rereference,this.dataBatching=e.dataBatching,this.maxBatchSize=r?1/0:e.maxBatchSize,this.gzipCompression=e.gzipCompression,this.fields=e.fields,this.recordValues=this.fields?h.compileRecordValues(this.fields):void 0,this.quantized=e.quantized?{...e.quantized,trackError:e.quantized.trackError??!0}:void 0,this.valueMin=e.valueMin,this.valueMax=e.valueMax,this.parent=e.group?.parent,this.variant=e.group?.variant,this.isParent=e.group?.isParent??!1,this.constructorName=e.constructorName,this.replay=e.replay,this.object=e.object,this.type=e.type,this.dataMax=e.dataMax,this.dataMin=e.dataMin,e.testObject(this)){this.maxSize=this.minSize=this.type.length;for(let t=0;t<this.type.length;t++)this.type[t]==s.PacketType.NONE&&(this.dataMax[t]=this.dataMin[t]=0);const t={types:this.type,dataMins:this.dataMin,dataMaxes:this.dataMax,enumData:this.enumData};this.processReceive=e=>(0,n.decodeNativeObject)(t,e).map((t,e)=>this.type[e]===s.PacketType.JSON?(0,o.decompressJSON)(t):t),this.processSend=e=>{let i=0;const a=e.map((t,e)=>{if(this.type[e]===s.PacketType.JSON)return(0,o.compressJSON)(t);if(this.type[e]!==s.PacketType.ENUMS)return t;const a=this.enumData[i++];return(Array.isArray(t)?t:[t]).map(t=>{if(!Number.isInteger(t)||t<0||t>=a.values.length)throw new Error(`Invalid wrapped enum index: ${t}`);return a.values[t]})});return(0,n.encodeNativeObject)(t,a)},this.validate=async e=>{(0,n.validateNativeObject)(t,e);return(0,n.decodeNativeObject)(t,e).forEach((t,e)=>{if(this.type[e]!==s.PacketType.JSON)return;const i=(0,o.decompressJSON)(t),a=Array.isArray(i)?i.length:1;if(a<this.dataMin[e]||a>this.dataMax[e])throw new Error("JSON value count is outside schema limits")}),[e,!0]}}else{{this.maxSize=this.dataMax,this.minSize=this.dataMin,this.type==s.PacketType.NONE&&(this.dataMax=this.dataMin=0);const t=this.enumData[0];this.processReceive=e=>this.type===s.PacketType.JSON?(0,o.decompressJSON)(e):(0,n.decodeNative)(this.type,e,this.dataMax,t),this.processSend=e=>{let i;if(this.type===s.PacketType.JSON)i=(0,o.compressJSON)(e);else if(this.type===s.PacketType.ENUMS)i=Uint8Array.from(e);else{const a=this.type===s.PacketType.RAW&&1===e.length&&e[0]instanceof Uint8Array?e[0]:e;i=(0,n.encodeNative)(this.type,a,t)}return this.gzipCompression&&0===this.dataBatching?(0,n.deflateNative)(i):i},this.validate=async e=>{const i=this.gzipCompression&&0===this.dataBatching?(0,n.inflateNative)(e):e;if(this.type===s.PacketType.JSON){const t=(0,o.decompressJSON)(i),e=Array.isArray(t)?t.length:1;if(e<this.dataMin||e>this.dataMax)throw new Error("JSON value count is outside schema limits")}else(0,n.validateNative)(this.type,i,this.dataMin,this.dataMax,{enumData:t});return[i,!0]}}}this.customValidator=i}static compileRecordValues(t){switch(t.length){case 0:return()=>[];case 1:{const[e]=t;return t=>[t[e]]}case 2:{const[e,i]=t;return t=>[t[e],t[i]]}case 3:{const[e,i,a]=t;return t=>[t[e],t[i],t[a]]}case 4:{const[e,i,a,s]=t;return t=>[t[e],t[i],t[a],t[s]]}case 5:{const[e,i,a,s,r]=t;return t=>[t[e],t[i],t[a],t[s],t[r]]}case 6:{const[e,i,a,s,r,n]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n]]}case 7:{const[e,i,a,s,r,n,o]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n],t[o]]}case 8:{const[e,i,a,s,r,n,o,c]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n],t[o],t[c]]}default:return e=>{const i=new Array(t.length);for(let a=0;a<t.length;a++)i[a]=e[t[a]];return i}}}assertRecord(t,e){if(!this.fields)throw new Error(`Packet "${this.tag}" ${e} requires schema`);if(null===t||"object"!=typeof t||Array.isArray(t))throw new Error(`Packet "${this.tag}" ${e} requires an object record`);for(const e of this.fields)if(!Object.prototype.hasOwnProperty.call(t,e))throw new Error(`Packet "${this.tag}" is missing schema field(s): ${e}`);const i=Object.keys(t);if(i.length!==this.fields.length){const t=i.filter(t=>!this.fields.includes(t));if(t.length)throw new Error(`Packet "${this.tag}" has unknown schema field(s): ${t.join(", ")}`)}return this.recordValues(t)}construct(t){return this.constructorName?new((0,c.resolvePacketConstructor)(this.constructorName))(t):t}logicalValue(t,e,i){const a=this.quantized?.scale;if("number"!=typeof t||!Number.isFinite(t))throw new Error(`Packet "${this.tag}" ${e} value must be a finite number`);const s="receive"===e&&a?t/a:t;if(void 0!==this.valueMin&&s<this.valueMin)throw new Error(`Packet "${this.tag}" value ${s} is below minimum ${this.valueMin}`);if(void 0!==this.valueMax&&s>this.valueMax)throw new Error(`Packet "${this.tag}" value ${s} exceeds maximum ${this.valueMax}`);if("send"===e&&a){const t=s*a+(this.quantized?.trackError?this.quantizationErrors[i]??0:0),e=Math.round(t);return this.quantized?.trackError&&(this.quantizationErrors[i]=t-e),e}return s}logical(t,e,i=-1){const a=new Array(t.length);for(let s=0;s<t.length;s++)a[s]=this.logicalValue(t[s],e,i);return a}prepareSend(t,e=-1){if(this.object){if(this.autoFlatten&&this.fields){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoTranspose requires one array of records`);const e=t[0].map(t=>this.assertRecord(t,"autoTranspose"));return this.fields.map((t,i)=>e.map(t=>t[i]))}return t}let i=t;if(this.autoFlatten){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoFlatten requires one array of records`);const a=t[0],s=this.fields.length;i=new Array(a.length*s);let r=0;const n=this.quantized||void 0!==this.valueMin||void 0!==this.valueMax;for(const t of a){const a=this.assertRecord(t,"autoFlatten");for(let t=0;t<s;t++){const s=a[t];i[r++]=n?this.logicalValue(s,"send",e):s}}if(this.fields&&i.length%this.fields.length!==0)throw new Error(`Packet "${this.tag}" flat value count must be divisible by schema length ${this.fields.length}`);if(n)return i}else this.fields&&1===t.length&&null!==t[0]&&"object"==typeof t[0]&&!Array.isArray(t[0])&&(i=this.assertRecord(t[0],"schema mapping"));return this.quantized||void 0!==this.valueMin||void 0!==this.valueMax?this.logical(i,"send",e):i}clearQuantizationState(t){delete this.quantizationErrors[t]}finishReceive(t){if(this.object){if(this.autoFlatten&&this.fields){const e=t,i=e[0]?.length??0;if(e.some(t=>t.length!==i))throw new Error(`Packet "${this.tag}" autoTranspose columns have different lengths`);return Array.from({length:i},(t,i)=>{const a=this.fields.map((t,a)=>[t,e[a][i]]);return this.construct(Object.fromEntries(a))})}return this.autoFlatten?(0,a.UnFlattenData)(t):t}let e=Array.isArray(t)?t:[t];if((this.quantized||void 0!==this.valueMin||void 0!==this.valueMax)&&(e=this.logical(e,"receive")),this.autoFlatten){const t=this.fields.length;if(e.length%t!==0)throw new Error(`Packet "${this.tag}" flat value count ${e.length} is not divisible by schema length ${t}`);return Array.from({length:e.length/t},(i,a)=>this.construct(Object.fromEntries(this.fields.map((i,s)=>[i,e[a*t+s]]))))}if(this.fields){const t=this.fields.map((t,i)=>[t,e[i]]);return this.construct(Object.fromEntries(t))}return t}async listen(t,e){try{const[i,a]=await this.validate(t);if(!this.client&&!1===a)return"Invalid packet";const s=this.processReceive(i,a),r=this.finishReceive(s);if(null!=this.customValidator)if(this.dontSpread||this.fields){if(!this.customValidator(e,r))return"Didn't pass custom validator"}else if(!this.customValidator(e,...r))return"Didn't pass custom validator";return[this.isParent?{variant:"",payload:r}:r,!this.isParent&&!this.fields&&!this.dontSpread]}catch(t){return console.error("SonicWS failed to process a packet. Report reproducible codec failures at https://github.com/liwybloc/sonic-ws",t),`Error: ${String(t)}`}}serialize(){const t=(new TextEncoder).encode(JSON.stringify({schema:this.fields,quantized:this.quantized,min:this.valueMin,max:this.valueMax,group:void 0!==this.parent?{parent:this.parent,variant:this.variant??"",isParent:this.isParent}:void 0,constructor:this.constructorName,replay:this.replay||void 0})),e=[this.tag.length,...(0,r.processCharCodes)(this.tag),(0,i.compressBools)([this.dontSpread,this.async,this.object,this.autoFlatten,this.gzipCompression,this.rereference]),...(0,i.convertVarInt)(t.length),...t,this.dataBatching,this.enumData.length,...this.enumData.flatMap(t=>t.serialize())];return this.object?[...e,this.maxSize,...this.dataMax.flatMap(i.convertVarInt),...this.dataMin.flatMap(i.convertVarInt),...this.type]:[...e,...(0,i.convertVarInt)(this.dataMax),...(0,i.convertVarInt)(this.dataMin),this.type]}static readVarInts(t,e,a){const s=[];for(let r=0;r<a;r++){const[a,r]=(0,i.readVarInt)(t,e);e=a,s.push(r)}return[s,e]}static deserialize(a,n,o){const c=n,l=a[n++],d=(0,r.as8String)(a.slice(n,n+=l)),[p,m,f,y,v,g]=(0,i.decompressBools)(a[n++]),[M,S]=(0,i.readVarInt)(a,n);n=M;const x=a.slice(n,n+=S),w=JSON.parse((new TextDecoder).decode(x)),z=Array.isArray(w.schema)?w.schema:void 0,N=w.quantized&&"number"==typeof w.quantized.scale?w.quantized:void 0,k="number"==typeof w.min?w.min:void 0,P="number"==typeof w.max?w.max:void 0,b=w.group&&"string"==typeof w.group.parent?w.group:void 0,E="string"==typeof w.constructor?w.constructor:void 0,q=a[n++],O=a[n++],A=[];for(let i=0;i<O;i++){const i=a[n++],s=(0,r.as8String)(a.slice(n,n+=i)),o=a[n++],c=[];for(let t=0;t<o;t++){const t=a[n++],i=a[n++],s=(0,r.as8String)(a.slice(n,n+=t));c.push(e.TYPE_CONVERSION_MAP[i](s))}A.push((0,t.DefineEnum)(s,c))}if(f){const t=a[n++],[e,i]=this.readVarInts(a,n,t);n=i;const[r,l]=this.readVarInts(a,n,t);n=l;const f=Array.from(a.slice(n,n+=t));let g=0;const M=f.map(t=>t===s.PacketType.ENUMS?A[g++]:t),S=new u(!0,M,m,r,e,-1,p,y,!1,q,-1,v,z,void 0,void 0,void 0,b,E,!0===w.replay);return[new h(d,S,null,!1,o),n-c]}const[j,V]=(0,i.readVarInt)(a,n);n=j;const[$,T]=(0,i.readVarInt)(a,n);n=$;const D=a[n++],J=D===s.PacketType.ENUMS?A[0]:D,R=new u(!1,J,m,T,V,-1,p,y,g,q,-1,v,z,N,k,P,b,E,!0===w.replay);return[new h(d,R,null,!1,o),n-c]}static deserializeAll(t,e){const i=[];let a=0;for(;a<t.length;){const[s,r]=this.deserialize(t,a,e);i.push(s),a+=r}return i}}function l(t,i){return t instanceof e.EnumPackage?(i.push(t),s.PacketType.ENUMS):t}exports.Packet=h;class u{type;enumData=[];dataMax;dataMin;dataBatching;maxBatchSize;rateLimit;dontSpread=!1;autoFlatten=!1;async=!1;rereference=!1;gzipCompression=!1;object;fields;quantized;valueMin;valueMax;group;constructorName;replay;constructor(t,e,i,a,s,r,n,o,c,h,u,d,p,m,f,y,v,g,M=!1){this.object=t,this.async=i,this.dataMin=a,this.dataMax=s,this.rateLimit=r,this.dontSpread=n,this.autoFlatten=o,this.rereference=c,this.dataBatching=h,this.maxBatchSize=u,this.gzipCompression=d,this.fields=p?[...p]:void 0,this.quantized=m?{...m}:void 0,this.valueMin=f,this.valueMax=y,this.group=v?{...v}:void 0,this.constructorName=g,this.replay=M,this.type=t?e.map(t=>l(t,this.enumData)):l(e,this.enumData)}testObject(t){return this.object}}exports.PacketSchema=u;
|
|
7
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketSchema=exports.Packet=void 0;const t=require("../util/enums/EnumHandler"),e=require("../util/enums/EnumType"),i=require("../util/packets/CompressionUtil"),a=require("../util/packets/PacketUtils"),s=require("./PacketType"),r=require("../util/StringUtil"),n=require("../../native/wrapper"),o=require("../util/JSONUtil"),h=require("../util/packets/metadata/ConstructorRegistry");class c{defaultEnabled;tag;maxSize;minSize;type;enumData;dataMax;dataMin;dataBatching;maxBatchSize;dontSpread;autoFlatten;fields;quantized;valueMin;valueMax;parent;variant;isParent;permutationValues;constructorName;replay;rateLimit;async;rereference;gzipCompression;object;client;processReceive;processSend;validate;customValidator;lastReceived={};lastSent={};quantizationErrors={};recordValues;constructor(t,e,i,a,r){if(this.tag=t,this.defaultEnabled=a,this.client=r,this.async=e.async,this.enumData=e.enumData,this.rateLimit=e.rateLimit,this.dontSpread=e.dontSpread,this.autoFlatten=e.autoFlatten,this.rereference=e.rereference,this.dataBatching=e.dataBatching,this.maxBatchSize=r?1/0:e.maxBatchSize,this.gzipCompression=e.gzipCompression,this.fields=e.fields,this.recordValues=this.fields?c.compileRecordValues(this.fields):void 0,this.quantized=e.quantized?{...e.quantized,trackError:e.quantized.trackError??!0}:void 0,this.valueMin=e.valueMin,this.valueMax=e.valueMax,this.parent=e.group?.parent,this.variant=e.group?.variant,this.isParent=e.group?.isParent??!1,this.permutationValues=e.group?.permutation,this.constructorName=e.constructorName,this.replay=e.replay,this.object=e.object,this.type=e.type,this.dataMax=e.dataMax,this.dataMin=e.dataMin,e.testObject(this)){this.maxSize=this.minSize=this.type.length;for(let t=0;t<this.type.length;t++)this.type[t]==s.PacketType.NONE&&(this.dataMax[t]=this.dataMin[t]=0);const t={types:this.type,dataMins:this.dataMin,dataMaxes:this.dataMax,enumData:this.enumData};this.processReceive=e=>(0,n.decodeNativeObject)(t,e).map((t,e)=>this.type[e]===s.PacketType.JSON?(0,o.decompressJSON)(t):t),this.processSend=e=>{let i=0;const a=e.map((t,e)=>{if(this.type[e]===s.PacketType.JSON)return(0,o.compressJSON)(t);if(this.type[e]!==s.PacketType.ENUMS)return t;const a=this.enumData[i++];return(Array.isArray(t)?t:[t]).map(t=>{if(!Number.isInteger(t)||t<0||t>=a.values.length)throw new Error(`Invalid wrapped enum index: ${t}`);return a.values[t]})});return(0,n.encodeNativeObject)(t,a)},this.validate=async e=>{(0,n.validateNativeObject)(t,e);return(0,n.decodeNativeObject)(t,e).forEach((t,e)=>{if(this.type[e]!==s.PacketType.JSON)return;const i=(0,o.decompressJSON)(t),a=Array.isArray(i)?i.length:1;if(a<this.dataMin[e]||a>this.dataMax[e])throw new Error("JSON value count is outside schema limits")}),[e,!0]}}else{{this.maxSize=this.dataMax,this.minSize=this.dataMin,this.type==s.PacketType.NONE&&(this.dataMax=this.dataMin=0);const t=this.enumData[0];this.processReceive=e=>this.type===s.PacketType.JSON?(0,o.decompressJSON)(e):(0,n.decodeNative)(this.type,e,this.dataMax,t),this.processSend=e=>{let i;if(this.type===s.PacketType.JSON)i=(0,o.compressJSON)(e);else if(this.type===s.PacketType.ENUMS)i=Uint8Array.from(e);else{const a=this.type===s.PacketType.RAW&&1===e.length&&e[0]instanceof Uint8Array?e[0]:e;i=(0,n.encodeNative)(this.type,a,t)}return this.gzipCompression&&0===this.dataBatching?(0,n.deflateNative)(i):i},this.validate=async e=>{const i=this.gzipCompression&&0===this.dataBatching?(0,n.inflateNative)(e):e;if(this.type===s.PacketType.JSON){const t=(0,o.decompressJSON)(i),e=Array.isArray(t)?t.length:1;if(e<this.dataMin||e>this.dataMax)throw new Error("JSON value count is outside schema limits")}else(0,n.validateNative)(this.type,i,this.dataMin,this.dataMax,{enumData:t});return[i,!0]}}}this.customValidator=i}static compileRecordValues(t){switch(t.length){case 0:return()=>[];case 1:{const[e]=t;return t=>[t[e]]}case 2:{const[e,i]=t;return t=>[t[e],t[i]]}case 3:{const[e,i,a]=t;return t=>[t[e],t[i],t[a]]}case 4:{const[e,i,a,s]=t;return t=>[t[e],t[i],t[a],t[s]]}case 5:{const[e,i,a,s,r]=t;return t=>[t[e],t[i],t[a],t[s],t[r]]}case 6:{const[e,i,a,s,r,n]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n]]}case 7:{const[e,i,a,s,r,n,o]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n],t[o]]}case 8:{const[e,i,a,s,r,n,o,h]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n],t[o],t[h]]}default:return e=>{const i=new Array(t.length);for(let a=0;a<t.length;a++)i[a]=e[t[a]];return i}}}assertRecord(t,e){if(!this.fields)throw new Error(`Packet "${this.tag}" ${e} requires schema`);if(null===t||"object"!=typeof t||Array.isArray(t))throw new Error(`Packet "${this.tag}" ${e} requires an object record`);for(const e of this.fields)if(!Object.prototype.hasOwnProperty.call(t,e))throw new Error(`Packet "${this.tag}" is missing schema field(s): ${e}`);const i=Object.keys(t);if(i.length!==this.fields.length){const t=i.filter(t=>!this.fields.includes(t));if(t.length)throw new Error(`Packet "${this.tag}" has unknown schema field(s): ${t.join(", ")}`)}return this.recordValues(t)}construct(t){return this.constructorName?new((0,h.resolvePacketConstructor)(this.constructorName))(t):t}logicalValue(t,e,i){const a=this.quantized?.scale;if("number"!=typeof t||!Number.isFinite(t))throw new Error(`Packet "${this.tag}" ${e} value must be a finite number`);const s="receive"===e&&a?t/a:t;if(void 0!==this.valueMin&&s<this.valueMin)throw new Error(`Packet "${this.tag}" value ${s} is below minimum ${this.valueMin}`);if(void 0!==this.valueMax&&s>this.valueMax)throw new Error(`Packet "${this.tag}" value ${s} exceeds maximum ${this.valueMax}`);if("send"===e&&a){const t=s*a+(this.quantized?.trackError?this.quantizationErrors[i]??0:0),e=Math.round(t);return this.quantized?.trackError&&(this.quantizationErrors[i]=t-e),e}return s}logical(t,e,i=-1){const a=new Array(t.length);for(let s=0;s<t.length;s++)a[s]=this.logicalValue(t[s],e,i);return a}prepareSend(t,e=-1){if(this.object){if(this.autoFlatten&&this.fields){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoTranspose requires one array of records`);const e=t[0].map(t=>this.assertRecord(t,"autoTranspose"));return this.fields.map((t,i)=>e.map(t=>t[i]))}return t}let i=t;if(this.autoFlatten){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoFlatten requires one array of records`);const a=t[0],s=this.fields.length;i=new Array(a.length*s);let r=0;const n=this.quantized||void 0!==this.valueMin||void 0!==this.valueMax;for(const t of a){const a=this.assertRecord(t,"autoFlatten");for(let t=0;t<s;t++){const s=a[t];i[r++]=n?this.logicalValue(s,"send",e):s}}if(this.fields&&i.length%this.fields.length!==0)throw new Error(`Packet "${this.tag}" flat value count must be divisible by schema length ${this.fields.length}`);if(n)return i}else this.fields&&1===t.length&&null!==t[0]&&"object"==typeof t[0]&&!Array.isArray(t[0])&&(i=this.assertRecord(t[0],"schema mapping"));return this.quantized||void 0!==this.valueMin||void 0!==this.valueMax?this.logical(i,"send",e):i}clearQuantizationState(t){delete this.quantizationErrors[t]}permutation(){if(!this.permutationValues)return;const t=new Set(this.variant?this.variant.split(","):[]);return Object.fromEntries(this.permutationValues.map(e=>[e,t.has(e)]))}finishReceive(t){if(this.object){if(this.autoFlatten&&this.fields){const e=t,i=e[0]?.length??0;if(e.some(t=>t.length!==i))throw new Error(`Packet "${this.tag}" autoTranspose columns have different lengths`);return Array.from({length:i},(t,i)=>{const a=this.fields.map((t,a)=>[t,e[a][i]]);return this.construct(Object.fromEntries(a))})}return this.autoFlatten?(0,a.UnFlattenData)(t):t}let e=Array.isArray(t)?t:[t];if((this.quantized||void 0!==this.valueMin||void 0!==this.valueMax)&&(e=this.logical(e,"receive")),this.autoFlatten){const t=this.fields.length;if(e.length%t!==0)throw new Error(`Packet "${this.tag}" flat value count ${e.length} is not divisible by schema length ${t}`);return Array.from({length:e.length/t},(i,a)=>this.construct(Object.fromEntries(this.fields.map((i,s)=>[i,e[a*t+s]]))))}if(this.fields){const t=this.fields.map((t,i)=>[t,e[i]]);return this.construct(Object.fromEntries(t))}return t}async listen(t,e){try{const[i,a]=await this.validate(t);if(!this.client&&!1===a)return"Invalid packet";const s=this.processReceive(i,a),r=this.finishReceive(s);if(null!=this.customValidator)if(this.dontSpread||this.fields){if(!this.customValidator(e,r))return"Didn't pass custom validator"}else if(!this.customValidator(e,...r))return"Didn't pass custom validator";const n=this.permutation();return[this.isParent?{variant:"",payload:r,...n&&{permutation:n}}:r,!this.isParent&&!this.fields&&!this.dontSpread]}catch(t){return console.error("SonicWS failed to process a packet. Report reproducible codec failures at https://github.com/liwybloc/sonic-ws",t),`Error: ${String(t)}`}}serialize(){const t=(new TextEncoder).encode(JSON.stringify({schema:this.fields,quantized:this.quantized,min:this.valueMin,max:this.valueMax,group:void 0!==this.parent?{parent:this.parent,variant:this.variant??"",isParent:this.isParent,permutation:this.permutationValues}:void 0,constructor:this.constructorName,replay:this.replay||void 0})),e=[this.tag.length,...(0,r.processCharCodes)(this.tag),(0,i.compressBools)([this.dontSpread,this.async,this.object,this.autoFlatten,this.gzipCompression,this.rereference]),...(0,i.convertVarInt)(t.length),...t,this.dataBatching,this.enumData.length,...this.enumData.flatMap(t=>t.serialize())];return this.object?[...e,this.maxSize,...this.dataMax.flatMap(i.convertVarInt),...this.dataMin.flatMap(i.convertVarInt),...this.type]:[...e,...(0,i.convertVarInt)(this.dataMax),...(0,i.convertVarInt)(this.dataMin),this.type]}static readVarInts(t,e,a){const s=[];for(let r=0;r<a;r++){const[a,r]=(0,i.readVarInt)(t,e);e=a,s.push(r)}return[s,e]}static deserialize(a,n,o){const h=n,l=a[n++],d=(0,r.as8String)(a.slice(n,n+=l)),[p,m,f,v,y,g]=(0,i.decompressBools)(a[n++]),[M,S]=(0,i.readVarInt)(a,n);n=M;const w=a.slice(n,n+=S),x=JSON.parse((new TextDecoder).decode(w)),z=Array.isArray(x.schema)?x.schema:void 0,N=x.quantized&&"number"==typeof x.quantized.scale?x.quantized:void 0,b="number"==typeof x.min?x.min:void 0,k="number"==typeof x.max?x.max:void 0,E=x.group&&"string"==typeof x.group.parent?x.group:void 0,P="string"==typeof x.constructor?x.constructor:void 0,q=a[n++],O=a[n++],V=[];for(let i=0;i<O;i++){const i=a[n++],s=(0,r.as8String)(a.slice(n,n+=i)),o=a[n++],h=[];for(let t=0;t<o;t++){const t=a[n++],i=a[n++],s=(0,r.as8String)(a.slice(n,n+=t));h.push(e.TYPE_CONVERSION_MAP[i](s))}V.push((0,t.DefineEnum)(s,h))}if(f){const t=a[n++],[e,i]=this.readVarInts(a,n,t);n=i;const[r,l]=this.readVarInts(a,n,t);n=l;const f=Array.from(a.slice(n,n+=t));let g=0;const M=f.map(t=>t===s.PacketType.ENUMS?V[g++]:t),S=new u(!0,M,m,r,e,-1,p,v,!1,q,-1,y,z,void 0,void 0,void 0,E,P,!0===x.replay);return[new c(d,S,null,!1,o),n-h]}const[A,j]=(0,i.readVarInt)(a,n);n=A;const[$,T]=(0,i.readVarInt)(a,n);n=$;const D=a[n++],J=D===s.PacketType.ENUMS?V[0]:D,R=new u(!1,J,m,T,j,-1,p,v,g,q,-1,y,z,N,b,k,E,P,!0===x.replay);return[new c(d,R,null,!1,o),n-h]}static deserializeAll(t,e){const i=[];let a=0;for(;a<t.length;){const[s,r]=this.deserialize(t,a,e);i.push(s),a+=r}return i}}function l(t,i){return t instanceof e.EnumPackage?(i.push(t),s.PacketType.ENUMS):t}exports.Packet=c;class u{type;enumData=[];dataMax;dataMin;dataBatching;maxBatchSize;rateLimit;dontSpread=!1;autoFlatten=!1;async=!1;rereference=!1;gzipCompression=!1;object;fields;quantized;valueMin;valueMax;group;constructorName;replay;constructor(t,e,i,a,s,r,n,o,h,c,u,d,p,m,f,v,y,g,M=!1){this.object=t,this.async=i,this.dataMin=a,this.dataMax=s,this.rateLimit=r,this.dontSpread=n,this.autoFlatten=o,this.rereference=h,this.dataBatching=c,this.maxBatchSize=u,this.gzipCompression=d,this.fields=p?[...p]:void 0,this.quantized=m?{...m}:void 0,this.valueMin=f,this.valueMax=v,this.group=y?{...y}:void 0,this.constructorName=g,this.replay=M,this.type=t?e.map(t=>l(t,this.enumData)):l(e,this.enumData)}testObject(t){return this.object}}exports.PacketSchema=u;
|
|
@@ -73,6 +73,7 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
|
|
|
73
73
|
*/
|
|
74
74
|
send(tag: string, ...values: any[]): Promise<void>;
|
|
75
75
|
sendVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
|
|
76
|
+
sendPermutation(parent: string, selection: readonly boolean[] | Record<string, boolean>, ...values: any[]): Promise<void>;
|
|
76
77
|
/** Sends a validated server packet as an RPC request to this client. */
|
|
77
78
|
request(tag: string, ...valuesAndOptions: any[]): Promise<any>;
|
|
78
79
|
/** Registers the server-side responder for client requests using this packet tag. */
|
|
@@ -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.SonicWSConnection=void 0;const e=require("../util/packets/PacketUtils"),t=require("../util/packets/BatchHelper"),s=require("../util/packets/RateHandler"),i=require("../util/BufferUtil"),a=require("../Connection"),r=require("../util/packets/ControlProtocol");class n extends a.Connection{host;print=!1;handshakePacket;handshakeLambda;messageLambda=e=>this.routeMessage(e);handshakedMessageLambda=e=>{if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);return null==t?this.socket.close(a.CloseCodes.INVALID_DATA):t[0]===this.handshakePacket?this.socket.close(a.CloseCodes.REPEATED_HANDSHAKE):void this.messageHandler(t)};rater;enabledPackets={};handshakeComplete=!1;asyncMap={};asyncData={};nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;upgradeRequest;constructor(e,t,i,r,n,o,h,c,l,d){super(e,i,`Socket ${i}`,e.addEventListener.bind(e),e.removeEventListener.bind(e)),this.host=t,this.sessionId=h,this.upgradeRequest=d,this.state=c,this.handshakePacket=r;for(const e of t.clientPackets.getPackets()){const t=e.tag;this.listeners[t]=[],e.lastReceived[this.id]=void 0,this.enabledPackets[t]=e.defaultEnabled,this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}this.setInterval=this.setInterval.bind(this),this.batcher.registerSendPackets(this.host.serverPackets,this),this.invalidPacket=this.invalidPacket.bind(this),this.rater=new s.RateHandler(this),this.rater.registerRate("C",n),this.rater.registerRate("S",o),this.rater.registerAll(t.clientPackets,"client"),this.rater.registerAll(t.serverPackets,"server"),this.rater.start(),null==this.handshakePacket?this.socket.addEventListener("message",this.messageLambda):(this.handshakeLambda=e=>this.handshakeHandler(e),this.socket.addEventListener("message",this.handshakeLambda),this.setTimeout(()=>{this.handshakeComplete||this.close(a.CloseCodes.INVALID_DATA,"Application handshake timed out")},l)),this.socket.on("close",()=>{for(const e of t.clientPackets.getPackets())delete e.lastReceived[this.id];for(const e of t.serverPackets.getPackets())delete e.lastSent[this.id],e.clearQuantizationState(this.id)})}parseData(e){if(this.rater.trigger("C"))return null;if(!(e.data instanceof Buffer))return null;const t=new Uint8Array(e.data);if(this.print&&console.log(`[31m⬇ [38;5;245m(${this.id},${t.byteLength})[0m`,t.length>0&&this.host.clientPackets.getTag(t[0])||"<INVALID>",(0,i.stringifyBuffer)(t)),t.byteLength<1)return this.socket.close(a.CloseCodes.SMALL),null;const s=t[0],r=t.slice(1);if(!this.host.clientPackets.hasKey(s))return this.socket.close(a.CloseCodes.INVALID_KEY),null;const n=this.host.clientPackets.getTag(s);return this.enabledPackets[n]?this.rater.trigger(`client${s}`)?null:[n,r]:(this.socket.close(a.CloseCodes.DISABLED_PACKET),null)}isControl(e){return e.data instanceof Buffer&&e.data.length>0&&0===e.data[0]}routeMessage(e){this.isControl(e)?this.handleControl(new Uint8Array(e.data)):this.messageHandler(this.parseData(e))}async handleControl(e){if(this.rater.trigger("C"))return;let t;try{t=(0,r.decodeControl)(e)}catch{return void this.close(a.CloseCodes.INVALID_DATA,"Malformed SonicWS control frame")}if(t.type!==r.ControlType.RESUME)if(this.handshakeComplete||null===this.handshakePacket){if(t.type===r.ControlType.REPLAY||t.type===r.ControlType.RESUMED)throw new Error("A server cannot receive replay delivery frames");if(t.type===r.ControlType.RESPONSE){const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}try{const e=this.host.clientPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);if(!this.enabledPackets[e])return this.close(a.CloseCodes.DISABLED_PACKET,`Packet "${e}" is disabled`);if(this.rater.trigger(`client${t.packetKey}`))return this.close(a.CloseCodes.RATELIMIT,`Packet "${e}" exceeded its rate limit`);if(await this.callMiddleware("onReceive_pre",e,t.payload,t.payload.length))throw new Error(`Packet "${e}" was rejected by middleware`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const i=await this.host.clientPackets.getPacket(e).listen(t.payload,this);if("string"==typeof i)throw new Error(i);const[n,o]=i,h=o?await s(...n):await s(n);this.raw_send((0,r.encodeControlResponse)(t.id,!0,h??null))}catch(e){this.raw_send((0,r.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}else this.close(a.CloseCodes.INVALID_DATA,"Handshake required before control requests");else await this.host.resumeSession(this,t.sessionId,t.lastSequence)}handshakeHandler(e){if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);if(null==t)return this.socket.close(a.CloseCodes.INVALID_DATA);t[0]===this.handshakePacket?(this.messageHandler(t),this.socket.removeEventListener("message",this.handshakeLambda),this.socket.addEventListener("message",this.handshakedMessageLambda),this.handshakeComplete=!0):this.socket.close(a.CloseCodes.INVALID_DATA)}invalidPacket(e){console.warn("Closing connection after an invalid packet",e),this.socket.close(a.CloseCodes.INVALID_PACKET,e)}isAsync(e){return this.asyncMap[e]}listenLock=!1;packetQueue=[];async deliverPacket(t,s,i,a,r){if(this.closed)return;await(0,e.listenPacket)(t,this.listeners[s],this.invalidPacket);const n=this.host.clientPackets.getPacket(s);if("string"!=typeof t&&n.parent&&n.variant)for(const e of this.listeners[n.parent]??[])await e({variant:n.variant,payload:t[0]})
|
|
7
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSConnection=void 0;const e=require("../util/packets/PacketUtils"),t=require("../util/packets/BatchHelper"),s=require("../util/packets/RateHandler"),i=require("../util/BufferUtil"),a=require("../Connection"),r=require("../util/packets/ControlProtocol");class n extends a.Connection{host;print=!1;handshakePacket;handshakeLambda;messageLambda=e=>this.routeMessage(e);handshakedMessageLambda=e=>{if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);return null==t?this.socket.close(a.CloseCodes.INVALID_DATA):t[0]===this.handshakePacket?this.socket.close(a.CloseCodes.REPEATED_HANDSHAKE):void this.messageHandler(t)};rater;enabledPackets={};handshakeComplete=!1;asyncMap={};asyncData={};nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;upgradeRequest;constructor(e,t,i,r,n,o,h,c,l,d){super(e,i,`Socket ${i}`,e.addEventListener.bind(e),e.removeEventListener.bind(e)),this.host=t,this.sessionId=h,this.upgradeRequest=d,this.state=c,this.handshakePacket=r;for(const e of t.clientPackets.getPackets()){const t=e.tag;this.listeners[t]=[],e.lastReceived[this.id]=void 0,this.enabledPackets[t]=e.defaultEnabled,this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}this.setInterval=this.setInterval.bind(this),this.batcher.registerSendPackets(this.host.serverPackets,this),this.invalidPacket=this.invalidPacket.bind(this),this.rater=new s.RateHandler(this),this.rater.registerRate("C",n),this.rater.registerRate("S",o),this.rater.registerAll(t.clientPackets,"client"),this.rater.registerAll(t.serverPackets,"server"),this.rater.start(),null==this.handshakePacket?this.socket.addEventListener("message",this.messageLambda):(this.handshakeLambda=e=>this.handshakeHandler(e),this.socket.addEventListener("message",this.handshakeLambda),this.setTimeout(()=>{this.handshakeComplete||this.close(a.CloseCodes.INVALID_DATA,"Application handshake timed out")},l)),this.socket.on("close",()=>{for(const e of t.clientPackets.getPackets())delete e.lastReceived[this.id];for(const e of t.serverPackets.getPackets())delete e.lastSent[this.id],e.clearQuantizationState(this.id)})}parseData(e){if(this.rater.trigger("C"))return null;if(!(e.data instanceof Buffer))return null;const t=new Uint8Array(e.data);if(this.print&&console.log(`[31m⬇ [38;5;245m(${this.id},${t.byteLength})[0m`,t.length>0&&this.host.clientPackets.getTag(t[0])||"<INVALID>",(0,i.stringifyBuffer)(t)),t.byteLength<1)return this.socket.close(a.CloseCodes.SMALL),null;const s=t[0],r=t.slice(1);if(!this.host.clientPackets.hasKey(s))return this.socket.close(a.CloseCodes.INVALID_KEY),null;const n=this.host.clientPackets.getTag(s);return this.enabledPackets[n]?this.rater.trigger(`client${s}`)?null:[n,r]:(this.socket.close(a.CloseCodes.DISABLED_PACKET),null)}isControl(e){return e.data instanceof Buffer&&e.data.length>0&&0===e.data[0]}routeMessage(e){this.isControl(e)?this.handleControl(new Uint8Array(e.data)):this.messageHandler(this.parseData(e))}async handleControl(e){if(1===e.length)return;if(this.rater.trigger("C"))return;let t;try{t=(0,r.decodeControl)(e)}catch{return void this.close(a.CloseCodes.INVALID_DATA,"Malformed SonicWS control frame")}if(t.type!==r.ControlType.HEARTBEAT)if(t.type!==r.ControlType.RESUME)if(this.handshakeComplete||null===this.handshakePacket){if(t.type===r.ControlType.REPLAY||t.type===r.ControlType.RESUMED)throw new Error("A server cannot receive replay delivery frames");if(t.type===r.ControlType.RESPONSE){const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}try{const e=this.host.clientPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);if(!this.enabledPackets[e])return this.close(a.CloseCodes.DISABLED_PACKET,`Packet "${e}" is disabled`);if(this.rater.trigger(`client${t.packetKey}`))return this.close(a.CloseCodes.RATELIMIT,`Packet "${e}" exceeded its rate limit`);if(await this.callMiddleware("onReceive_pre",e,t.payload,t.payload.length))throw new Error(`Packet "${e}" was rejected by middleware`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const i=await this.host.clientPackets.getPacket(e).listen(t.payload,this);if("string"==typeof i)throw new Error(i);const[n,o]=i,h=o?await s(...n):await s(n);this.raw_send((0,r.encodeControlResponse)(t.id,!0,h??null))}catch(e){this.raw_send((0,r.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}else this.close(a.CloseCodes.INVALID_DATA,"Handshake required before control requests");else await this.host.resumeSession(this,t.sessionId,t.lastSequence)}handshakeHandler(e){if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);if(null==t)return this.socket.close(a.CloseCodes.INVALID_DATA);t[0]===this.handshakePacket?(this.messageHandler(t),this.socket.removeEventListener("message",this.handshakeLambda),this.socket.addEventListener("message",this.handshakedMessageLambda),this.handshakeComplete=!0):this.socket.close(a.CloseCodes.INVALID_DATA)}invalidPacket(e){console.warn("Closing connection after an invalid packet",e),this.socket.close(a.CloseCodes.INVALID_PACKET,e)}isAsync(e){return this.asyncMap[e]}listenLock=!1;packetQueue=[];async deliverPacket(t,s,i,a,r){if(this.closed)return;await(0,e.listenPacket)(t,this.listeners[s],this.invalidPacket);const n=this.host.clientPackets.getPacket(s);if("string"!=typeof t&&n.parent&&n.variant)for(const e of this.listeners[n.parent]??[]){const s=n.permutation();await e({variant:n.variant,payload:t[0],...s&&{permutation:s}})}await this.callMiddleware("onReceive_post",s,"string"==typeof t?[t]:t[0]),this.releasePacketLock(i,a,r)}releasePacketLock(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.messageHandler(e.shift())}async messageHandler(e,s=!1){if(null==e)return;const[i,a]=e,r=this.isAsync(i);let n,o,h;if(r?(h=this.asyncData[i],n=h[0],o=h[1]):(n=this.listenLock,o=this.packetQueue),n)return void o.push(e);r?h[0]=!0:this.listenLock=!0;const c=this.host.clientPackets.getPacket(i);if(!s&&await this.callMiddleware("onReceive_pre",c.tag,a,a.length))return void this.releasePacketLock(o,r,h);if(c.rereference&&0===a.length){const e=c.lastReceived[this.id];return void 0===e?void this.invalidPacket("No previous value to rereference"):void await this.deliverPacket(e,i,o,r,h)}if(0===c.dataBatching){const e=await c.listen(a,this);return c.lastReceived[this.id]=e,void await this.deliverPacket(e,i,o,r,h)}const l=await t.BatchHelper.unravelBatch(c,a,this);if("string"!=typeof l)for(const e of l)r?h[0]=!0:this.listenLock=!0,await this.deliverPacket(e,i,o,r,h);else this.invalidPacket(l)}enablePacket(e){this.enabledPackets[e]=!0}disablePacket(e){this.enabledPackets[e]=!1}on_close(e){this.socket.on("close",(t,s)=>e(t,String(s)))}on(e,t){if(!this.host.clientPackets.hasTag(e))throw new Error(`Packet tag "${e}" has not been created`);const s=this.host.clientPackets.resolveTag(e);this.listeners[s]??=[],this.listeners[s].push(t)}send_processed(e,t,s){if(!this.rater.trigger(`server${e}`))if(0===s.dataBatching){const a=(0,i.toPacketBuffer)(e,t);this.raw_send(s.replay?this.host.replayFrame(this,a):a)}else this.batcher.batchPacket(e,t)}sendQueue=[!1,[],void 0];async send(t,...s){if(await this.callMiddleware("onSend_pre",t,s,Date.now(),performance.now()))return;const[i,a,r]=await(0,e.processPacket)(this.host.serverPackets,t,s,this.sendQueue,this.id);await this.callMiddleware("onSend_post",t,a,a.length)||this.send_processed(i,a,r)}sendVariant(e,t,...s){return this.send(this.host.serverPackets.getVariantTag(e,t),...s)}sendPermutation(e,t,...s){return this.send(this.host.serverPackets.getPermutationVariant(e,t),...s)}async request(t,...s){const i=s.at(-1),a=s.length>1&&i&&"object"==typeof i&&!Array.isArray(i)&&Object.keys(i).every(e=>"timeoutMs"===e)?s.pop():{},[n,o]=await(0,e.processPacket)(this.host.serverPackets,t,s,this.sendQueue,this.id);if(this.rater.trigger(`server${n}`))throw new Error(`Packet "${t}" exceeded its rate limit`);const h=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((e,s)=>{const i=setTimeout(()=>{this.pendingRequests.delete(h),s(new Error(`RPC request "${t}" timed out`))},a.timeoutMs??5e3);this.pendingRequests.set(h,{resolve:e,reject:s,timer:i}),this.raw_send((0,r.encodeControlRequest)(h,n,o))})}respond(e,t){this.responders.set(this.host.clientPackets.resolveTag(e),t)}async sendSafe(e,...t){try{return await this.send(e,...t),!0}catch(t){return this.host.handleSendError(t,{packetTag:e,connection:this}),!1}}async sendVolatile(e,...t){return!!this.canSendVolatile()&&(await this.send(e,...t),!0)}sendReliable(e,...t){return this.send(e,...t)}broadcastFiltered(e,t,...s){this.host.broadcastFiltered(e,e=>e!==this&&t(e),...s)}broadcast(e,...t){this.broadcastFiltered(e,()=>!0,...t)}join(e){this.host.join(this,e)}leave(e){this.host.leave(this,e)}getRooms(){return this.host.rooms(this)}broadcastRoom(e,t,...s){return this.host.broadcastRoomExcept(this,e,t,...s)}togglePrint(){this.print=!this.print}raw_send(e){this.isClosed()?console.warn("Cannot send through a closed connection",this.id,(0,i.stringifyBuffer)(e)):this.rater.trigger("S")||(this.print&&console.log(`[32m⬆ [38;5;245m(${this.id},${e.byteLength})[0m`,e.length>0&&this.host.serverPackets.getTag(e[0])||"<INVALID>",(0,i.stringifyBuffer)(e)),super.raw_send(e))}tag(e,t=!0){this.host.tag(this,e,t)}}exports.SonicWSConnection=n;
|
|
@@ -19,9 +19,11 @@ export type SonicServerSettings = {
|
|
|
19
19
|
readonly serveBrowserClient?: boolean;
|
|
20
20
|
/** Limits a required application handshake. Defaults to 10 seconds. */
|
|
21
21
|
readonly handshakeTimeoutMs?: number;
|
|
22
|
-
/**
|
|
22
|
+
/** Enables portable CONTROL heartbeats. Defaults to true. */
|
|
23
|
+
readonly heartbeat?: boolean;
|
|
24
|
+
/** Controls the heartbeat interval. Zero disables it. */
|
|
23
25
|
readonly heartbeatIntervalMs?: number;
|
|
24
|
-
/** Limits how long the server waits for
|
|
26
|
+
/** Limits how long the server waits for any reply before terminating. */
|
|
25
27
|
readonly heartbeatTimeoutMs?: number;
|
|
26
28
|
};
|
|
27
29
|
/**
|
|
@@ -154,6 +156,7 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
|
|
|
154
156
|
broadcast(tag: string, ...values: any): Promise<void>;
|
|
155
157
|
broadcastSafe(tag: string, ...values: any[]): Promise<boolean>;
|
|
156
158
|
broadcastVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
|
|
159
|
+
broadcastPermutation(parent: string, selection: readonly boolean[] | Record<string, boolean>, ...values: any[]): Promise<void>;
|
|
157
160
|
handleSendError(error: unknown, context: {
|
|
158
161
|
packetTag: string;
|
|
159
162
|
connection?: SonicWSConnection;
|
|
@@ -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"),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;
|
|
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"),h=require("../util/packets/CompressionUtil"),d=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))>h.MAX_USHORT?(console.warn(`A rate limit above ${h.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?.heartbeat??1?e.sonicServerSettings?.heartbeatIntervalMs??3e4:0,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,h.convertVarInt)(v.length),...v,...f];(0,g.setHashFunc)(e.sonicServerSettings?.bit64Hash??!0),this.wss.on("connection",async(t,s)=>{const r=e.sonicServerSettings?.heartbeat??1?e.sonicServerSettings?.heartbeatIntervalMs??3e4:0,a=e.sonicServerSettings?.heartbeatTimeoutMs??1e4;let i,c,l=Date.now();t.on("message",()=>{l=Date.now(),i&&clearTimeout(i),i=void 0});const u=(0,w.randomUUID)(),v={state:{},rooms:new Set,sequence:0,frames:[],expiresAt:1/0};this.sessions.set(u,v);const g=new o.SonicWSConnection(t,this,this.generateSocketID(),this.handshakePacket,this.clientRateLimit,this.serverRateLimit,u,v.state,e.sonicServerSettings?.handshakeTimeoutMs??1e4,s);if(r>0&&(c=setInterval(()=>{if(t.readyState!==n.WebSocket.OPEN)return;if(Date.now()-l<r)return;t.send(Uint8Array.of(0));const e=Date.now();i=setTimeout(()=>{if(l>=e)return;t.close(p.CloseCodes.HEARTBEAT_TIMEOUT,"Heartbeat timeout");const s=setTimeout(()=>t.terminate(),250);s.unref?.()},a),i.unref?.()},r),c.unref?.()),await this.callMiddleware("onClientConnect",g)){const e="Connection blocked by middleware";return g.close(p.CloseCodes.MIDDLEWARE,e),this.callMiddleware("onClientDisconnect",g,p.CloseCodes.MIDDLEWARE,Buffer.from(e)),this.availableIds.push(g.id),void this.sessions.delete(u)}const f=(new TextEncoder).encode(u),S=new Uint8Array([...(0,h.convertVarInt)(g.id),...(0,h.convertVarInt)(f.length),...f,...m]);t.send([...b,...(0,d.deflateNative)(S)]),this.connections.push(g),this.connectionMap[g.id]=g,this.connectListeners.forEach(e=>e(g)),t.on("close",(e,t)=>{c&&clearInterval(c),i&&clearTimeout(i);const s=new Set(this.tags.get(g)??[]);if(this.connections.splice(this.connections.indexOf(g),1),delete this.connectionMap[g.id],this.availableIds.push(g.id),this.tags.has(g)){for(const e of this.tags.get(g))this.tagsInv.get(e)?.delete(g);this.tags.delete(g)}Promise.resolve(this.adapter?.disconnect(g.id)).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",connection:g}));const r=this.sessions.get(g.sessionId);if(r){r.rooms=s,r.expiresAt=Date.now()+this.recoveryMaxDisconnectionMs;const e=setTimeout(()=>{const e=this.sessions.get(g.sessionId);e===r&&e.expiresAt<=Date.now()&&this.sessions.delete(g.sessionId)},this.recoveryMaxDisconnectionMs+1);e.unref?.()}this.callMiddleware("onClientDisconnect",g,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)}broadcastPermutation(e,t,...s){return this.broadcast(this.serverPackets.getPermutationVariant(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;
|
|
@@ -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.decompressJSON=exports.compressJSON=void 0;const r=require("./BufferUtil"),e=r=>Array.from({length:8},(e,t)=>!!(r&1<<7-t));function t(r){if(!Number.isInteger(r)||r<0)throw new Error(`Invalid JSON variable integer: ${r}`);const e=[];do{let t=127&r;(r>>>=7)>0&&(t|=128),e.push(t)}while(r>0);return e}function n(r,e){let t=0,n=0;for(;;){if(e>=r.length||n>28)throw new Error("Invalid JSON variable integer");const s=r[e++];if(t+=(127&s)<<n,!(128&s))return[e,t];n+=7}}var s;!function(r){r[r.NULL=0]="NULL",r[r.BOOL=1]="BOOL",r[r.INT=2]="INT",r[r.FLOAT=3]="FLOAT",r[r.STRING=4]="STRING",r[r.ARRAY=5]="ARRAY",r[r.OBJECT=6]="OBJECT"}(s||(s={}));const o=r=>{const e=(new TextEncoder).encode(r);return[...t(e.length),...e]},a=(r,e)=>{const[t,s]=n(r,e);return{value:(new TextDecoder).decode(r.subarray(t,t+s)),length:t+s-e}},u=r=>{let e="";for(const t of r)e+=t.toString(2).padStart(3,"0");return(r=>new Uint8Array(r.match(/.{1,8}/g)?.map(r=>parseInt(r.padEnd(8,"0"),2))??[]))(e)},c=(r,e)=>{const t=(r=>Array.from(r,r=>r.toString(2).padStart(8,"0")).join(""))(r),n=[];for(let r=0;r<e;r++)n.push(parseInt(t.slice(3*r,3*r+3),2));return n};exports.compressJSON=e=>{const n=[],a=[],c=[],l=r=>{switch(null===r?"null":Array.isArray(r)?"array":typeof r){case"null":c.push(s.NULL);break;case"boolean":c.push(s.BOOL),n.push(r);break;case"number":Number.isInteger(r)?(c.push(s.INT),a.push(...t((r=>r<<1^r>>31)(r)))):(c.push(s.FLOAT),a.push(...function(r){if(Number.isNaN(r))return[127,128,0,1];const e=r<0?1:0;if(0===(r=Math.abs(r)))return[0,0,0,0];const t=Math.floor(Math.log2(r));if(!Number.isFinite(r)||t>127||t<-126)return[e?255:127,128,0,0];const n=(e<<31|t+127<<23|8388607&Math.round((r/2**t-1)*2**23))>>>0;return[n>>>24,n>>>16&255,n>>>8&255,255&n]}(r)));break;case"string":c.push(s.STRING),a.push(...o(r));break;case"array":c.push(s.ARRAY),a.push(...t(r.length));for(const e of r)l(e);break;case"object":{c.push(s.OBJECT);const e=Object.keys(r);a.push(...t(e.length));for(const t of e)a.push(...o(t)),l(r[t]);break}default:throw new Error("Unsupported type")}};l(e);const h=n.length?(0,r.splitArray)(n,8).map(r=>r.reduce((r,e,t)=>r|Number(e)<<7-t,0)):[],p=u(c),i=[...t(h.length),...t(p.length)];return Uint8Array.from([...i,...h.flat(),...p,...a])};exports.decompressJSON=r=>{let t=0;const[o,u]=n(r,t);t=o;const[l,h]=n(r,t);t=l;const p=[];for(let n=0;n<u;n++)p.push(...e(r[t++]));let i=0;const f=r.subarray(t,t+h);t+=h;const N=c(f,8*f.length/3);let O=0;const d=e=>{if(e>500)throw new Error("JSON array too deep.");const o=N[O++];switch(o){case s.NULL:return null;case s.BOOL:return p[i++];case s.INT:{const[e,s]=n(r,t);return t=e,(u=s)>>>1^-(1&u)}case s.FLOAT:{const e=function(r){const e=(r[0]<<24|r[1]<<16|r[2]<<8|r[3])>>>0,t=e>>>23&255,n=(0===t?0:1)+(8388607&e)/2**23,s=255===t?0===n?1/0:NaN:n*2**(0===t?-126:t-127);return e>>>31?-s:s}(Array.from(r.subarray(t,t+4)));return t+=4,e}case s.STRING:{const{value:e,length:n}=a(r,t);return t+=n,e}case s.ARRAY:{const[s,o]=n(r,t);t=s;const a=[];for(let r=0;r<o;r++)a.push(d(e+1));return a}case s.OBJECT:{const[s,o]=n(r,t);t=s;const u={};for(let n=0;n<o;n++){const{value:n,length:s}=a(r,t);t+=s,u[n]=d(e+1)}return u}default:throw new Error(`Unknown type ${o}`)}var u};return d(0)};
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const CONTROL_KEY = 0;
|
|
7
7
|
export declare enum ControlType {
|
|
8
|
+
HEARTBEAT = 0,
|
|
8
9
|
REQUEST = 1,
|
|
9
10
|
RESPONSE = 2,
|
|
10
11
|
REPLAY = 3,
|
|
@@ -38,7 +39,12 @@ export type ControlResumed = {
|
|
|
38
39
|
recovered: boolean;
|
|
39
40
|
replayed: number;
|
|
40
41
|
};
|
|
41
|
-
export type
|
|
42
|
+
export type ControlHeartbeat = {
|
|
43
|
+
type: ControlType.HEARTBEAT;
|
|
44
|
+
};
|
|
45
|
+
export type ControlMessage = ControlHeartbeat | ControlRequest | ControlResponse | ControlReplay | ControlResume | ControlResumed;
|
|
46
|
+
/** Encodes the minimal heartbeat CONTROL frame. */
|
|
47
|
+
export declare const encodeHeartbeat: () => Uint8Array<ArrayBuffer>;
|
|
42
48
|
export declare function encodeControlRequest(id: number, packetKey: number, payload: Uint8Array): Uint8Array;
|
|
43
49
|
export declare function encodeControlResponse(id: number, ok: boolean, value: any): Uint8Array;
|
|
44
50
|
export declare const encodeReplay: (sequence: number, payload: Uint8Array) => Uint8Array<ArrayBuffer>;
|
|
@@ -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.encodeResumed=exports.encodeReplay=exports.ControlType=exports.CONTROL_KEY=void 0,exports.encodeControlRequest=function(r,o,n){return Uint8Array.from([exports.CONTROL_KEY,t.REQUEST,...(0,e.convertVarInt)(r),o,...n])},exports.encodeControlResponse=function(o,n,s){return Uint8Array.from([exports.CONTROL_KEY,t.RESPONSE,...(0,e.convertVarInt)(o),Number(n),...(0,r.compressJSON)(s)])},exports.encodeResume=function(r,o){const n=(new TextEncoder).encode(r);return Uint8Array.from([exports.CONTROL_KEY,t.RESUME,...(0,e.convertVarInt)(n.length),...n,...(0,e.convertVarInt)(o)])},exports.decodeControl=function(o){if(o[0]!==exports.CONTROL_KEY||o.length<3)throw new Error("Invalid SonicWS control frame");const n=o[1];switch(n){case t.REPLAY:{const[r,t]=(0,e.readVarInt)(o,2);return{type:n,sequence:t,payload:o.slice(r)}}case t.RESUME:{const[r,t]=(0,e.readVarInt)(o,2),s=r+t;if(s>o.length)throw new Error("Recovery frame has an invalid session id");const[c
|
|
7
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.encodeResumed=exports.encodeReplay=exports.encodeHeartbeat=exports.ControlType=exports.CONTROL_KEY=void 0,exports.encodeControlRequest=function(r,o,n){return Uint8Array.from([exports.CONTROL_KEY,t.REQUEST,...(0,e.convertVarInt)(r),o,...n])},exports.encodeControlResponse=function(o,n,s){return Uint8Array.from([exports.CONTROL_KEY,t.RESPONSE,...(0,e.convertVarInt)(o),Number(n),...(0,r.compressJSON)(s)])},exports.encodeResume=function(r,o){const n=(new TextEncoder).encode(r);return Uint8Array.from([exports.CONTROL_KEY,t.RESUME,...(0,e.convertVarInt)(n.length),...n,...(0,e.convertVarInt)(o)])},exports.decodeControl=function(o){if(1===o.length&&o[0]===exports.CONTROL_KEY)return{type:t.HEARTBEAT};if(o[0]!==exports.CONTROL_KEY||o.length<3)throw new Error("Invalid SonicWS control frame");const n=o[1];switch(n){case t.REPLAY:{const[r,t]=(0,e.readVarInt)(o,2);return{type:n,sequence:t,payload:o.slice(r)}}case t.RESUME:{const[r,t]=(0,e.readVarInt)(o,2),s=r+t;if(s>o.length)throw new Error("Recovery frame has an invalid session id");const[E,c]=(0,e.readVarInt)(o,s);return{type:n,sessionId:(new TextDecoder).decode(o.slice(r,s)),lastSequence:c}}case t.RESUMED:{if(o.length<4)throw new Error("Invalid recovery result frame");const[r,t]=(0,e.readVarInt)(o,3);return{type:n,recovered:0!==o[2],replayed:t}}case t.REQUEST:{const[r,t]=(0,e.readVarInt)(o,2);if(r>=o.length)throw new Error("RPC request is missing its packet key");return{type:n,id:t,packetKey:o[r],payload:o.slice(r+1)}}case t.RESPONSE:{const[t,s]=(0,e.readVarInt)(o,2);if(t>=o.length)throw new Error("RPC response is missing its status");return{type:n,id:s,ok:0!==o[t],value:(0,r.decompressJSON)(o.slice(t+1))}}default:throw new Error(`Unknown SonicWS control frame type: ${n}`)}};const e=require("./CompressionUtil"),r=require("../JSONUtil");var t;exports.CONTROL_KEY=0,function(e){e[e.HEARTBEAT=0]="HEARTBEAT",e[e.REQUEST=1]="REQUEST",e[e.RESPONSE=2]="RESPONSE",e[e.REPLAY=3]="REPLAY",e[e.RESUME=4]="RESUME",e[e.RESUMED=5]="RESUMED"}(t||(exports.ControlType=t={}));exports.encodeHeartbeat=()=>Uint8Array.of(exports.CONTROL_KEY);exports.encodeReplay=(r,o)=>Uint8Array.from([exports.CONTROL_KEY,t.REPLAY,...(0,e.convertVarInt)(r),...o]);exports.encodeResumed=(r,o)=>Uint8Array.from([exports.CONTROL_KEY,t.RESUMED,Number(r),...(0,e.convertVarInt)(o)]);
|
|
@@ -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;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()}};
|
|
7
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketHolder=void 0;const t=require("./metadata/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}getPermutationVariant(t,e){const a=this.getPacket(t).permutationValues;if(!a)throw new Error(`Packet group "${t}" does not define a VariantPermutation`);let r;if(Array.isArray(e)){if(e.length!==a.length||e.some(t=>"boolean"!=typeof t))throw new Error(`Variant permutation requires ${a.length} boolean flags`);r=a.filter((t,a)=>e[a])}else{const t=e,s=Object.keys(t);if(s.length!==a.length||s.some(e=>!a.includes(e)||"boolean"!=typeof t[e]))throw new Error("Variant permutation object must define every known key as a boolean");r=a.filter(e=>t[e])}if(!r.length)return t;const s=Object.keys(this.variants).filter(e=>e.startsWith(`${t}.`)).map(e=>e.slice(t.length+1)).find(t=>{const e=t.split(",");return e.length===r.length&&e.every(t=>r.includes(t))});if(!s)throw new Error("Variant permutation contains an invalid or opposite combination");return this.getVariantTag(t,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()}};
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { ConvertType, Packet, ValidatorFunction } from "../../packets/Packets";
|
|
7
7
|
import { PacketType } from "../../packets/PacketType";
|
|
8
8
|
import { EnumPackage } from "../enums/EnumType";
|
|
9
|
+
import { VariantPermutation } from "./VariantPermutation";
|
|
9
10
|
export type ProcessedPacket = [code: number, data: Uint8Array, packet: Packet<any>];
|
|
10
11
|
/** Valid packet type */
|
|
11
12
|
export type ArguableType = PacketType | EnumPackage;
|
|
@@ -99,14 +100,35 @@ export type MultiPacketSettings = SharedPacketSettings & {
|
|
|
99
100
|
/** Local class constructed for every decoded schema record. */
|
|
100
101
|
constructor?: Function;
|
|
101
102
|
};
|
|
103
|
+
type PacketGroupVariant = Omit<SinglePacketSettings, "tag">;
|
|
104
|
+
type PacketGroupDefaults = {
|
|
105
|
+
/** Settings inherited by every variant. Individual variant settings take precedence. */
|
|
106
|
+
defaults?: PacketGroupVariant;
|
|
107
|
+
/** Deprecated alias for `defaults`. */
|
|
108
|
+
delegate?: PacketGroupVariant;
|
|
109
|
+
};
|
|
102
110
|
export type PacketGroupSettings = {
|
|
103
111
|
tag: string;
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
} & PacketGroupDefaults & ({
|
|
113
|
+
variants: Record<string, PacketGroupVariant>;
|
|
114
|
+
} | {
|
|
115
|
+
variants: readonly string[];
|
|
116
|
+
defaults: PacketGroupVariant;
|
|
117
|
+
} | {
|
|
118
|
+
variants: readonly string[];
|
|
119
|
+
delegate: PacketGroupVariant;
|
|
120
|
+
} | {
|
|
121
|
+
variants: VariantPermutation;
|
|
122
|
+
defaults: PacketGroupVariant;
|
|
123
|
+
} | {
|
|
124
|
+
variants: VariantPermutation;
|
|
125
|
+
delegate: PacketGroupVariant;
|
|
126
|
+
});
|
|
106
127
|
type GroupMetadata = {
|
|
107
128
|
parent: string;
|
|
108
129
|
variant: string;
|
|
109
130
|
isParent: boolean;
|
|
131
|
+
permutation?: string[];
|
|
110
132
|
};
|
|
111
133
|
/** Settings for single-typed enum packets */
|
|
112
134
|
export type EnumPacketSettings = SharedPacketSettings & {
|
|
@@ -150,6 +172,10 @@ export declare function CreateObjPacket<T extends readonly ArguableType[], V ext
|
|
|
150
172
|
* listeners on `movement.move` receive the child payload, while listeners on
|
|
151
173
|
* `movement` also receive `{ variant: "move", payload }`. Sending `movement`
|
|
152
174
|
* directly represents the parent/empty variant (`variant: ""`).
|
|
175
|
+
*
|
|
176
|
+
* `defaults` applies shared settings before each variant override. Array variants
|
|
177
|
+
* require `defaults`, for example `{ variants: ["W", "A"], defaults: { type:
|
|
178
|
+
* PacketType.SHORTS } }`. `delegate` remains accepted as a deprecated alias.
|
|
153
179
|
*/
|
|
154
180
|
export declare function CreatePacketGroup(settings: PacketGroupSettings): Packet<any>[];
|
|
155
181
|
/**
|
|
@@ -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=
|
|
7
|
+
Object.defineProperty(exports,"__esModule",{value:!0}),exports.processPacket=c,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=m,exports.CreateObjPacket=function(a){const r=Object.prototype.hasOwnProperty.call(a,"constructor")?a.constructor:void 0;let{tag:n,types:i=[],dataMaxes:c,dataMins:m,noDataRange:h=!1,dontSpread:w=!1,autoFlatten:g=!1,autoTranspose:y,schema:k,validator:P=null,dataBatching:v=0,maxBatchSize:E=10,rateLimit:$=0,enabled:S=!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(d(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!==y&&g&&y!==g)throw new Error(`Packet "${n}" has conflicting autoFlatten and autoTranspose options`);const A=y??g;for(const e of i)if(s(e))throw new Error(`Invalid packet type in "${n}" packet: ${e}`);if(h)c=Array.from({length:i.length},()=>u),m=Array.from({length:i.length},()=>0);else if(void 0===c?c=Array.from({length:i.length},()=>1):Array.isArray(c)||(c=Array.from({length:i.length},()=>c)),void 0===m){const e=c;m=Array.from({length:i.length},(t,a)=>e[a])}else Array.isArray(m)||(m=Array.from({length:i.length},()=>m));const x=m,q=c.map(l),M=x.map((e,a)=>i[a]===t.PacketType.NONE?0:p(e,q[a])),O=new e.PacketSchema(!0,i,T,M,q,f($),w,A,!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,S,!1)},exports.CreatePacketGroup=function(e){if(!e.tag||e.tag.includes("$"))throw new Error("Packet group tag is required and cannot contain '$'");if(void 0!==e.defaults&&void 0!==e.delegate)throw new Error(`Packet group "${e.tag}" cannot define both defaults and delegate`);const a=e.defaults??e.delegate,r=e.variants instanceof i.VariantPermutation?e.variants:void 0;if((Array.isArray(e.variants)||r)&&void 0===a)throw new Error(`Packet group "${e.tag}" array variants require defaults`);const n=r?r.generate().map(e=>[e,{}]):Array.isArray(e.variants)?e.variants.map(e=>[e,{}]):Object.entries(e.variants);if(!n.length)throw new Error(`Packet group "${e.tag}" requires at least one variant`);if(new Set(n.map(([e])=>e)).size!==n.length)throw new Error(`Packet group "${e.tag}" contains duplicate variant names`);const o=m({tag:e.tag,type:t.PacketType.NONE,dataMin:0,dataMax:0,_group:{parent:e.tag,variant:"",isParent:!0,permutation:r?.getValues()}}),c=n.map(([t,n])=>{if("string"!=typeof t||!t||t.includes("$"))throw new Error("Packet variant names cannot be empty or contain '$'");return m({...a,...n,tag:`${e.tag}.${t}`,_group:{parent:e.tag,variant:t,isParent:!1,permutation:r?.getValues()}})});return[o,...c]},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:u=10,rateLimit:l=0,enabled:p=!0,async:f=!1}=e;return m({tag:t,type:a,dataMax:r,dataMin:n,noDataRange:o,dontSpread:i,validator:c,dataBatching:s,maxBatchSize:u,rateLimit:l,enabled:p,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("./metadata/ConstructorRegistry"),i=require("./VariantPermutation");async function c(e,a,o,i,s,u=!1){const l=e.getKey(a),p=e.getPacket(a);return async function(e,t,a,r,n,o,i){if(e[0]&&!o)return new Promise(t=>e[1].push([t,r,n]));e[0]=!0;const s=await i();if(e[1].length>0){const[r,n,o]=e[1].shift();queueMicrotask(async()=>{r(await c(t,n,o,e,a,!0))})}else e[0]=!1;return s}(i,e,s,a,o,u,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[l,r.EMPTY_UINT8,p];p.lastSent[s]=e}if(o=p.prepareSend(o,s),p.autoFlatten&&p.object&&!p.fields)o=w(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[l,e,p]})}function s(e){return!("number"==typeof e&&e in t.PacketType||e instanceof a.EnumPackage)}const u=2048383;function l(e){return e<0?(console.warn("A data maximum below zero is treated as zero"),0):e>u?(console.warn(`A packet type can contain at most ${u} values`),u):e}function p(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 f(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 m(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:c=t.PacketType.NONE,dataMax:m=1,dataMin:w,noDataRange:g=!1,dontSpread:y=!1,validator:k=null,dataBatching:P=0,maxBatchSize:v=10,rateLimit:E=0,enabled:$=!0,async:S=!1,gzipCompression:T=c===t.PacketType.JSON,rereference:b=!1,schema:A,autoFlatten:x=!1,quantized:q,min:M,max:O}=a;if(!i)throw new Error("Packet tag is required");if(g||r?(w=b?1:0,m=u):void 0===w&&(w=c===t.PacketType.NONE?0:m),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(s(c))throw new Error(`Invalid packet type: ${c}`);if(d(A,i),n&&!A)throw new Error(`Packet "${i}" constructor requires schema`);if(n&&(0,o.RegisterPacketConstructor)(n),x&&(!A||0===A.length))throw new Error(`Packet "${i}" autoFlatten requires schema`);if(A&&!x&&w===m&&A.length!==m)throw new Error(`Packet "${i}" schema length must match its fixed value count (${m})`);!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)&&!h.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`)}(c,q,M,O,i);const z=new e.PacketSchema(!1,c,S,p(w,m),l(m),f(E),y,x,b,P,v,T,A,q,M,O,a._group,n?.name,a.replay??!1);return new e.Packet(i,z,k,$,!1)}function d(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 h=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]))}
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
* Generates valid permutations/combinations of variant values.
|
|
8
|
+
*
|
|
9
|
+
* Opposite pairs prevent invalid combinations from being generated.
|
|
10
|
+
* For example, with WASD:
|
|
11
|
+
*
|
|
12
|
+
* - W and S are opposites
|
|
13
|
+
* - A and D are opposites
|
|
14
|
+
*
|
|
15
|
+
* So `W,S`, `A,D`, `W,A,S`, etc. are excluded.
|
|
16
|
+
*/
|
|
17
|
+
export declare class VariantPermutation {
|
|
18
|
+
private readonly values;
|
|
19
|
+
private readonly opposites;
|
|
20
|
+
private generated?;
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new variant permutation generator.
|
|
23
|
+
*
|
|
24
|
+
* @param values The available variant values.
|
|
25
|
+
* @param opposites Optional pairs of indexes that cannot appear together.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const variants = new VariantPermutation(
|
|
29
|
+
* ["W", "A", "S", "D"],
|
|
30
|
+
* [[0, 2], [1, 3]]
|
|
31
|
+
* ).generate();
|
|
32
|
+
*/
|
|
33
|
+
constructor(values: string[], opposites?: [number, number][]);
|
|
34
|
+
/**
|
|
35
|
+
* Convenience helper for generating variants without manually creating
|
|
36
|
+
* a VariantPermutation instance.
|
|
37
|
+
*
|
|
38
|
+
* @param values The available variant values.
|
|
39
|
+
* @param opposites Optional pairs of indexes that cannot appear together.
|
|
40
|
+
* @returns A variant permutation object.
|
|
41
|
+
*/
|
|
42
|
+
static from(values: string[], opposites?: [number, number][]): VariantPermutation;
|
|
43
|
+
/**
|
|
44
|
+
* Generates valid WASD movement combinations.
|
|
45
|
+
*
|
|
46
|
+
* @returns Valid WASD combinations, excluding opposite directions.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* VariantPermutation.WASD();
|
|
50
|
+
* // [
|
|
51
|
+
* // "W", "A", "S", "D",
|
|
52
|
+
* // "W,A", "W,D", "S,A", "S,D"
|
|
53
|
+
* // ]
|
|
54
|
+
*/
|
|
55
|
+
static WASD(): VariantPermutation;
|
|
56
|
+
/**
|
|
57
|
+
* Generates valid arrow key movement combinations.
|
|
58
|
+
*
|
|
59
|
+
* @returns Valid arrow key combinations, excluding opposite directions.
|
|
60
|
+
*/
|
|
61
|
+
static Arrows(): VariantPermutation;
|
|
62
|
+
/** Returns the ordered boolean keys used by this permutation. */
|
|
63
|
+
getValues(): string[];
|
|
64
|
+
/** Resolves boolean flags or a keyed boolean object to a generated variant. */
|
|
65
|
+
resolve(selection: readonly boolean[] | Record<string, boolean>): string;
|
|
66
|
+
/** Expands a generated variant name into its keyed boolean representation. */
|
|
67
|
+
expand(variant: string): Record<string, boolean>;
|
|
68
|
+
/**
|
|
69
|
+
* Checks whether a combination contains any invalid opposite pair.
|
|
70
|
+
*/
|
|
71
|
+
private containsOpposites;
|
|
72
|
+
/**
|
|
73
|
+
* Creates a stable ordering map used for formatting generated combinations.
|
|
74
|
+
*
|
|
75
|
+
* Opposite pairs are treated as directional groups. For WASD:
|
|
76
|
+
*
|
|
77
|
+
* - W/S belong to group 0
|
|
78
|
+
* - A/D belong to group 1
|
|
79
|
+
*
|
|
80
|
+
* This makes diagonals display as `W,A`, `W,D`, `S,A`, `S,D`.
|
|
81
|
+
*/
|
|
82
|
+
private createGroupOrder;
|
|
83
|
+
}
|