sonic-ws 2.1.0 → 2.2.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 (43) hide show
  1. package/README.md +7 -6
  2. package/bundled/bundle.js +1 -1
  3. package/bundled/bundle.wasm +0 -0
  4. package/dist/index.d.ts +8 -0
  5. package/dist/index.js +7 -1
  6. package/dist/native/wasm/node/sonic_ws_core_bg.wasm +0 -0
  7. package/dist/native/wrapper.d.ts +3 -3
  8. package/dist/native/wrapper.js +1 -1
  9. package/dist/version.d.ts +1 -1
  10. package/dist/version.js +1 -1
  11. package/dist/ws/Connection.d.ts +2 -0
  12. package/dist/ws/Connection.js +1 -1
  13. package/dist/ws/client/core/ClientCore.d.ts +51 -1
  14. package/dist/ws/client/core/ClientCore.js +1 -1
  15. package/dist/ws/client/node/ClientNode.d.ts +7 -2
  16. package/dist/ws/client/node/ClientNode.js +1 -1
  17. package/dist/ws/packets/PacketType.d.ts +0 -2
  18. package/dist/ws/packets/PacketType.js +1 -1
  19. package/dist/ws/packets/Packets.d.ts +16 -1
  20. package/dist/ws/packets/Packets.js +1 -1
  21. package/dist/ws/server/Adapter.d.ts +21 -0
  22. package/dist/ws/server/Adapter.js +7 -0
  23. package/dist/ws/server/SonicWSConnection.d.ts +16 -1
  24. package/dist/ws/server/SonicWSConnection.js +1 -1
  25. package/dist/ws/server/SonicWSServer.d.ts +32 -2
  26. package/dist/ws/server/SonicWSServer.js +1 -1
  27. package/dist/ws/util/packets/ConstructorRegistry.d.ts +10 -0
  28. package/dist/ws/util/packets/ConstructorRegistry.js +7 -0
  29. package/dist/ws/util/packets/ControlProtocol.d.ts +47 -0
  30. package/dist/ws/util/packets/ControlProtocol.js +7 -0
  31. package/dist/ws/util/packets/PacketHolder.js +1 -1
  32. package/dist/ws/util/packets/PacketManifest.d.ts +17 -0
  33. package/dist/ws/util/packets/PacketManifest.js +7 -0
  34. package/dist/ws/util/packets/PacketUtils.d.ts +24 -6
  35. package/dist/ws/util/packets/PacketUtils.js +1 -1
  36. package/package.json +4 -4
  37. package/dist/native/wasm/pkg/README.md +0 -6
  38. package/dist/native/wasm/pkg/package.json +0 -19
  39. package/dist/native/wasm/pkg/sonic_ws_core.d.ts +0 -51
  40. package/dist/native/wasm/pkg/sonic_ws_core.js +0 -7
  41. package/dist/native/wasm/pkg/sonic_ws_core_bg.js +0 -7
  42. package/dist/native/wasm/pkg/sonic_ws_core_bg.wasm +0 -0
  43. package/dist/native/wasm/pkg/sonic_ws_core_bg.wasm.d.ts +0 -36
@@ -6,6 +6,19 @@
6
6
  import { PacketHolder } from "../../util/packets/PacketHolder";
7
7
  import { Connection } from "../../Connection";
8
8
  import { AsyncPQ, ClientPQ, PacketQueue } from "../../PacketProcessor";
