sonic-ws 2.2.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.
Files changed (42) hide show
  1. package/README.md +28 -4
  2. package/bin/sonicws.mjs +159 -0
  3. package/bundled/bundle.js +1 -1
  4. package/dist/index.d.ts +9 -4
  5. package/dist/index.js +10 -3
  6. package/dist/native/wrapper.js +1 -1
  7. package/dist/version.d.ts +1 -1
  8. package/dist/version.js +1 -1
  9. package/dist/ws/Connection.d.ts +15 -3
  10. package/dist/ws/Connection.js +1 -1
  11. package/dist/ws/PacketProcessor.js +1 -1
  12. package/dist/ws/client/core/ClientCore.d.ts +20 -8
  13. package/dist/ws/client/core/ClientCore.js +1 -1
  14. package/dist/ws/client/node/ClientNode.d.ts +3 -2
  15. package/dist/ws/client/node/ClientNode.js +1 -1
  16. package/dist/ws/debug/PacketLogger.d.ts +30 -0
  17. package/dist/ws/debug/PacketLogger.js +7 -0
  18. package/dist/ws/packets/PacketType.d.ts +4 -1
  19. package/dist/ws/packets/Packets.d.ts +10 -1
  20. package/dist/ws/packets/Packets.js +1 -1
  21. package/dist/ws/server/SonicWSConnection.d.ts +14 -2
  22. package/dist/ws/server/SonicWSConnection.js +1 -1
  23. package/dist/ws/server/SonicWSServer.d.ts +18 -7
  24. package/dist/ws/server/SonicWSServer.js +1 -1
  25. package/dist/ws/util/JSONUtil.js +7 -0
  26. package/dist/ws/util/packets/BatchHelper.js +1 -1
  27. package/dist/ws/util/packets/ControlProtocol.d.ts +7 -1
  28. package/dist/ws/util/packets/ControlProtocol.js +1 -1
  29. package/dist/ws/util/packets/PacketHolder.js +1 -1
  30. package/dist/ws/util/packets/PacketUtils.d.ts +50 -12
  31. package/dist/ws/util/packets/PacketUtils.js +1 -1
  32. package/dist/ws/util/packets/VariantPermutation.d.ts +83 -0
  33. package/dist/ws/util/packets/VariantPermutation.js +7 -0
  34. package/dist/ws/util/packets/{PacketManifest.d.ts → metadata/PacketManifest.d.ts} +1 -1
  35. package/dist/ws/util/packets/{PacketManifest.js → metadata/PacketManifest.js} +1 -1
  36. package/dist/ws/util/packets/metadata/SchemaValidation.d.ts +17 -0
  37. package/dist/ws/util/packets/metadata/SchemaValidation.js +7 -0
  38. package/package.json +10 -4
  39. package/dist/ws/util/packets/JSONUtil.js +0 -7
  40. /package/dist/ws/util/{packets/JSONUtil.d.ts → JSONUtil.d.ts} +0 -0
  41. /package/dist/ws/util/packets/{ConstructorRegistry.d.ts → metadata/ConstructorRegistry.d.ts} +0 -0
  42. /package/dist/ws/util/packets/{ConstructorRegistry.js → metadata/ConstructorRegistry.js} +0 -0
@@ -5,7 +5,6 @@
5
5
  */
6
6
  import { PacketHolder } from "../../util/packets/PacketHolder";
7
7
  import { Connection } from "../../Connection";
8
- import { AsyncPQ, ClientPQ, PacketQueue } from "../../PacketProcessor";
9
8
  export type ReconnectOptions = {
10
9
  enabled?: boolean;
11
10
  attempts?: number;
@@ -19,11 +18,12 @@ type TransportBinding<T, K> = {
19
18
  on: Function;
20
19
  off: Function;
21
20
  };
22
- export declare abstract class SonicWSCore<T extends {
21
+ type ClientTransport = {
23
22
  readyState: number;
24
- send: (u: Uint8Array<ArrayBufferLike>) => void;
25
- close: (c: number, d: string | undefined) => void;
26
- }, K> extends Connection<T, K> {
23
+ send: (data: Uint8Array<ArrayBufferLike>) => void;
24
+ close: (code: number, reason: string | undefined) => void;
25
+ };
26
+ export declare abstract class SonicWSCore<T extends ClientTransport, K> extends Connection<T, K> {
27
27
  protected preListen: {
28
28
  [key: string]: Array<(data: any[]) => void>;
29
29
  } | null;
@@ -51,7 +51,11 @@ export declare abstract class SonicWSCore<T extends {
51
51
  private lastReplaySequence;
52
52
  private recoveredListeners;
53
53
  private pendingResumeSession?;
54
+ private handshakeTimeoutMs;
55
+ private handshakeTimer?;
54
56
  constructor(ws: T, bufferHandler: (val: K) => Promise<Uint8Array>, on: Function, off: Function);
57
+ protected configureHandshakeTimeout(milliseconds: number): void;
58
+ private armHandshakeTimeout;
55
59
  protected configureReconnect(factory: () => TransportBinding<T, K>, options?: ReconnectOptions): void;
56
60
  private attachClientTransport;
57
61
  private scheduleReconnect;
@@ -62,10 +66,9 @@ export declare abstract class SonicWSCore<T extends {
62
66
  private invalidPacket;
63
67
  private listenLock;
64
68
  private packetQueue;
65
- listenPacket(data: string | [any[], boolean], tag: string, packetQueue: PacketQueue<ClientPQ>, isAsync: boolean, asyncData: AsyncPQ<ClientPQ>): Promise<void>;
69
+ private deliverPacket;
70
+ private releasePacketLock;
66
71
  private isAsync;
67
- private enqueuePacket;
68
- private triggerNextPacket;
69
72
  private dataHandler;
70
73
  private handleControl;
71
74
  private messageHandler;
@@ -78,11 +81,16 @@ export declare abstract class SonicWSCore<T extends {
78
81
  */
79
82
  send(tag: string, ...values: any[]): Promise<void>;
80
83
  sendVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
84
+ sendPermutation(parent: string, selection: readonly boolean[] | Record<string, boolean>, ...values: any[]): Promise<void>;
81
85
  /** Sends a validated packet as an RPC request and waits for its response. */
82
86
  request(tag: string, ...valuesAndOptions: any[]): Promise<any>;
83
87
  /** Registers the client-side responder for server requests using this packet tag. */
84
88
  respond(tag: string, handler: (...values: any[]) => any): void;
85
89
  sendSafe(tag: string, ...values: any[]): Promise<boolean>;
90
+ /** Drops this update before encoding when the transport is backpressured. */
91
+ sendVolatile(tag: string, ...values: any[]): Promise<boolean>;
92
+ /** Sends a packet without applying the volatile backpressure drop policy. */
93
+ sendReliable(tag: string, ...values: any[]): Promise<void>;
86
94
  /**
87
95
  * Listens for when the client connects
88
96
  * @param listener Callback on connection
@@ -93,12 +101,16 @@ export declare abstract class SonicWSCore<T extends {
93
101
  * @param listener Callback on close with close event
94
102
  */
95
103
  on_close(listener: (...args: any[]) => void): void;
104
+ /** Listens for reconnect attempts before a replacement transport opens. */
96
105
  on_reconnecting(listener: (event: {
97
106
  attempt: number;
98
107
  delayMs: number;
99
108
  }) => void): void;
109
+ /** Listens for successful reconnection and schema negotiation. */
100
110
  on_reconnect(listener: () => void): void;
111
+ /** Listens for reconnect exhaustion. */
101
112
  on_reconnect_failed(listener: () => void): void;
113
+ /** Listens for the result of connection-state recovery. */
102
114
  on_recovered(listener: (event: {
103
115
  recovered: boolean;
104
116
  replayed: number;
@@ -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"),c=require("../../util/BufferUtil"),o=require("../../Connection"),h=require("../../util/StringUtil"),l=require("../../util/packets/ControlProtocol");class d extends o.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;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.bufferHandler=t}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()}catch{this.scheduleReconnect()}}reading=!1;readQueue=[];async serverKeyHandler(e){if(this.reading)return 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 server requested is not a Sonic WS server.");const a=n[3];if(a!=i.VERSION)throw this.close(1e3),new Error(`Version mismatch: ${a>i.VERSION?"client":"server"} is outdated (server: ${a}, client: ${i.VERSION})`);const c=(0,s.inflateNative)(n.subarray(4,n.length)),o=this.sessionId,[d,u]=(0,t.readVarInt)(c,0);this.id=u;const[p,y]=(0,t.readVarInt)(c,d);this.sessionId=(new TextDecoder).decode(c.slice(p,p+y));const[k,f]=(0,t.readVarInt)(c,p+y),v=c.subarray(k,k+f);this.clientPackets.holdPackets(r.Packet.deserializeAll(v,!0));const g=c.subarray(k+f,c.length);this.serverPackets.holdPackets(r.Packet.deserializeAll(g,!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,[]])}Object.keys(this.preListen??{}).forEach(e=>this.preListen[e].forEach(t=>{if(!this.serverPackets.hasTag(e))return console.error(new Error(`The server does not send the packet with tag "${e}"!`));this.listen(e,t)})),this.preListen=null,this.pastKeys=!0,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),o&&o!==this.sessionId&&(this.pendingResumeSession=o,this.raw_send((0,l.encodeResume)(o,this.lastReplaySequence))),this.readQueue.forEach(e=>this.messageHandler(e)),this.readQueue=[]}invalidPacket(e){throw console.error(e),new Error("An error occured with data from the server!! This is probably my fault.. make an issue at https://github.com/liwybloc/sonic-ws")}listenLock=!1;packetQueue=[];async listenPacket(e,t,s,i,r){const a=this.listeners[t],c=this.serverPackets.getPacket(t),o=c.parent?this.listeners[c.parent]:void 0;if(!a&&!o)return console.warn("Warn: No listener for packet "+t),void await this.triggerNextPacket(s,i,r);if(a&&await(0,n.listenPacket)(e,a,this.invalidPacket),"string"!=typeof e&&c.parent&&c.variant)for(const t of o??[])await t({variant:c.variant,payload:e[0]});await this.triggerNextPacket(s,i,r),await this.callMiddleware("onReceive_post",t,"string"==typeof e?[e]:e[0])}isAsync(e){return this.asyncMap[e]}async enqueuePacket(e,t,s,i,r){await(0,n.listenPacket)(e,t,this.invalidPacket),await this.triggerNextPacket(s,i,r)}async triggerNextPacket(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.dataHandler(e.shift())}async dataHandler(e){if(0===e[0])return this.handleControl(e);const t=e[0],s=e.slice(1),n=this.isAsync(t);let i,r,c;if(n?(c=this.asyncData[t],i=c[0],r=c[1]):(i=this.listenLock,r=this.packetQueue),i)return void r.push(e);n?c[0]=!0:this.listenLock=!0;const o=this.serverPackets.getTag(t),h=this.serverPackets.getPacket(o);if(await this.callMiddleware("onReceive_pre",h.tag,e,e.length))return;if(h.rereference&&0==s.length)return void 0===h.lastReceived[0]?this.invalidPacket("No previous value to rereference"):void this.listenPacket(h.lastReceived[0],o,r,n,c);if(0==h.dataBatching){const e=h.lastReceived[0]=await h.listen(s,null);return void this.listenPacket(e,o,r,n,c)}const l=await a.BatchHelper.unravelBatch(h,s,null);if("string"==typeof l)return this.invalidPacket(l);l.forEach(e=>this.listenPacket(e,o,r,n,c))}async handleControl(e){const t=(0,l.decodeControl)(e);if(t.type===l.ControlType.REPLAY){if(t.sequence<=this.lastReplaySequence)return;return this.lastReplaySequence=t.sequence,void await this.dataHandler(t.payload)}if(t.type===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));if(t.type===l.ControlType.RESUME)throw new Error("A client cannot receive a recovery request");if(t.type===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))))}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.log("Tag is not available on server: "+e);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,c.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),c=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((t,s)=>{const n=setTimeout(()=>{this.pendingRequests.delete(c),s(new Error(`RPC request "${e}" timed out`))},i.timeoutMs??5e3);this.pendingRequests.set(c,{resolve:t,reject:s,timer:n}),this.raw_send((0,l.encodeControlRequest)(c,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}}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]||(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;
@@ -7,15 +7,16 @@ import WS from 'ws';
7
7
  import { ReconnectOptions, SonicWSCore } from "../core/ClientCore";
8
8
  export type SonicConnectOptions = WS.ClientOptions & {
9
9
  reconnect?: ReconnectOptions;
10
+ readyTimeoutMs?: number;
10
11
  };
11
- /** Class to connect to a SonicWS server */
12
+ /** Connects a Node.js process to a SonicWS server. */
12
13
  export declare class SonicWS extends SonicWSCore<WS.WebSocket, Buffer> {
13
14
  /**
14
15
  * Creates a connection to the url
15
16
  * @param url The url to connect to
16
17
  * @param options The websocket options
17
18
  */
18
- constructor(url: string, options?: WS.ClientOptions, reconnect?: ReconnectOptions);
19
+ constructor(url: string, options?: WS.ClientOptions, reconnect?: ReconnectOptions, readyTimeoutMs?: number);
19
20
  /** Creates a client and resolves after WASM loading and schema negotiation. */
20
21
  static connect(url: string, options?: SonicConnectOptions): Promise<SonicWS>;
21
22
  }
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- var e=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWS=void 0;const n=e(require("ws")),o=require("../core/ClientCore");class r extends o.SonicWSCore{constructor(e,o,r){const t=new n.default.WebSocket(e,o);t.on("error",()=>{}),super(t,e=>Promise.resolve(new Uint8Array(e)),t.on.bind(t),t.off.bind(t)),r?.enabled&&this.configureReconnect(()=>{const r=new n.default.WebSocket(e,o);return r.on("error",()=>{}),{socket:r,bufferHandler:e=>Promise.resolve(new Uint8Array(e)),on:r.on.bind(r),off:r.off.bind(r)}},r)}static async connect(e,n={}){const{reconnect:o,...t}=n,c=new r(e,t,o);return await new Promise((e,n)=>{c.on_ready(e),c.on_close((e,o)=>{c.clientPackets.getPackets()?.length||n(new Error(`SonicWS connection closed before ready (${e}: ${o})`))})}),c}}exports.SonicWS=r;
7
+ var e=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWS=void 0;const o=e(require("ws")),n=require("../core/ClientCore");class r extends n.SonicWSCore{constructor(e,n,r,t=1e4){const c=new o.default.WebSocket(e,n);c.on("error",()=>{}),super(c,e=>Promise.resolve(new Uint8Array(e)),c.on.bind(c),c.off.bind(c)),this.configureHandshakeTimeout(t),r?.enabled&&this.configureReconnect(()=>{const r=new o.default.WebSocket(e,n);return r.on("error",()=>{}),{socket:r,bufferHandler:e=>Promise.resolve(new Uint8Array(e)),on:r.on.bind(r),off:r.off.bind(r)}},r)}static async connect(e,o={}){const{reconnect:n,readyTimeoutMs:t,...c}=o,s=new r(e,c,n,t);return await new Promise((e,o)=>{s.on_ready(e),s.on_close((e,n)=>{s.clientPackets.getPackets()?.length||o(new Error(`SonicWS connection closed before ready (${e}: ${n})`))})}),s}}exports.SonicWS=r;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright (c) 2026 Lily (liwybloc)
3
+ * License-Identifier: LicenseRef-Lily-Personal-NonCommercial-2026
4
+ * See LICENSE for personal, non-commercial license terms.
5
+ */
6
+ import type { ConnectionMiddleware } from "../PacketProcessor";
7
+ export type PacketLogEntry = {
8
+ direction: "send" | "receive";
9
+ tag: string;
10
+ values: unknown;
11
+ bytes: number;
12
+ timestamp: number;
13
+ };
14
+ export type PacketLoggerOptions = {
15
+ logger?: (entry: PacketLogEntry) => void;
16
+ includeValues?: boolean;
17
+ };
18
+ /** Readable packet logging implemented through the public middleware hooks. */
19
+ export declare class PacketLogger implements ConnectionMiddleware {
20
+ private readonly logger;
21
+ private readonly includeValues;
22
+ private readonly sends;
23
+ private readonly receiveSizes;
24
+ constructor(options?: PacketLoggerOptions);
25
+ onSend_pre: (tag: string, values: unknown[]) => void;
26
+ onSend_post: (tag: string, _data: Uint8Array, size: number) => void;
27
+ onReceive_pre: (tag: string, _data: Uint8Array, size: number) => void;
28
+ onReceive_post: (tag: string, values: unknown) => void;
29
+ private emit;
30
+ }
@@ -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.PacketLogger=void 0;exports.PacketLogger=class{logger;includeValues;sends=new Map;receiveSizes=new Map;constructor(e={}){this.includeValues=e.includeValues??!0,this.logger=e.logger??(e=>{const s="send"===e.direction?"→":"←";console.debug(`${s} ${e.tag}`,e.values,`${e.bytes} bytes`)})}onSend_pre=(e,s)=>{this.sends.set(e,s)};onSend_post=(e,s,t)=>{this.emit("send",e,this.sends.get(e),t+1),this.sends.delete(e)};onReceive_pre=(e,s,t)=>{const i=this.receiveSizes.get(e)??[];i.push(t+1),this.receiveSizes.set(e,i)};onReceive_post=(e,s)=>{const t=this.receiveSizes.get(e);this.emit("receive",e,s,t?.shift()??0),0===t?.length&&this.receiveSizes.delete(e)};emit(e,s,t,i){this.logger({direction:e,tag:s,values:this.includeValues?t:void 0,bytes:i,timestamp:Date.now()})}};
@@ -39,6 +39,9 @@ export declare enum PacketType {
39
39
  BOOLEANS = 14,
40
40
  /** TypeScript-side JSONUtil codec carried as opaque RAW bytes through reserved wire type 16. */
41
41
  JSON = 16,
42
- /** Hex bytes, e.g. 0xFFFFFF - the result will always be returned in lowercase. This can only hold 1 hex string, since it's the same as UBYTES but auto-parses. */
42
+ /**
43
+ * Hex bytes returned in lowercase.
44
+ * This carries one string and behaves like automatically parsed UBYTES.
45
+ */
43
46
  HEX = 17
44
47
  }
@@ -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;
@@ -42,23 +43,29 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
42
43
  readonly object: boolean;
43
44
  readonly client: boolean;
44
45
  processReceive: (data: Uint8Array, validationResult: any) => any;
45
- processSend: (data: any[]) => Promise<Uint8Array>;
46
+ processSend: (data: any[]) => Uint8Array | Promise<Uint8Array>;
46
47
  validate: (data: Uint8Array) => Promise<[Uint8Array, boolean]>;
47
48
  customValidator: ((socket: SonicWSConnection, ...values: any[]) => boolean) | null;
48
49
  lastReceived: Record<number, any>;
49
50
  lastSent: Record<number, number | bigint>;
50
51
  private quantizationErrors;
52
+ private readonly recordValues?;
51
53
  constructor(tag: string, schema: PacketSchema<T>, customValidator: ValidatorFunction, enabled: boolean, client: boolean);
54
+ private static compileRecordValues;
52
55
  private assertRecord;
53
56
  private construct;
57
+ private logicalValue;
54
58
  private logical;
55
59
  /** Converts ergonomic application values into the existing positional wire model. */
56
60
  prepareSend(values: any[], stateKey?: number): any[];
57
61
  /** Clears error-feedback state for a disconnected sender. */
58
62
  clearQuantizationState(stateKey: number): void;
63
+ /** Expands this group variant into its negotiated boolean permutation. */
64
+ permutation(): Record<string, boolean> | undefined;
59
65
  /** Converts decoded positional data into schema objects and application-level numbers. */
60
66
  finishReceive(decoded: any): any;
61
67
  listen(value: Uint8Array, socket: SonicWSConnection | null): Promise<[processed: any, flatten: boolean] | string>;
68
+ /** Serializes this packet definition for schema negotiation. */
62
69
  serialize(): number[];
63
70
  private static readVarInts;
64
71
  static deserialize(data: Uint8Array, offset: number, client: boolean): [packet: Packet<any>, offset: number];
@@ -89,6 +96,7 @@ export declare class PacketSchema<T extends (PacketType | readonly PacketType[])
89
96
  parent: string;
90
97
  variant: string;
91
98
  isParent: boolean;
99
+ permutation?: string[];
92
100
  };
93
101
  constructorName?: string;
94
102
  replay: boolean;
@@ -99,6 +107,7 @@ export declare class PacketSchema<T extends (PacketType | readonly PacketType[])
99
107
  parent: string;
100
108
  variant: string;
101
109
  isParent: boolean;
110
+ permutation?: string[];
102
111
  }, constructorName?: string, replay?: boolean);