9
+ export type ReconnectOptions = {
10
+ enabled?: boolean;
11
+ attempts?: number;
12
+ minDelayMs?: number;
13
+ maxDelayMs?: number;
14
+ jitter?: number;
15
+ };
16
+ type TransportBinding<T, K> = {
17
+ socket: T;
18
+ bufferHandler: (value: K) => Promise<Uint8Array>;
19
+ on: Function;
20
+ off: Function;
21
+ };
9
22
  export declare abstract class SonicWSCore<T extends {
10
23
  readyState: number;
11
24
  send: (u: Uint8Array<ArrayBufferLike>) => void;
@@ -22,7 +35,27 @@ export declare abstract class SonicWSCore<T extends {
22
35
  _timers: Record<number, [number, (closed: boolean) => void, boolean]>;
23
36
  private asyncData;
24
37
  private asyncMap;
38
+ private reconnectFactory?;
39
+ private reconnectOptions;
40
+ private reconnectAttempt;
41
+ private reconnectTimer?;
42
+ private intentionalClose;
43
+ private connectedOnce;
44
+ private reconnectingListeners;
45
+ private reconnectListeners;
46
+ private reconnectFailedListeners;
47
+ private nextRequestId;
48
+ private pendingRequests;
49
+ private responders;
50
+ private sessionId?;
51
+ private lastReplaySequence;
52
+ private recoveredListeners;
53
+ private pendingResumeSession?;
25
54
  constructor(ws: T, bufferHandler: (val: K) => Promise<Uint8Array>, on: Function, off: Function);
55
+ protected configureReconnect(factory: () => TransportBinding<T, K>, options?: ReconnectOptions): void;
56
+ private attachClientTransport;
57
+ private scheduleReconnect;
58
+ private beginReconnect;
26
59
  private reading;
27
60
  private readQueue;
28
61
  private serverKeyHandler;
@@ -34,6 +67,7 @@ export declare abstract class SonicWSCore<T extends {
34
67
  private enqueuePacket;
35
68
  private triggerNextPacket;
36
69
  private dataHandler;
70
+ private handleControl;
37
71
  private messageHandler;
38
72
  protected listen(tag: string, listener: (data: any[]) => void): void;
39
73
  private sendQueue;
@@ -44,6 +78,10 @@ export declare abstract class SonicWSCore<T extends {
44
78
  */
45
79
  send(tag: string, ...values: any[]): Promise<void>;
46
80
  sendVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
81
+ /** Sends a validated packet as an RPC request and waits for its response. */
82
+ request(tag: string, ...valuesAndOptions: any[]): Promise<any>;
83
+ /** Registers the client-side responder for server requests using this packet tag. */
84
+ respond(tag: string, handler: (...values: any[]) => any): void;
47
85
  sendSafe(tag: string, ...values: any[]): Promise<boolean>;
48
86
  /**
49
87
  * Listens for when the client connects
@@ -54,7 +92,18 @@ export declare abstract class SonicWSCore<T extends {
54
92
  * Listens for when the client closes
55
93
  * @param listener Callback on close with close event
56
94
  */
57
- on_close(listener: (event: CloseEvent) => void): void;
95
+ on_close(listener: (...args: any[]) => void): void;
96
+ on_reconnecting(listener: (event: {
97
+ attempt: number;
98
+ delayMs: number;
99
+ }) => void): void;
100
+ on_reconnect(listener: () => void): void;
101
+ on_reconnect_failed(listener: () => void): void;
102
+ on_recovered(listener: (event: {
103
+ recovered: boolean;
104
+ replayed: number;
105
+ }) => void): void;
106
+ close(code?: number, reason?: string | Buffer): void;
58
107
  /**
59
108
  * Listens to a packet from the server
60
109
  * @param tag The tag to listen for
@@ -62,3 +111,4 @@ export declare abstract class SonicWSCore<T extends {
62
111
  */
63
112
  on(tag: string, listener: (value: any[]) => void): void;
64
113
  }
114
+ export {};
@@ -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"),i=require("../../util/packets/PacketUtils"),a=require("../../../version"),r=require("../../packets/Packets"),n=require("../../util/packets/BatchHelper"),c=require("../../util/BufferUtil"),o=require("../../Connection"),h=require("../../util/StringUtil");class l extends o.Connection{preListen;clientPackets=new e.PacketHolder;serverPackets=new e.PacketHolder;pastKeys=!1;readyListeners=[];bufferHandler;_timers={};asyncData={};asyncMap={};constructor(e,t,s,i){super(e,-1,"LocalSocket",s,i),this.socket=e,this.preListen={},this.invalidPacket=this.invalidPacket.bind(this),this.serverKeyHandler=this.serverKeyHandler.bind(this),this.messageHandler=this.messageHandler.bind(this),this._on("message",this.serverKeyHandler),this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED);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]}),this.bufferHandler=t}reading=!1;readQueue=[];async serverKeyHandler(e){if(this.reading)return this.readQueue.push(e);this.reading=!0;const i=await this.bufferHandler(e);if(i.length<3||(0,h.as8String)(i.slice(0,3))!=a.SERVER_SUFFIX)throw this.close(1e3),new Error("The server requested is not a Sonic WS server.");const n=i[3];if(n!=a.VERSION)throw this.close(1e3),new Error(`Version mismatch: ${n>a.VERSION?"client":"server"} is outdated (server: ${n}, client: ${a.VERSION})`);const c=(0,s.inflateNative)(i.subarray(4,i.length)),[o,l]=(0,t.readVarInt)(c,0);this.id=l;const[d,u]=(0,t.readVarInt)(c,o),k=c.subarray(d,d+u);this.clientPackets.holdPackets(r.Packet.deserializeAll(k,!0));const v=c.subarray(d+u,c.length);this.serverPackets.holdPackets(r.Packet.deserializeAll(v,!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.readyListeners.forEach(e=>e()),this.readyListeners=null,this._off("message",this.serverKeyHandler),this._on("message",this.messageHandler),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,a,r){const n=this.listeners[t],c=this.serverPackets.getPacket(t),o=c.parent?this.listeners[c.parent]:void 0;if(!n&&!o)return console.warn("Warn: No listener for packet "+t),void await this.triggerNextPacket(s,a,r);if(n&&await(0,i.listenPacket)(e,n,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,a,r),await this.callMiddleware("onReceive_post",t,"string"==typeof e?[e]:e[0])}isAsync(e){return this.asyncMap[e]}async enqueuePacket(e,t,s,a,r){await(0,i.listenPacket)(e,t,this.invalidPacket),await this.triggerNextPacket(s,a,r)}async triggerNextPacket(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.dataHandler(e.shift())}async dataHandler(e){const t=e[0],s=e.slice(1),i=this.isAsync(t);let a,r,c;if(i?(c=this.asyncData[t],a=c[0],r=c[1]):(a=this.listenLock,r=this.packetQueue),a)return void r.push(e);i?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,i,c);if(0==h.dataBatching){const e=h.lastReceived[0]=await h.listen(s,null);return void this.listenPacket(e,o,r,i,c)}const l=await n.BatchHelper.unravelBatch(h,s,null);if("string"==typeof l)return this.invalidPacket(l);l.forEach(e=>this.listenPacket(e,o,r,i,c))}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,a,r]=await(0,i.processPacket)(this.clientPackets,e,t,this.sendQueue,0);0==r.dataBatching?this.raw_send((0,c.toPacketBuffer)(s,a)):this.batcher.batchPacket(s,a),await this.callMiddleware("onSend_post",e,a,a.length)}sendVariant(e,t,...s){return this.send(this.clientPackets.getVariantTag(e,t),...s)}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(e,t){if(this.socket.readyState!==WebSocket.OPEN)return this.preListen[e]||(this.preListen[e]=[]),void this.preListen[e].push(t);this.listen(e,t)}}exports.SonicWSCore=l;
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;
@@ -4,7 +4,10 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
  import WS from 'ws';
7
- import { SonicWSCore } from "../core/ClientCore";
7
+ import { ReconnectOptions, SonicWSCore } from "../core/ClientCore";
8
+ export type SonicConnectOptions = WS.ClientOptions & {
9
+ reconnect?: ReconnectOptions;
10
+ };
8
11
  /** Class to connect to a SonicWS server */
9
12
  export declare class SonicWS extends SonicWSCore<WS.WebSocket, Buffer> {
10
13
  /**
@@ -12,5 +15,7 @@ export declare class SonicWS extends SonicWSCore<WS.WebSocket, Buffer> {
12
15
  * @param url The url to connect to
13
16
  * @param options The websocket options
14
17
  */
15
- constructor(url: string, options?: WS.ClientOptions);
18
+ constructor(url: string, options?: WS.ClientOptions, reconnect?: ReconnectOptions);
19
+ /** Creates a client and resolves after WASM loading and schema negotiation. */
20
+ static connect(url: string, options?: SonicConnectOptions): Promise<SonicWS>;
16
21
  }
@@ -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 o=e(require("ws")),r=require("../core/ClientCore");class t extends r.SonicWSCore{constructor(e,r){const t=new o.default.WebSocket(e,r);super(t,e=>Promise.resolve(new Uint8Array(e)),t.on.bind(t),t.off.bind(t))}}exports.SonicWS=t;
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;
@@ -37,8 +37,6 @@ export declare enum PacketType {
37
37
  DOUBLES = 13,
38
38
  /** One or more true/false */
39
39
  BOOLEANS = 14,
40
- /** Consumes multiple keys to describe the value. E.g. if you want to send a boolean, this could take up 2 keys instead of sending 2 bytes. Currently W.I.P. */
41
- KEY_EFFECTIVE = 15,
42
40
  /** TypeScript-side JSONUtil codec carried as opaque RAW bytes through reserved wire type 16. */
43
41
  JSON = 16,
44
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. */
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- var S;Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketType=void 0,function(S){S[S.NONE=0]="NONE",S[S.RAW=1]="RAW",S[S.STRINGS_ASCII=2]="STRINGS_ASCII",S[S.STRINGS_UTF16=3]="STRINGS_UTF16",S[S.STRINGS=2]="STRINGS",S[S.ENUMS=4]="ENUMS",S[S.BYTES=5]="BYTES",S[S.UBYTES=6]="UBYTES",S[S.SHORTS=7]="SHORTS",S[S.USHORTS=8]="USHORTS",S[S.VARINT=9]="VARINT",S[S.UVARINT=10]="UVARINT",S[S.DELTAS=11]="DELTAS",S[S.FLOATS=12]="FLOATS",S[S.DOUBLES=13]="DOUBLES",S[S.BOOLEANS=14]="BOOLEANS",S[S.KEY_EFFECTIVE=15]="KEY_EFFECTIVE",S[S.JSON=16]="JSON",S[S.HEX=17]="HEX"}(S||(exports.PacketType=S={}));
7
+ var S;Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketType=void 0,function(S){S[S.NONE=0]="NONE",S[S.RAW=1]="RAW",S[S.STRINGS_ASCII=2]="STRINGS_ASCII",S[S.STRINGS_UTF16=3]="STRINGS_UTF16",S[S.STRINGS=2]="STRINGS",S[S.ENUMS=4]="ENUMS",S[S.BYTES=5]="BYTES",S[S.UBYTES=6]="UBYTES",S[S.SHORTS=7]="SHORTS",S[S.USHORTS=8]="USHORTS",S[S.VARINT=9]="VARINT",S[S.UVARINT=10]="UVARINT",S[S.DELTAS=11]="DELTAS",S[S.FLOATS=12]="FLOATS",S[S.DOUBLES=13]="DOUBLES",S[S.BOOLEANS=14]="BOOLEANS",S[S.JSON=16]="JSON",S[S.HEX=17]="HEX"}(S||(exports.PacketType=S={}));
@@ -32,6 +32,9 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
32
32
  readonly valueMax?: number;
33
33
  readonly parent?: string;
34
34
  readonly variant?: string;
35
+ readonly isParent: boolean;
36
+ readonly constructorName?: string;
37
+ readonly replay: boolean;
35
38
  readonly rateLimit: number;
36
39
  readonly async: boolean;
37
40
  readonly rereference: boolean;
@@ -47,6 +50,7 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
47
50
  private quantizationErrors;
48
51
  constructor(tag: string, schema: PacketSchema<T>, customValidator: ValidatorFunction, enabled: boolean, client: boolean);
49
52
  private assertRecord;
53
+ private construct;
50
54
  private logical;
51
55
  /** Converts ergonomic application values into the existing positional wire model. */
52
56
  prepareSend(values: any[], stateKey?: number): any[];
@@ -81,10 +85,21 @@ export declare class PacketSchema<T extends (PacketType | readonly PacketType[])
81
85
  };
82
86
  valueMin?: number;
83
87
  valueMax?: number;
88
+ group?: {
89
+ parent: string;
90
+ variant: string;
91
+ isParent: boolean;
92
+ };
93
+ constructorName?: string;
94
+ replay: boolean;
84
95
  constructor(object: boolean, type: ImpactType<T, ArguableType>, async: boolean, dataMin: ImpactType<T, number>, dataMax: ImpactType<T, number>, rateLimit: number, dontSpread: boolean, autoFlatten: boolean, rereference: boolean, dataBatching: number, maxBatchSize: number, gzipCompression: boolean, fields?: readonly string[], quantized?: {
85
96
  scale: number;
86
97
  trackError?: boolean;
87
- }, valueMin?: number, valueMax?: number);
98
+ }, valueMin?: number, valueMax?: number, group?: {
99
+ parent: string;
100
+ variant: string;
101
+ isParent: boolean;
102
+ }, constructorName?: string, replay?: boolean);
88
103
  testObject(packet: Packet<PacketType | readonly PacketType[]>): packet is Packet<PacketType[]>;
89
104
  }