103
112
  testObject(packet: Packet<PacketType | readonly PacketType[]>): packet is Packet<PacketType[]>;
104
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"),h=require("../util/packets/ConstructorRegistry");class c{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={};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.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=async 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=async 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}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`);const i=this.fields.filter(e=>!Object.prototype.hasOwnProperty.call(t,e));if(i.length)throw new Error(`Packet "${this.tag}" is missing schema field(s): ${i.join(", ")}`);const a=Object.keys(t).filter(t=>!this.fields.includes(t));if(a.length)throw new Error(`Packet "${this.tag}" has unknown schema field(s): ${a.join(", ")}`);return this.fields.map(e=>t[e])}construct(t){return this.constructorName?new((0,h.resolvePacketConstructor)(this.constructorName))(t):t}logical(t,e,i=-1){const a=this.quantized?.scale;return t.map(t=>{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})}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`);if(i=t[0].flatMap(t=>this.assertRecord(t,"autoFlatten")),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}`)}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)=>this.construct(Object.fromEntries(this.fields.map((t,a)=>[t,e[a][i]]))))}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]]))))}return this.fields?this.construct(Object.fromEntries(this.fields.map((t,i)=>[t,e[i]]))):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("There was an error processing the packet! This is probably my fault... report at https://github.com/liwybloc/sonic-ws",t),"Error: "+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.map(t=>t.serialize()).flat()];return this.object?[...e,this.maxSize,...this.dataMax.map(i.convertVarInt).flat(),...this.dataMin.map(i.convertVarInt).flat(),...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,y,v,g]=(0,i.decompressBools)(a[n++]),[M,S]=(0,i.readVarInt)(a,n);n=M;const x=JSON.parse((new TextDecoder).decode(a.slice(n,n+=S))),z=Array.isArray(x.schema)?x.schema:void 0,N=x.quantized&&"number"==typeof x.quantized.scale?x.quantized:void 0,k="number"==typeof x.min?x.min:void 0,w="number"==typeof x.max?x.max:void 0,b=x.group&&"string"==typeof x.group.parent?x.group:void 0,P="string"==typeof x.constructor?x.constructor:void 0,E=a[n++],q=a[n++],O=[];for(let i=0;i<q;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))}O.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?O[g++]:t),S=new u(!0,M,m,r,e,-1,p,y,!1,E,-1,v,z,void 0,void 0,void 0,b,P,!0===x.replay);return[new c(d,S,null,!1,o),n-h]}const[j,A]=(0,i.readVarInt)(a,n);n=j;const[$,T]=(0,i.readVarInt)(a,n);n=$;const D=a[n++],V=D==s.PacketType.ENUMS?O[0]:D,J=new u(!1,V,m,T,A,-1,p,y,g,E,-1,v,z,N,k,w,b,P,!0===x.replay);return[new c(d,J,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}}exports.Packet=c;const l=(t,i)=>t instanceof e.EnumPackage?(i.push(t),s.PacketType.ENUMS):t;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,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=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=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;
@@ -4,6 +4,7 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
  import * as WS from 'ws';
7
+ import type { IncomingMessage } from 'node:http';
7
8
  import { SonicWSServer } from "./SonicWSServer";
8
9
  import { Packet } from "../packets/Packets";
9
10
  import { Connection } from "../Connection";
@@ -24,7 +25,9 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
24
25
  private pendingRequests;
25
26
  private responders;
26
27
  sessionId: string;
27
- constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number, sessionId: string, state: Record<string, unknown>);
28
+ /** Provides the HTTP upgrade request for authentication headers and cookies. */
29
+ readonly upgradeRequest: IncomingMessage;
30
+ constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number, sessionId: string, state: Record<string, unknown>, handshakeTimeoutMs: number, upgradeRequest: IncomingMessage);
28
31
  private parseData;
29
32
  private isControl;
30
33
  private routeMessage;
@@ -34,7 +37,8 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
34
37
  private isAsync;
35
38
  private listenLock;
36
39
  private packetQueue;
37
- private listenPacket;
40
+ private deliverPacket;
41
+ private releasePacketLock;
38
42
  private messageHandler;
39
43
  /**
40
44
  * Enables a packet for the client.
@@ -69,11 +73,16 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
69
73
  */
70
74
  send(tag: string, ...values: any[]): Promise<void>;
71
75
  sendVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
76
+ sendPermutation(parent: string, selection: readonly boolean[] | Record<string, boolean>, ...values: any[]): Promise<void>;
72
77
  /** Sends a validated server packet as an RPC request to this client. */