90
105
  export {};
@@ -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"),h=require("../util/packets/JSONUtil");class o{defaultEnabled;tag;maxSize;minSize;type;enumData;dataMax;dataMin;dataBatching;maxBatchSize;dontSpread;autoFlatten;fields;quantized;valueMin;valueMax;parent;variant;rateLimit;async;rereference;gzipCompression;object;client;processReceive;processSend;validate;customValidator;lastReceived={};lastSent={};quantizationErrors={};constructor(t,e,i,a,r){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;const o=/^__(.+)\$([^$]+)$/.exec(t);if(this.parent=o?.[1],this.variant=o?.[2],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,h.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,h.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,h.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,h.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,h.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,h.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])}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)=>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)=>Object.fromEntries(this.fields.map((i,s)=>[i,e[a*t+s]])))}return this.fields?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[r,!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})),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,h){const c=n,d=a[n++],u=(0,r.as8String)(a.slice(n,n+=d)),[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))),w=a[n++],z=a[n++],k=[];for(let i=0;i<z;i++){const i=a[n++],s=(0,r.as8String)(a.slice(n,n+=i)),h=a[n++],o=[];for(let t=0;t<h;t++){const t=a[n++],i=a[n++],s=(0,r.as8String)(a.slice(n,n+=t));o.push(e.TYPE_CONVERSION_MAP[i](s))}k.push((0,t.DefineEnum)(s,o))}if(f){const t=a[n++],[e,i]=this.readVarInts(a,n,t);n=i;const[r,d]=this.readVarInts(a,n,t);n=d;const f=Array.from(a.slice(n,n+=t));let g=0;const M=f.map(t=>t==s.PacketType.ENUMS?k[g++]:t),S=new l(!0,M,m,r,e,-1,p,y,!1,w,-1,v,x.schema);return[new o(u,S,null,!1,h),n-c]}const[E,b]=(0,i.readVarInt)(a,n);n=E;const[N,P]=(0,i.readVarInt)(a,n);n=N;const O=a[n++],q=O==s.PacketType.ENUMS?k[0]:O,$=new l(!1,q,m,P,b,-1,p,y,g,w,-1,v,x.schema,x.quantized,x.min,x.max);return[new o(u,$,null,!1,h),n-c]}static deserializeAll(t,e){const i=[];let a=0;for(;a<t.length;){const[s,r]=this.deserialize(t,a,e);i.push(s),a+=r}return i}}exports.Packet=o;const c=(t,i)=>t instanceof e.EnumPackage?(i.push(t),s.PacketType.ENUMS):t;class l{type;enumData=[];dataMax;dataMin;dataBatching;maxBatchSize;rateLimit;dontSpread=!1;autoFlatten=!1;async=!1;rereference=!1;gzipCompression=!1;object;fields;quantized;valueMin;valueMax;constructor(t,e,i,a,s,r,n,h,o,l,d,u,p,m,f,y){this.object=t,this.async=i,this.dataMin=a,this.dataMax=s,this.rateLimit=r,this.dontSpread=n,this.autoFlatten=h,this.rereference=o,this.dataBatching=l,this.maxBatchSize=d,this.gzipCompression=u,this.fields=p?[...p]:void 0,this.quantized=m?{...m}:void 0,this.valueMin=f,this.valueMax=y,this.type=t?e.map(t=>c(t,this.enumData)):c(e,this.enumData)}testObject(t){return this.object}}exports.PacketSchema=l;
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;
@@ -0,0 +1,21 @@
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
+ export type AdapterBroadcast = {
7
+ origin: string;
8
+ room: string;
9
+ packetTag: string;
10
+ values: any[];
11
+ exceptConnectionId?: number;
12
+ };
13
+ /** Cross-process room membership and broadcast contract. */
14
+ export interface SonicWSAdapter {
15
+ start?(serverId: string, receiver: (message: AdapterBroadcast) => Promise<void>): void | Promise<void>;
16
+ publish(message: AdapterBroadcast): void | Promise<void>;
17
+ join(connectionId: number, room: string): void | Promise<void>;
18
+ leave(connectionId: number, room: string): void | Promise<void>;
19
+ disconnect(connectionId: number): void | Promise<void>;
20
+ close?(): void | Promise<void>;
21
+ }
@@ -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});
@@ -20,8 +20,15 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
20
20
  handshakeComplete: boolean;