73
78
  request(tag: string, ...valuesAndOptions: any[]): Promise<any>;
74
79
  /** Registers the server-side responder for client requests using this packet tag. */
75
80
  respond(tag: string, handler: (...values: any[]) => any): void;
76
81
  sendSafe(tag: string, ...values: any[]): Promise<boolean>;
82
+ /** Drops this update before encoding when the transport is backpressured. */
83
+ sendVolatile(tag: string, ...values: any[]): Promise<boolean>;
84
+ /** Sends a packet without applying the volatile backpressure drop policy. */
85
+ sendReliable(tag: string, ...values: any[]): Promise<void>;
77
86
  /**
78
87
  * Broadcasts a packet to all other users connected
79
88
  * @param tag The tag to send
@@ -86,8 +95,11 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
86
95
  * @param values The values to send
87
96
  */
88
97
  broadcast(tag: string, ...values: any[]): void;
98
+ /** Adds this connection to a server-side room. */
89
99
  join(room: string): void;
100
+ /** Removes this connection from a server-side room. */
90
101
  leave(room: string): void;
102
+ /** Returns the server-side rooms currently assigned to this connection. */
91
103
  getRooms(): ReadonlySet<string>;
92
104
  broadcastRoom(room: string, tag: string, ...values: any[]): Promise<void>;
93
105
  /**
@@ -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;constructor(e,t,i,a,r,n,o,h){super(e,i,"Socket "+i,e.addEventListener.bind(e),e.removeEventListener.bind(e)),this.host=t,this.sessionId=o,this.state=h,this.handshakePacket=a;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",r),this.rater.registerRate("S",n),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.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(`⬇ (${this.id},${t.byteLength})`,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;const t=(0,r.decodeControl)(e);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.log("Closure cause",e),this.socket.close(a.CloseCodes.INVALID_PACKET,e)}isAsync(e){return this.asyncMap[e]}listenLock=!1;packetQueue=[];async listenPacket(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]});await this.callMiddleware("onReceive_post",s,"string"==typeof t?[t]:t[0]),a?r[0]=!1:this.listenLock=!1,0==i.length||this.messageHandler(i.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;if(c.rereference&&0==a.length){const e=c.lastReceived[this.id];return void 0===e?this.invalidPacket("No previous value to rereference"):void await this.listenPacket(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.listenPacket(e,i,o,r,h)}const l=await t.BatchHelper.unravelBatch(c,a,this);if("string"==typeof l)return this.invalidPacket(l);for(const e of l)r?h[0]=!0:this.listenLock=!0,await this.listenPacket(e,i,o,r,h)}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(`Tag "${String(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)}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}}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("WARN! Connection already closed when trying to send message!",this.id,(0,i.stringifyBuffer)(e)):this.rater.trigger("S")||(this.print&&console.log(`⬆ (${this.id},${e.byteLength})`,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;
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(`⬇ (${this.id},${t.byteLength})`,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(`⬆ (${this.id},${e.byteLength})`,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;
@@ -11,22 +11,30 @@ import { PacketType } from "../packets/PacketType";
11
11
  import { MiddlewareHolder, ServerMiddleware } from "../PacketProcessor";
12
12
  import type { SonicWSAdapter } from "./Adapter";
13
13
  export type SonicServerSettings = {
14
- /** If it should check for updates; defaults to true. */
14
+ /** Checks for protocol updates when enabled. Defaults to true. */
15
15
  readonly checkForUpdates?: boolean;
16
16
  /** If the rereference should use a 64 bit hash which is less prone to collision (1% after ~600 million) or a 32 bit hash. Defaults to true. */
17
17
  readonly bit64Hash?: boolean;
18
- /** Automatically serves the browser client at /SonicWS/bundle.js and /SonicWS/bundle.wasm. Defaults to true. */
18
+ /** Serves browser assets from `/SonicWS` when enabled. Defaults to true. */
19
19
  readonly serveBrowserClient?: boolean;
20
+ /** Limits a required application handshake. Defaults to 10 seconds. */
21
+ readonly handshakeTimeoutMs?: number;
22
+ /** Enables portable CONTROL heartbeats. Defaults to true. */
23
+ readonly heartbeat?: boolean;
24
+ /** Controls the heartbeat interval. Zero disables it. */
25
+ readonly heartbeatIntervalMs?: number;
26
+ /** Limits how long the server waits for any reply before terminating. */
27
+ readonly heartbeatTimeoutMs?: number;
20
28
  };
21
29
  /**
22
- * Sonic WS Server Options
30
+ * Configures a SonicWS server.
23
31
  */
24
32
  export type SonicServerOptions = {
25
- /** An array of packets the client can send and server can listen for; using CreatePacket(), CreateObjPacket(), and CreateEnumPacket() */
33
+ /** Declares packets accepted from clients. */
26
34
  readonly clientPackets?: PacketTypings;
27
- /** An array of packets the server can send and client can listen for; using CreatePacket(), CreateObjPacket(), and CreateEnumPacket() */
35
+ /** Declares packets sent to clients. */
28
36
  readonly serverPackets?: PacketTypings;
29
- /** Default WS Options */
37
+ /** Forwards options to the underlying `ws` server. */
30
38
  readonly websocketOptions?: WS.ServerOptions;
31
39
  readonly sonicServerSettings?: SonicServerSettings;
32
40
  readonly onSendError?: (error: unknown, context: {
@@ -40,6 +48,7 @@ export type SonicServerOptions = {
40
48
  readonly recovery?: {
41
49
  maxDisconnectionMs?: number;
42
50
  maxPackets?: number;
51
+ authorize?: (previousState: Record<string, unknown>, currentState: Record<string, unknown>, connection: SonicWSConnection) => boolean | Promise<boolean>;
43
52
  };
44
53
  };
45
54
  export type PacketTypings = readonly Packet<PacketType | readonly PacketType[]>[];
@@ -65,6 +74,7 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
65
74
  private readonly sessions;
66
75
  private readonly recoveryMaxDisconnectionMs;
67
76
  private readonly recoveryMaxPackets;
77
+ private readonly recoveryAuthorize?;
68
78
  /**
69
79
  * Initializes and hosts a websocket with sonic protocol
70
80
  * Rate limits can be set with wss.setClientRateLimit(x) and wss.setServerRateLimit(x); it is defaulted at 500/second per both
@@ -138,7 +148,7 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
138
148
  resumeSession(connection: SonicWSConnection, sessionId: string, lastSequence: number): Promise<void>;
139
149
  private broadcastInternal;
140
150
  broadcastTagged(tag: string, packetTag: string, ...values: any): Promise<void>;
141
- /** Sends a packet to every local and adapter-connected member of a room. */
151
+ /** Sends a packet to every local and adapter-connected room member. */
142
152
  broadcastRoom(room: string, packetTag: string, ...values: any[]): Promise<void>;
143
153
  /** Sends a room packet except to one connection. */
144
154
  broadcastRoomExcept(connection: SonicWSConnection, room: string, packetTag: string, ...values: any[]): Promise<void>;
@@ -146,6 +156,7 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
146
156
  broadcast(tag: string, ...values: any): Promise<void>;
147
157
  broadcastSafe(tag: string, ...values: any[]): Promise<boolean>;
148
158
  broadcastVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
159
+ broadcastPermutation(parent: string, selection: readonly boolean[] | Record<string, boolean>, ...values: any[]): Promise<void>;
149
160
  handleSendError(error: unknown, context: {
150
161
  packetTag: string;
151
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"),p=require("../PacketProcessor"),g=require("../util/packets/HashUtil"),v=require("../Connection"),w=require("../debug/DebugServer"),f=require("node:crypto"),b=require("../util/packets/ControlProtocol"),k=e=>{if(!Number.isFinite(e)||e<0)throw new Error("Rate limit must be a non-negative finite number.");return(e=Math.floor(e))>d.MAX_USHORT?(console.warn(`A rate limit above ${d.MAX_USHORT} is considered infinite.`),0):e};class m extends p.MiddlewareHolder{wss;availableIds=[];lastId=0;connectListeners=[];recoveredListeners=[];clientPackets;serverPackets;connections=[];connectionMap={};clientRateLimit=500;serverRateLimit=500;handshakePacket=null;tags=new Map;tagsInv=new Map;serverwideSendQueue=[!1,[],void 0];sendErrorHandler;adapter;serverId=(0,f.randomUUID)();sessions=new Map;recoveryMaxDisconnectionMs;recoveryMaxPackets;constructor(e){super();const{clientPackets:t=[],serverPackets:s=[],websocketOptions:r={}}=e;if(this.sendErrorHandler=e.onSendError,this.adapter=e.adapter,this.recoveryMaxDisconnectionMs=e.recovery?.maxDisconnectionMs??12e4,this.recoveryMaxPackets=e.recovery?.maxPackets??1e3,!Number.isFinite(this.recoveryMaxDisconnectionMs)||this.recoveryMaxDisconnectionMs<0)throw new Error("recovery.maxDisconnectionMs must be a non-negative finite number");if(!Number.isInteger(this.recoveryMaxPackets)||this.recoveryMaxPackets<0)throw new Error("recovery.maxPackets must be a non-negative integer");if(this.wss=new n.WebSocketServer(r),e.sonicServerSettings?.serveBrowserClient??1){const e=r.server??this.wss._server;e&&this.installBrowserAssetRoutes(e)}this.clientPackets=new c.PacketHolder(t),this.serverPackets=new c.PacketHolder(s),Promise.resolve(this.adapter?.start?.(this.serverId,e=>this.receiveAdapterBroadcast(e))).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",operation:"broadcast"}));const a=this.clientPackets.serialize(),i=this.serverPackets.serialize(),u=[...l.SERVER_SUFFIX_NUMS,l.VERSION],p=[...(0,d.convertVarInt)(a.length),...a,...i];(0,g.setHashFunc)(e.sonicServerSettings?.bit64Hash??!0),this.wss.on("connection",async e=>{const t=(0,f.randomUUID)(),s={state:{},rooms:new Set,sequence:0,frames:[],expiresAt:1/0};this.sessions.set(t,s);const r=new o.SonicWSConnection(e,this,this.generateSocketID(),this.handshakePacket,this.clientRateLimit,this.serverRateLimit,t,s.state);if(await this.callMiddleware("onClientConnect",r))return r.close(v.CloseCodes.MIDDLEWARE,"Connection blocked by middleware."),this.callMiddleware("onClientDisconnect",r,v.CloseCodes.MIDDLEWARE,Buffer.from("Connection blocked by middleware.")),this.availableIds.push(r.id),void this.sessions.delete(t);const n=(new TextEncoder).encode(t),a=new Uint8Array([...(0,d.convertVarInt)(r.id),...(0,d.convertVarInt)(n.length),...n,...p]);e.send([...u,...(0,h.deflateNative)(a)]),this.connections.push(r),this.connectionMap[r.id]=r,this.connectListeners.forEach(e=>e(r)),e.on("close",(e,t)=>{const s=new Set(this.tags.get(r)??[]);if(this.connections.splice(this.connections.indexOf(r),1),delete this.connectionMap[r.id],this.availableIds.push(r.id),this.tags.has(r)){for(const e of this.tags.get(r))this.tagsInv.get(e)?.delete(r);this.tags.delete(r)}Promise.resolve(this.adapter?.disconnect(r.id)).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",connection:r}));const n=this.sessions.get(r.sessionId);if(n){n.rooms=s,n.expiresAt=Date.now()+this.recoveryMaxDisconnectionMs;const e=setTimeout(()=>{const e=this.sessions.get(r.sessionId);e===n&&e.expiresAt<=Date.now()&&this.sessions.delete(r.sessionId)},this.recoveryMaxDisconnectionMs+1);e.unref?.()}this.callMiddleware("onClientDisconnect",r,e,t)})}),(e.sonicServerSettings?.checkForUpdates??1)&&fetch("https://raw.githubusercontent.com/liwybloc/sonic-ws/refs/heads/main/release/version").then(e=>e.text()).then(e=>{parseInt(e)>l.VERSION&&console.warn(`SonicWS is currently running outdated! (current: ${l.VERSION}, latest: ${e}) Update with "npm update sonic-ws"`)}).catch(e=>{console.error(e),console.warn("Could not check SonicWS version.")})}installBrowserAssetRoutes(e){const t=e.listeners("request");e.removeAllListeners("request"),e.on("request",(s,r)=>{const n=new URL(s.url??"/","http://localhost").pathname,o="/SonicWS/bundle.js"===n?["bundle.js","text/javascript; charset=utf-8"]:"/SonicWS/bundle.wasm"===n?["bundle.wasm","application/wasm"]:void 0;if(!o){for(const n of t)Reflect.apply(n,e,[s,r]);return}const c=(0,i.resolve)(__dirname,"../../../bundled",o[0]);(0,a.readFile)(c).then(e=>{r.writeHead(200,{"content-type":o[1],"content-length":e.byteLength,"cache-control":"public, max-age=3600"}),r.end(e)}).catch(e=>{r.writeHead(500,{"content-type":"text/plain; charset=utf-8"}),r.end(`Unable to load SonicWS browser asset: ${e instanceof Error?e.message:String(e)}`)})})}generateSocketID(){return 0==this.availableIds.length&&this.availableIds.push(this.lastId+1),this.lastId=this.availableIds.shift(),this.lastId}requireHandshake(e){if(!this.clientPackets.hasTag(e))throw new Error(`The client does not send "${e}" and so it cannot use it as a handshake!`);if(0!=this.clientPackets.getPacket(e).dataBatching)throw new Error(`The packet "${e}" is a batched packet, and cannot be used as a handshake!`);this.handshakePacket=e}setClientRateLimit(e){this.clientRateLimit=k(e)}setServerRateLimit(e){this.serverRateLimit=k(e)}enablePacket(e){this.clientPackets.getPacket(e).defaultEnabled=!0,this.connections.forEach(t=>t.enablePacket(e))}disablePacket(e){this.clientPackets.getPacket(e).defaultEnabled=!1,this.connections.forEach(t=>t.disablePacket(e))}on_connect(e){this.connectListeners.push(e)}on_recovered(e){this.recoveredListeners.push(e)}on_ready(e){this.wss.on("listening",e)}shutdown(e){this.sessions.clear(),Promise.resolve(this.adapter?.close?.()).catch(e=>this.handleSendError(e,{packetTag:"<adapter>",operation:"broadcast"})),this.wss.close(e)}async receiveAdapterBroadcast(e){e.origin!==this.serverId&&await this.broadcastInternal(e.packetTag,{type:"filter",filter:t=>!0===this.tags.get(t)?.has(e.room)&&t.id!==e.exceptConnectionId},e.values)}replayFrame(e,t){const s=this.sessions.get(e.sessionId);if(!s)return t;const r=++s.sequence,n=(0,b.encodeReplay)(r,t);return s.frames.push({sequence:r,data:n}),s.frames.length>this.recoveryMaxPackets&&s.frames.splice(0,s.frames.length-this.recoveryMaxPackets),n}async resumeSession(e,t,s){const r=this.sessions.get(t);if(!r||r.expiresAt<Date.now())return void e.raw_send((0,b.encodeResumed)(!1,0));this.sessions.delete(e.sessionId),e.sessionId=t,e.state=r.state,r.expiresAt=1/0,this.sessions.set(t,r);for(const t of r.rooms)this.join(e,t);const n=r.frames.filter(e=>e.sequence>s);for(const t of n)e.raw_send(t.data);e.raw_send((0,b.encodeResumed)(!0,n.length));for(const t of this.recoveredListeners)await t(e,n.length)}async broadcastInternal(e,t,s){let r;if("all"===t.type)r=this.connections;else if("tagged"===t.type){if(!this.tagsInv.has(t.tag))return;r=Array.from(this.tagsInv.get(t.tag))}else r=this.connections.filter(t.filter);if(await this.callMiddleware("onPacketBroadcast_pre",e,{recipients:r,...t},s))return;if(0===r.length)return;const[n,a,i]=await(0,u.processPacket)(this.serverPackets,e,s,this.serverwideSendQueue,-1);await this.callMiddleware("onPacketBroadcast_post",e,{recipients:r,...t},a,a.length)||r.forEach(e=>e.send_processed(n,a,i))}async broadcastTagged(e,t,...s){await this.broadcastInternal(t,{type:"tagged",tag:e},s)}async broadcastRoom(e,t,...s){await this.broadcastInternal(t,{type:"tagged",tag:e},s),await(this.adapter?.publish({origin:this.serverId,room:e,packetTag:t,values:s}))}async broadcastRoomExcept(e,t,s,...r){await this.broadcastInternal(s,{type:"filter",filter:s=>s!==e&&!0===this.tags.get(s)?.has(t)},r),await(this.adapter?.publish({origin:this.serverId,room:t,packetTag:s,values:r,exceptConnectionId:e.id}))}async broadcastFiltered(e,t,...s){await this.broadcastInternal(e,{type:"filter",filter:t},s)}async broadcast(e,...t){await this.broadcastInternal(e,{type:"all"},t)}async broadcastSafe(e,...t){try{return await this.broadcast(e,...t),!0}catch(t){return this.handleSendError(t,{packetTag:e,operation:"broadcast"}),!1}}broadcastVariant(e,t,...s){return this.broadcast(this.serverPackets.getVariantTag(e,t),...s)}handleSendError(e,t){this.sendErrorHandler?this.sendErrorHandler(e,t):console.error(`Failed to send packet "${t.packetTag}"`,e)}getConnected(){return this.connections}getSocket(e){return this.connectionMap[e]}closeSocket(e,t=1e3,s){this.getSocket(e).close(t,s)}tag(e,t,s=!0){this.tags.get(e)||this.tags.set(e,new Set),this.tagsInv.get(t)||this.tagsInv.set(t,new Set),s&&(this.tags.get(e).forEach(t=>{this.tagsInv.get(t)?.delete(e),Promise.resolve(this.adapter?.leave(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}),this.tags.get(e).clear()),this.tags.get(e).add(t),this.tagsInv.get(t).add(e),Promise.resolve(this.adapter?.join(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}join(e,t){if(!t)throw new Error("Room name cannot be empty");this.tag(e,t,!1)}leave(e,t){this.tags.get(e)?.delete(t);const s=this.tagsInv.get(t);s?.delete(e),0===s?.size&&this.tagsInv.delete(t),Promise.resolve(this.adapter?.leave(e.id,t)).catch(t=>this.handleSendError(t,{packetTag:"<adapter>",connection:e}))}rooms(e){return this.tags.get(e)??new Set}debugServer=null;OpenDebug(e={}){if(null!=this.debugServer)throw new Error("Attempted to open a debug server when one has already been opened.");this.debugServer=new w.DebugServer(this,e)}}exports.SonicWSServer=m;
7
+ var e,t=this&&this.__createBinding||(Object.create?function(e,t,s,r){void 0===r&&(r=s);var n=Object.getOwnPropertyDescriptor(t,s);n&&!("get"in n?!t.__esModule:n.writable||n.configurable)||(n={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,r,n)}:function(e,t,s,r){void 0===r&&(r=s),e[r]=t[s]}),s=this&&this.__setModuleDefault||(Object.create?function(e,t){Object.defineProperty(e,"default",{enumerable:!0,value:t})}:function(e,t){e.default=t}),r=this&&this.__importStar||(e=function(t){return e=Object.getOwnPropertyNames||function(e){var t=[];for(var s in e)Object.prototype.hasOwnProperty.call(e,s)&&(t[t.length]=s);return t},e(t)},function(r){if(r&&r.__esModule)return r;var n={};if(null!=r)for(var a=e(r),i=0;i<a.length;i++)"default"!==a[i]&&t(n,r,a[i]);return s(n,r),n});Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSServer=void 0;const n=r(require("ws")),a=require("node:fs/promises"),i=require("node:path"),o=require("./SonicWSConnection"),c=require("../util/packets/PacketHolder"),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)};
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.BatchHelper=void 0;const t=require("../BufferUtil"),e=require("../../../native/wrapper");exports.BatchHelper=class{batchInfo={};batchTimeouts={};batchedData={};conn;registerSendPackets(t,e){this.conn=e,t.getTags().forEach(e=>{const a=t.getPacket(e);if(0==a.dataBatching)return;const c=t.getKey(e);this.initiateBatch(c,a.dataBatching,a.gzipCompression)})}initiateBatch(t,e,a){this.batchedData[t]=[],this.batchInfo[t]=[e,a]}startBatch(a){const[c,s]=this.batchInfo[a];this.batchTimeouts[a]=this.conn.setInterval(()=>{if(0==this.batchedData[a].length)return;const c=(0,e.encodeNativeBatch)(this.batchedData[a],s);this.conn.raw_send((0,t.toPacketBuffer)(a,c)),this.batchedData[a]=[],delete this.batchTimeouts[a]},c)}batchPacket(t,e){this.batchedData[t].push(e),this.batchTimeouts[t]||this.startBatch(t)}static async unravelBatch(t,a,c){let s;try{s=(0,e.decodeNativeBatch)(a,t.gzipCompression,t.maxBatchSize)}catch(t){return"Invalid batch: "+t}const i=[];for(const e of s){const a=await t.listen(e,c);if("string"==typeof a)return"Batched packet: "+a;i.push(a)}return i}};
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.BatchHelper=void 0;const t=require("../BufferUtil"),e=require("../../../native/wrapper");exports.BatchHelper=class{batchInfo={};batchTimeouts={};batchedData={};conn;registerSendPackets(t,e){this.conn=e,t.getTags().forEach(e=>{const a=t.getPacket(e);if(0===a.dataBatching)return;const c=t.getKey(e);this.initiateBatch(c,a.dataBatching,a.gzipCompression)})}initiateBatch(t,e,a){this.batchedData[t]=[],this.batchInfo[t]=[e,a]}startBatch(a){const[c,i]=this.batchInfo[a];this.batchTimeouts[a]=this.conn.setInterval(()=>{if(0===this.batchedData[a].length)return;const c=(0,e.encodeNativeBatch)(this.batchedData[a],i);this.conn.raw_send((0,t.toPacketBuffer)(a,c)),this.batchedData[a]=[],delete this.batchTimeouts[a]},c)}batchPacket(t,e){this.batchedData[t].push(e),this.batchTimeouts[t]||this.startBatch(t)}static async unravelBatch(t,a,c){let i;try{i=(0,e.decodeNativeBatch)(a,t.gzipCompression,t.maxBatchSize)}catch(t){return`Invalid batch: ${String(t)}`}const s=[];for(const e of i){const a=await t.listen(e,c);if("string"==typeof a)return`Batched packet: ${a}`;s.push(a)}return s}};