21
21
  private asyncMap;
22
22
  private asyncData;
23
- constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number);
23
+ private nextRequestId;
24
+ private pendingRequests;
25
+ private responders;
26
+ sessionId: string;
27
+ constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number, sessionId: string, state: Record<string, unknown>);
24
28
  private parseData;
29
+ private isControl;
30
+ private routeMessage;
31
+ private handleControl;
25
32
  private handshakeHandler;
26
33
  private invalidPacket;
27
34
  private isAsync;
@@ -62,6 +69,10 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
62
69
  */
63
70
  send(tag: string, ...values: any[]): Promise<void>;
64
71
  sendVariant(parent: string, variant: string, ...values: any[]): Promise<void>;
72
+ /** Sends a validated server packet as an RPC request to this client. */
73
+ request(tag: string, ...valuesAndOptions: any[]): Promise<any>;
74
+ /** Registers the server-side responder for client requests using this packet tag. */
75
+ respond(tag: string, handler: (...values: any[]) => any): void;
65
76
  sendSafe(tag: string, ...values: any[]): Promise<boolean>;
66
77
  /**
67
78
  * Broadcasts a packet to all other users connected
@@ -75,6 +86,10 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
75
86
  * @param values The values to send
76
87
  */
77
88
  broadcast(tag: string, ...values: any[]): void;
89
+ join(room: string): void;
90
+ leave(room: string): void;
91
+ getRooms(): ReadonlySet<string>;
92
+ broadcastRoom(room: string, tag: string, ...values: any[]): Promise<void>;
78
93
  /**
79
94
  * Toggles printing all sent and received messages
80
95
  */
@@ -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"),a=require("../util/BufferUtil"),i=require("../Connection");class n extends i.Connection{host;print=!1;handshakePacket;handshakeLambda;messageLambda=e=>this.messageHandler(this.parseData(e));handshakedMessageLambda=e=>{const t=this.parseData(e);return null==t?this.socket.close(i.CloseCodes.INVALID_DATA):t[0]==this.handshakePacket?this.socket.close(i.CloseCodes.REPEATED_HANDSHAKE):void this.messageHandler(t)};rater;enabledPackets={};handshakeComplete=!1;asyncMap={};asyncData={};constructor(e,t,a,i,n,r){super(e,a,"Socket "+a,e.addEventListener.bind(e),e.removeEventListener.bind(e)),this.host=t,this.handshakePacket=i;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",r),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,a.stringifyBuffer)(t)),t.byteLength<1)return this.socket.close(i.CloseCodes.SMALL),null;const s=t[0],n=t.slice(1);if(!this.host.clientPackets.hasKey(s))return this.socket.close(i.CloseCodes.INVALID_KEY),null;const r=this.host.clientPackets.getTag(s);return this.enabledPackets[r]?this.rater.trigger("client"+s)?null:[r,n]:(this.socket.close(i.CloseCodes.DISABLED_PACKET),null)}handshakeHandler(e){const t=this.parseData(e);if(null==t)return this.socket.close(i.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(i.CloseCodes.INVALID_DATA)}invalidPacket(e){console.log("Closure cause",e),this.socket.close(i.CloseCodes.INVALID_PACKET,e)}isAsync(e){return this.asyncMap[e]}listenLock=!1;packetQueue=[];async listenPacket(t,s,a,i,n){if(this.closed)return;await(0,e.listenPacket)(t,this.listeners[s],this.invalidPacket);const r=this.host.clientPackets.getPacket(s);if("string"!=typeof t&&r.parent&&r.variant)for(const e of this.listeners[r.parent]??[])await e({variant:r.variant,payload:t[0]});await this.callMiddleware("onReceive_post",s,"string"==typeof t?[t]:t[0]),i?n[0]=!1:this.listenLock=!1,0==a.length||this.messageHandler(a.shift())}async messageHandler(e,s=!1){if(null==e)return;const[a,i]=e,n=this.isAsync(a);let r,h,c;if(n?(c=this.asyncData[a],r=c[0],h=c[1]):(r=this.listenLock,h=this.packetQueue),r)return void h.push(e);n?c[0]=!0:this.listenLock=!0;const o=this.host.clientPackets.getPacket(a);if(!s&&await this.callMiddleware("onReceive_pre",o.tag,i,i.length))return;if(o.rereference&&0==i.length){const e=o.lastReceived[this.id];return void 0===e?this.invalidPacket("No previous value to rereference"):void await this.listenPacket(e,a,h,n,c)}if(0==o.dataBatching){const e=await o.listen(i,this);return o.lastReceived[this.id]=e,void await this.listenPacket(e,a,h,n,c)}const l=await t.BatchHelper.unravelBatch(o,i,this);if("string"==typeof l)return this.invalidPacket(l);for(const e of l)n?c[0]=!0:this.listenLock=!0,await this.listenPacket(e,a,h,n,c)}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){this.rater.trigger("server"+e)||(0==s.dataBatching?this.raw_send((0,a.toPacketBuffer)(e,t)):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[a,i,n]=await(0,e.processPacket)(this.host.serverPackets,t,s,this.sendQueue,this.id);await this.callMiddleware("onSend_post",t,i,i.length)||this.send_processed(a,i,n)}sendVariant(e,t,...s){return this.send(this.host.serverPackets.getVariantTag(e,t),...s)}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)}togglePrint(){this.print=!this.print}raw_send(e){this.isClosed()?console.warn("WARN! Connection already closed when trying to send message!",this.id,(0,a.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,a.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;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;
@@ -9,6 +9,7 @@ import { PacketHolder } from "../util/packets/PacketHolder";
9
9
  import { Packet } from "../packets/Packets";
10
10
  import { PacketType } from "../packets/PacketType";
11
11
  import { MiddlewareHolder, ServerMiddleware } from "../PacketProcessor";
12
+ import type { SonicWSAdapter } from "./Adapter";
12
13
  export type SonicServerSettings = {
13
14
  /** If it should check for updates; defaults to true. */
14
15
  readonly checkForUpdates?: boolean;
@@ -33,6 +34,13 @@ export type SonicServerOptions = {
33
34
  connection?: SonicWSConnection;
34
35
  operation?: "broadcast";
35
36
  }) => void;
37
+ /** Optional cross-process adapter used for room membership and room broadcasts. */
38
+ readonly adapter?: SonicWSAdapter;
39
+ /** Bounded replay storage used by reconnecting clients. */
40
+ readonly recovery?: {
41
+ maxDisconnectionMs?: number;
42
+ maxPackets?: number;
43
+ };
36
44
  };
37
45
  export type PacketTypings = readonly Packet<PacketType | readonly PacketType[]>[];
38
46
  export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
@@ -40,6 +48,7 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
40
48
  private availableIds;
41
49
  private lastId;
42
50
  private connectListeners;
51
+ private recoveredListeners;
43
52
  clientPackets: PacketHolder;
44
53
  serverPackets: PacketHolder;
45
54
  connections: SonicWSConnection[];
@@ -47,10 +56,15 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
47
56
  private clientRateLimit;
48
57
  private serverRateLimit;
49
58
  private handshakePacket;
50
- tags: Map<SonicWSConnection, Set<String>>;
51
- tagsInv: Map<String, Set<SonicWSConnection>>;
59
+ tags: Map<SonicWSConnection, Set<string>>;
60
+ tagsInv: Map<string, Set<SonicWSConnection>>;
52
61
  private serverwideSendQueue;
53
62
  private readonly sendErrorHandler?;
63
+ private readonly adapter?;
64
+ private readonly serverId;
65
+ private readonly sessions;
66
+ private readonly recoveryMaxDisconnectionMs;
67
+ private readonly recoveryMaxPackets;
54
68
  /**
55
69
  * Initializes and hosts a websocket with sonic protocol
56
70
  * Rate limits can be set with wss.setClientRateLimit(x) and wss.setServerRateLimit(x); it is defaulted at 500/second per both
@@ -105,6 +119,8 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
105
119
  * @param runner Called when ready
106
120
  */
107
121
  on_connect(runner: (client: SonicWSConnection) => void): void;
122
+ /** Runs after a reconnecting client has restored its state and rooms. */
123
+ on_recovered(runner: (client: SonicWSConnection, replayed: number) => void | Promise<void>): void;
108
124
  /**
109
125
  * Listens for whenever the server is ready
110
126
  * @param runner Called when ready
@@ -115,8 +131,17 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
115
131
  * @param callback Called when server closes
116
132
  */
117
133
  shutdown(callback: (err?: Error) => void): void;
134
+ private receiveAdapterBroadcast;
135
+ /** Wraps and stores an opted-in packet for bounded replay. */
136
+ replayFrame(connection: SonicWSConnection, packetFrame: Uint8Array): Uint8Array;
137
+ /** Restores state, rooms, and missed replayable packets onto a replacement connection. */
138
+ resumeSession(connection: SonicWSConnection, sessionId: string, lastSequence: number): Promise<void>;
118
139
  private broadcastInternal;
119
140
  broadcastTagged(tag: string, packetTag: string, ...values: any): Promise<void>;
141
+ /** Sends a packet to every local and adapter-connected member of a room. */
142
+ broadcastRoom(room: string, packetTag: string, ...values: any[]): Promise<void>;
143
+ /** Sends a room packet except to one connection. */
144
+ broadcastRoomExcept(connection: SonicWSConnection, room: string, packetTag: string, ...values: any[]): Promise<void>;
120
145
  broadcastFiltered(tag: string, filter: (socket: SonicWSConnection) => boolean, ...values: any): Promise<void>;
121
146
  broadcast(tag: string, ...values: any): Promise<void>;
122
147
  broadcastSafe(tag: string, ...values: any[]): Promise<boolean>;
@@ -147,6 +172,11 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
147
172
  * @param replace If it should replace a previous tag; defaults to true. If using false, you can add multiple tags.
148
173
  */
149
174
  tag(socket: SonicWSConnection, tag: string, replace?: boolean): void;
175
+ /** Adds a connection to a server-side room without removing its other rooms. */
176
+ join(socket: SonicWSConnection, room: string): void;
177
+ /** Removes a connection from a server-side room. */
178
+ leave(socket: SonicWSConnection, room: string): void;
179
+ rooms(socket: SonicWSConnection): ReadonlySet<string>;
150
180
  private debugServer;
151
181
  /**
152
182
  * Opens a debug menu; this launches the browser and starts a subserver
@@ -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,n){void 0===n&&(n=s);var r=Object.getOwnPropertyDescriptor(t,s);r&&!("get"in r?!t.__esModule:r.writable||r.configurable)||(r={enumerable:!0,get:function(){return t[s]}}),Object.defineProperty(e,n,r)}:function(e,t,s,n){void 0===n&&(n=s),e[n]=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}),n=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(n){if(n&&n.__esModule)return n;var r={};if(null!=n)for(var a=e(n),i=0;i<a.length;i++)"default"!==a[i]&&t(r,n,a[i]);return s(r,n),r});Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSServer=void 0;const r=n(require("ws")),a=require("node:fs/promises"),i=require("node:path"),o=require("./SonicWSConnection"),c=require("../util/packets/PacketHolder"),l=require("../util/packets/CompressionUtil"),d=require("../../native/wrapper"),h=require("../../version"),u=require("../util/packets/PacketUtils"),g=require("../PacketProcessor"),p=require("../util/packets/HashUtil"),b=require("../Connection"),w=require("../debug/DebugServer"),v=e=>{if(!Number.isFinite(e)||e<0)throw new Error("Rate limit must be a non-negative finite number.");return(e=Math.floor(e))>l.MAX_USHORT?(console.warn(`A rate limit above ${l.MAX_USHORT} is considered infinite.`),0):e};class f extends g.MiddlewareHolder{wss;availableIds=[];lastId=0;connectListeners=[];clientPackets;serverPackets;connections=[];connectionMap={};clientRateLimit=500;serverRateLimit=500;handshakePacket=null;tags=new Map;tagsInv=new Map;serverwideSendQueue=[!1,[],void 0];sendErrorHandler;constructor(e){super();const{clientPackets:t=[],serverPackets:s=[],websocketOptions:n={}}=e;if(this.sendErrorHandler=e.onSendError,this.wss=new r.WebSocketServer(n),e.sonicServerSettings?.serveBrowserClient??1){const e=n.server??this.wss._server;e&&this.installBrowserAssetRoutes(e)}this.clientPackets=new c.PacketHolder(t),this.serverPackets=new c.PacketHolder(s);const a=this.clientPackets.serialize(),i=this.serverPackets.serialize(),u=[...h.SERVER_SUFFIX_NUMS,h.VERSION],g=[...(0,l.convertVarInt)(a.length),...a,...i];(0,p.setHashFunc)(e.sonicServerSettings?.bit64Hash??!0),this.wss.on("connection",async e=>{const t=new o.SonicWSConnection(e,this,this.generateSocketID(),this.handshakePacket,this.clientRateLimit,this.serverRateLimit);if(await this.callMiddleware("onClientConnect",t))return t.close(b.CloseCodes.MIDDLEWARE,"Connection blocked by middleware."),this.callMiddleware("onClientDisconnect",t,b.CloseCodes.MIDDLEWARE,Buffer.from("Connection blocked by middleware.")),void this.availableIds.push(t.id);const s=new Uint8Array([...(0,l.convertVarInt)(t.id),...g]);e.send([...u,...(0,d.deflateNative)(s)]),this.connections.push(t),this.connectionMap[t.id]=t,this.connectListeners.forEach(e=>e(t)),e.on("close",(e,s)=>{if(this.connections.splice(this.connections.indexOf(t),1),delete this.connectionMap[t.id],this.availableIds.push(t.id),this.tags.has(t)){for(const e of this.tags.get(t))this.tagsInv.get(e)?.delete(t);this.tags.delete(t)}this.callMiddleware("onClientDisconnect",t,e,s)})}),(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)>h.VERSION&&console.warn(`SonicWS is currently running outdated! (current: ${h.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,n)=>{const r=new URL(s.url??"/","http://localhost").pathname,o="/SonicWS/bundle.js"===r?["bundle.js","text/javascript; charset=utf-8"]:"/SonicWS/bundle.wasm"===r?["bundle.wasm","application/wasm"]:void 0;if(!o){for(const r of t)Reflect.apply(r,e,[s,n]);return}const c=(0,i.resolve)(__dirname,"../../../bundled",o[0]);(0,a.readFile)(c).then(e=>{n.writeHead(200,{"content-type":o[1],"content-length":e.byteLength,"cache-control":"public, max-age=3600"}),n.end(e)}).catch(e=>{n.writeHead(500,{"content-type":"text/plain; charset=utf-8"}),n.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=v(e)}setServerRateLimit(e){this.serverRateLimit=v(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_ready(e){this.wss.on("listening",e)}shutdown(e){this.wss.close(e)}async broadcastInternal(e,t,s){let n;if("all"===t.type)n=this.connections;else if("tagged"===t.type){if(!this.tagsInv.has(t.tag))return;n=Array.from(this.tagsInv.get(t.tag))}else n=this.connections.filter(t.filter);if(await this.callMiddleware("onPacketBroadcast_pre",e,{recipients:n,...t},s))return;if(0===n.length)return;const[r,a,i]=await(0,u.processPacket)(this.serverPackets,e,s,this.serverwideSendQueue,-1);await this.callMiddleware("onPacketBroadcast_post",e,{recipients:n,...t},a,a.length)||n.forEach(e=>e.send_processed(r,a,i))}async broadcastTagged(e,t,...s){await this.broadcastInternal(t,{type:"tagged",tag:e},s)}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)),this.tags.get(e).add(t),this.tagsInv.get(t).add(e)}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=f;
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;
@@ -0,0 +1,10 @@
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
+ export type PacketConstructor<T = any> = new (values: Record<string, any>) => T;
7
+ /** Registers a local class that packet schema metadata may reference by name. */
8
+ export declare function RegisterPacketConstructor<T extends PacketConstructor>(constructor: T): T;
9
+ /** Removes a registered packet class, primarily for tests and hot reload. */
10
+ export declare function UnregisterPacketConstructor(name: string): void;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2026 Lily (liwybloc)
3
+ * License-Identifier: LicenseRef-Lily-Personal-NonCommercial-2026
4
+ * See LICENSE for personal, non-commercial license terms.
5
+ */
6
+
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.RegisterPacketConstructor=function(t){const r=t.name;if(!r)throw new Error("Packet constructors must have a stable class name");const o=e.get(r);if(o&&o!==t)throw new Error(`A different packet constructor named "${r}" is already registered`);return e.set(r,t),t},exports.UnregisterPacketConstructor=function(t){e.delete(t)},exports.resolvePacketConstructor=function(t){const r=e.get(t);if(!r)throw new Error(`Packet constructor "${t}" is not registered locally. Call RegisterPacketConstructor(${t}) before decoding.`);return r};const e=new Map;
@@ -0,0 +1,47 @@
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
+ export declare const CONTROL_KEY = 0;
7
+ export declare enum ControlType {
8
+ REQUEST = 1,
9
+ RESPONSE = 2,
10
+ REPLAY = 3,
11
+ RESUME = 4,
12
+ RESUMED = 5
13
+ }
14
+ export type ControlRequest = {
15
+ type: ControlType.REQUEST;
16
+ id: number;
17
+ packetKey: number;
18
+ payload: Uint8Array;
19
+ };
20
+ export type ControlResponse = {
21
+ type: ControlType.RESPONSE;
22
+ id: number;
23
+ ok: boolean;
24
+ value: any;
25
+ };
26
+ export type ControlReplay = {
27
+ type: ControlType.REPLAY;
28
+ sequence: number;
29
+ payload: Uint8Array;
30
+ };
31
+ export type ControlResume = {
32
+ type: ControlType.RESUME;
33
+ sessionId: string;
34
+ lastSequence: number;
35
+ };
36
+ export type ControlResumed = {
37
+ type: ControlType.RESUMED;
38
+ recovered: boolean;
39
+ replayed: number;
40
+ };
41
+ export type ControlMessage = ControlRequest | ControlResponse | ControlReplay | ControlResume | ControlResumed;
42
+ export declare function encodeControlRequest(id: number, packetKey: number, payload: Uint8Array): Uint8Array;
43
+ export declare function encodeControlResponse(id: number, ok: boolean, value: any): Uint8Array;
44
+ export declare const encodeReplay: (sequence: number, payload: Uint8Array) => Uint8Array<ArrayBuffer>;
45
+ export declare function encodeResume(sessionId: string, lastSequence: number): Uint8Array;
46
+ export declare const encodeResumed: (recovered: boolean, replayed: number) => Uint8Array<ArrayBuffer>;
47
+ export declare function decodeControl(data: Uint8Array): ControlMessage;
@@ -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.encodeResumed=exports.encodeReplay=exports.ControlType=exports.CONTROL_KEY=void 0,exports.encodeControlRequest=function(r,o,n){return Uint8Array.from([exports.CONTROL_KEY,t.REQUEST,...(0,e.convertVarInt)(r),o,...n])},exports.encodeControlResponse=function(o,n,s){return Uint8Array.from([exports.CONTROL_KEY,t.RESPONSE,...(0,e.convertVarInt)(o),Number(n),...(0,r.compressJSON)(s)])},exports.encodeResume=function(r,o){const n=(new TextEncoder).encode(r);return Uint8Array.from([exports.CONTROL_KEY,t.RESUME,...(0,e.convertVarInt)(n.length),...n,...(0,e.convertVarInt)(o)])},exports.decodeControl=function(o){if(o[0]!==exports.CONTROL_KEY||o.length<3)throw new Error("Invalid SonicWS control frame");const n=o[1];switch(n){case t.REPLAY:{const[r,t]=(0,e.readVarInt)(o,2);return{type:n,sequence:t,payload:o.slice(r)}}case t.RESUME:{const[r,t]=(0,e.readVarInt)(o,2),s=r+t;if(s>o.length)throw new Error("Recovery frame has an invalid session id");const[c,a]=(0,e.readVarInt)(o,s);return{type:n,sessionId:(new TextDecoder).decode(o.slice(r,s)),lastSequence:a}}case t.RESUMED:{if(o.length<4)throw new Error("Invalid recovery result frame");const[r,t]=(0,e.readVarInt)(o,3);return{type:n,recovered:0!==o[2],replayed:t}}case t.REQUEST:{const[r,t]=(0,e.readVarInt)(o,2);if(r>=o.length)throw new Error("RPC request is missing its packet key");return{type:n,id:t,packetKey:o[r],payload:o.slice(r+1)}}case t.RESPONSE:{const[t,s]=(0,e.readVarInt)(o,2);if(t>=o.length)throw new Error("RPC response is missing its status");return{type:n,id:s,ok:0!==o[t],value:(0,r.decompressJSON)(o.slice(t+1))}}default:throw new Error(`Unknown SonicWS control frame type: ${n}`)}};const e=require("./CompressionUtil"),r=require("./JSONUtil");var t;exports.CONTROL_KEY=0,function(e){e[e.REQUEST=1]="REQUEST",e[e.RESPONSE=2]="RESPONSE",e[e.REPLAY=3]="REPLAY",e[e.RESUME=4]="RESUME",e[e.RESUMED=5]="RESUMED"}(t||(exports.ControlType=t={}));exports.encodeReplay=(r,o)=>Uint8Array.from([exports.CONTROL_KEY,t.REPLAY,...(0,e.convertVarInt)(r),...o]);exports.encodeResumed=(r,o)=>Uint8Array.from([exports.CONTROL_KEY,t.RESUMED,Number(r),...(0,e.convertVarInt)(o)]);