sonic-ws 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.setNativeCore=setNativeCore,exports.initializeWasmCore=initializeWasmCore,exports.loadNativeCore=loadNativeCore,exports.encodeNative=encodeNative,exports.decodeNative=decodeNative,exports.validateNative=validateNative,exports.encodeNativeObject=encodeNativeObject,exports.decodeNativeObject=decodeNativeObject,exports.validateNativeObject=validateNativeObject,exports.encodeNativeBatch=encodeNativeBatch,exports.decodeNativeBatch=decodeNativeBatch,exports.deflateNative=deflateNative,exports.inflateNative=inflateNative;const PacketType_1=require("../ws/packets/PacketType"),version_1=require("../version"),CDN_RELEASE="https://cdn.jsdelivr.net/gh/liwybloc/sonic-ws/release",CDN_WASM=`${CDN_RELEASE}/bundle.wasm`,CDN_VERSION=`${CDN_RELEASE}/version`;let loadedCore,wasmInitialization;function buffer(e){return"undefined"!=typeof Buffer?Buffer.from(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}function values(e){return Array.isArray(e)?[...e]:[e]}function assertNativeCore(e){const t=["encodeSigned","decodeSigned","encodeUnsigned","decodeUnsigned","encodeFloats","decodeFloats","encodeStrings","decodeStrings","encodeBooleans","decodeBooleans","decodeRaw","encodeHex","decodeHex","frameObject","unframeObject","encodeBatch","decodeBatch","deflateRaw","inflateRaw","validateEncoded","validateEnum","validateObject"].filter(t=>"function"!=typeof e[t]);if(t.length>0)throw new Error(`Invalid SonicWS codec module; missing: ${t.join(", ")}`)}function setNativeCore(e){assertNativeCore(e),loadedCore=e}function fetchUrl(e){return"string"==typeof e?e:e instanceof URL?e.href:e.url}async function isWasm(e){if(!e.ok)return!1;const t=new Uint8Array(await e.clone().arrayBuffer());return t.length>=4&&0===t[0]&&97===t[1]&&115===t[2]&&109===t[3]}async function fetchCdnWasm(e){const t=await e(CDN_VERSION,{cache:"no-store"});if(!t.ok)throw new Error(`Unable to verify SonicWS CDN protocol version (${t.status})`);const a=Number((await t.text()).trim());if(!Number.isInteger(a)||a!==version_1.VERSION)throw new Error(`SonicWS CDN protocol mismatch: expected ${version_1.VERSION}, received ${String(a)}`);const r=await e(CDN_WASM);if(!await isWasm(r))throw new Error("SonicWS CDN returned an invalid WASM module");return r}async function initializeBrowserWasm(){const e=window.fetch.bind(window);window.fetch=async(t,a)=>{const r=fetchUrl(t);if(r===CDN_WASM||!new URL(r,window.location.href).pathname.endsWith("/bundle.wasm"))return e(t,a);try{const r=await e(t,a);if(await isWasm(r))return r}catch{}return fetchCdnWasm(e)};try{return setNativeCore(await import("./wasm/pkg/sonic_ws_core.js")),loadedCore}finally{window.fetch=e}}function initializeWasmCore(){return loadedCore?Promise.resolve(loadedCore):wasmInitialization||(wasmInitialization=(async()=>{if("undefined"!=typeof window)return initializeBrowserWasm();const wasm=eval("require")("./wasm/node/sonic_ws_core.js");return setNativeCore(wasm),loadedCore})().finally(()=>{wasmInitialization=void 0}),wasmInitialization)}function loadNativeCore(){if(loadedCore)return loadedCore;if("undefined"!=typeof window)throw new Error("SonicWS codec is not initialized; await initializeWasmCore() in browsers.");try{const wasm=eval("require")("./wasm/node/sonic_ws_core.js");return assertNativeCore(wasm),loadedCore=wasm,wasm}catch(e){throw new Error(`Unable to load the packaged SonicWS Node WASM codec: ${e instanceof Error?e.message:String(e)}`)}}function enumIndex(e,t){const a=e.values.findIndex(e=>e===t||"number"==typeof e&&"number"==typeof t&&Number.isNaN(e)&&Number.isNaN(t));if(a<0)throw new Error(`Value ${String(t)} does not exist in enum ${e.tag}`);return a}function encodeNative(e,t,a,r=loadNativeCore()){switch(e){case PacketType_1.PacketType.NONE:if(null!=t&&(!Array.isArray(t)||0!==t.length))throw new TypeError("NONE only accepts null, undefined, or an empty array");return new Uint8Array(0);case PacketType_1.PacketType.RAW:case PacketType_1.PacketType.JSON:if(!(t instanceof Uint8Array||Array.isArray(t)))throw new TypeError((e===PacketType_1.PacketType.JSON?"JSON wire data":"RAW")+" requires a Uint8Array or number array");return t instanceof Uint8Array?buffer(t):Uint8Array.from(t);case PacketType_1.PacketType.BYTES:case PacketType_1.PacketType.SHORTS:case PacketType_1.PacketType.VARINT:case PacketType_1.PacketType.DELTAS:return r.encodeSigned(e,values(t));case PacketType_1.PacketType.UBYTES:case PacketType_1.PacketType.USHORTS:case PacketType_1.PacketType.UVARINT:return r.encodeUnsigned(e,values(t));case PacketType_1.PacketType.FLOATS:case PacketType_1.PacketType.DOUBLES:return r.encodeFloats(e,values(t));case PacketType_1.PacketType.STRINGS_ASCII:case PacketType_1.PacketType.STRINGS_UTF16:return r.encodeStrings(e,values(t));case PacketType_1.PacketType.BOOLEANS:return r.encodeBooleans(values(t));case PacketType_1.PacketType.HEX:{const e=Array.isArray(t)?t[0]:t;if("string"!=typeof e)throw new TypeError("HEX requires one string");return r.encodeHex(e)}case PacketType_1.PacketType.ENUMS:if(!a)throw new Error("ENUMS requires an EnumPackage");return Uint8Array.from(values(t).map(e=>enumIndex(a,e)));default:throw new Error(`Unknown packet type: ${e}`)}}function decodeNative(e,t,a=4294967295,r,n=loadNativeCore()){const o=buffer(t);switch(e){case PacketType_1.PacketType.NONE:if(0!==t.byteLength)throw new Error("NONE packet contains data");return;case PacketType_1.PacketType.RAW:case PacketType_1.PacketType.JSON:return n.decodeRaw(o);case PacketType_1.PacketType.BYTES:case PacketType_1.PacketType.SHORTS:case PacketType_1.PacketType.VARINT:case PacketType_1.PacketType.DELTAS:return n.decodeSigned(e,o);case PacketType_1.PacketType.UBYTES:case PacketType_1.PacketType.USHORTS:case PacketType_1.PacketType.UVARINT:return n.decodeUnsigned(e,o);case PacketType_1.PacketType.FLOATS:case PacketType_1.PacketType.DOUBLES:return n.decodeFloats(e,o);case PacketType_1.PacketType.STRINGS_ASCII:case PacketType_1.PacketType.STRINGS_UTF16:return n.decodeStrings(e,o);case PacketType_1.PacketType.BOOLEANS:return n.decodeBooleans(o,a);case PacketType_1.PacketType.HEX:return n.decodeHex(o);case PacketType_1.PacketType.ENUMS:if(!r)throw new Error("ENUMS requires an EnumPackage");return[...t].map(e=>{if(e>=r.values.length)throw new Error(`Enum index ${e} is out of range`);return r.values[e]});default:throw new Error(`Unknown packet type: ${e}`)}}function validateNative(e,t,a,r,n={},o=loadNativeCore()){if(e!==PacketType_1.PacketType.ENUMS)o.validateEncoded(e,buffer(t),a,r,n.compressed??!1,n.batched??!1,n.maxBatchSize);else{if(!n.enumData)throw new Error("ENUMS requires an EnumPackage");o.validateEnum(buffer(t),n.enumData.values.length,a,r)}}function encodeNativeObject(e,t,a=loadNativeCore()){if(t.length!==e.types.length)throw new Error("Object field count does not match schema");let r=0;const n=e.types.map((n,o)=>encodeNative(n,t[o],n===PacketType_1.PacketType.ENUMS?e.enumData?.[r++]:void 0,a));return a.frameObject(n)}function decodeNativeObject(e,t,a=loadNativeCore()){const r=a.unframeObject(buffer(t),e.types.length);let n=0;return r.map((t,r)=>decodeNative(e.types[r],t,e.dataMaxes[r],e.types[r]===PacketType_1.PacketType.ENUMS?e.enumData?.[n++]:void 0,a))}function validateNativeObject(e,t,a=loadNativeCore()){a.validateObject(buffer(t),[...e.types],[...e.dataMins],[...e.dataMaxes],(e.enumData??[]).map(e=>e.values.length))}function encodeNativeBatch(e,t,a=loadNativeCore()){return a.encodeBatch(e.map(buffer),t)}function decodeNativeBatch(e,t,a=0,r,n=loadNativeCore()){const o=Number.isFinite(a)&&a>0?a:0;return n.decodeBatch(buffer(e),t,o,r)}function deflateNative(e,t=loadNativeCore()){return t.deflateRaw(buffer(e))}function inflateNative(e,t,a=loadNativeCore()){return a.inflateRaw(buffer(e),t)}
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.setNativeCore=setNativeCore,exports.initializeWasmCore=initializeWasmCore,exports.loadNativeCore=loadNativeCore,exports.encodeNative=encodeNative,exports.decodeNative=decodeNative,exports.validateNative=validateNative,exports.encodeNativeObject=encodeNativeObject,exports.decodeNativeObject=decodeNativeObject,exports.validateNativeObject=validateNativeObject,exports.encodeNativeBatch=encodeNativeBatch,exports.decodeNativeBatch=decodeNativeBatch,exports.deflateNative=deflateNative,exports.inflateNative=inflateNative;const PacketType_1=require("../ws/packets/PacketType"),version_1=require("../version"),CDN_RELEASE="https://cdn.jsdelivr.net/gh/liwybloc/sonic-ws/release",CDN_WASM=`${CDN_RELEASE}/bundle.wasm`,CDN_VERSION=`${CDN_RELEASE}/version`;let loadedCore,wasmInitialization;function buffer(e){return"undefined"!=typeof Buffer?Buffer.from(e.buffer,e.byteOffset,e.byteLength):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}function values(e){return Array.isArray(e)?[...e]:[e]}function assertNativeCore(e){const t=["encodeSigned","decodeSigned","encodeUnsigned","decodeUnsigned","encodeFloats","decodeFloats","encodeStrings","decodeStrings","encodeBooleans","decodeBooleans","decodeRaw","encodeHex","decodeHex","frameObject","unframeObject","encodeBatch","decodeBatch","deflateRaw","inflateRaw","validateEncoded","validateEnum","validateObject"].filter(t=>"function"!=typeof e[t]);if(t.length>0)throw new Error(`Invalid SonicWS codec module; missing: ${t.join(", ")}`)}function setNativeCore(e){assertNativeCore(e),loadedCore=e}function fetchUrl(e){return"string"==typeof e?e:e instanceof URL?e.href:e.url}async function isWasm(e){if(!e.ok)return!1;const t=new Uint8Array(await e.clone().arrayBuffer());return t.length>=4&&0===t[0]&&97===t[1]&&115===t[2]&&109===t[3]}async function fetchCdnWasm(e){const t=await e(CDN_VERSION,{cache:"no-store"});if(!t.ok)throw new Error(`Unable to verify SonicWS CDN protocol version (${t.status})`);const a=Number((await t.text()).trim());if(!Number.isInteger(a)||a!==version_1.VERSION)throw new Error(`SonicWS CDN protocol mismatch: expected ${version_1.VERSION}, received ${String(a)}`);const r=await e(CDN_WASM);if(!await isWasm(r))throw new Error("SonicWS CDN returned an invalid WASM module");return r}async function initializeBrowserWasm(){const e=window.fetch.bind(window);window.fetch=async(t,a)=>{const r=fetchUrl(t);if(r===CDN_WASM||!new URL(r,window.location.href).pathname.endsWith("/bundle.wasm"))return e(t,a);try{const r=await e(t,a);if(await isWasm(r))return r}catch{}return fetchCdnWasm(e)};try{return setNativeCore(await import("./wasm/pkg/sonic_ws_core.js")),loadedCore}finally{window.fetch=e}}function initializeWasmCore(){return loadedCore?Promise.resolve(loadedCore):wasmInitialization||(wasmInitialization=(async()=>{if("undefined"!=typeof window)return initializeBrowserWasm();const wasm=eval("require")("./wasm/node/sonic_ws_core.js");return setNativeCore(wasm),loadedCore})().finally(()=>{wasmInitialization=void 0}),wasmInitialization)}function loadNativeCore(){if(loadedCore)return loadedCore;if("undefined"!=typeof window)throw new Error("SonicWS codec is not initialized; await initializeWasmCore() in browsers.");try{const wasm=eval("require")("./wasm/node/sonic_ws_core.js");return assertNativeCore(wasm),loadedCore=wasm,wasm}catch(e){const t=e instanceof Error?e.message:String(e);throw new Error(`Unable to load the packaged SonicWS Node WASM codec: ${t}`)}}function enumIndex(e,t){const a=e.values.findIndex(e=>e===t||"number"==typeof e&&"number"==typeof t&&Number.isNaN(e)&&Number.isNaN(t));if(a<0)throw new Error(`Value ${String(t)} does not exist in enum ${e.tag}`);return a}function encodeIntegerVarints(e,t){const a=values(e),r=new Uint8Array(5*a.length);let n=0;for(let e=0;e<a.length;e++){const o=a[e];if(!Number.isInteger(o)||!Number.isFinite(o))throw new TypeError((t?"VARINT":"UVARINT")+" requires integer values");if(!t&&(o<0||o>4294967295))throw new RangeError("UVARINT value must be between 0 and 4294967295");const c=t?Math.max(-2147483648,Math.min(2147483647,o)):o;let i=t?(c<<1^c>>31)>>>0:c;do{let e=i%128;i=Math.floor(i/128),0!==i&&(e|=128),r[n++]=e}while(0!==i)}return r.slice(0,n)}function encodeNative(e,t,a,r=loadNativeCore()){switch(e){case PacketType_1.PacketType.NONE:if(null!=t&&(!Array.isArray(t)||0!==t.length))throw new TypeError("NONE only accepts null, undefined, or an empty array");return new Uint8Array(0);case PacketType_1.PacketType.RAW:case PacketType_1.PacketType.JSON:if(!(t instanceof Uint8Array||Array.isArray(t))){const t=e===PacketType_1.PacketType.JSON?"JSON wire data":"RAW";throw new TypeError(`${t} requires a Uint8Array or number array`)}return t instanceof Uint8Array?buffer(t):Uint8Array.from(t);case PacketType_1.PacketType.BYTES:case PacketType_1.PacketType.SHORTS:case PacketType_1.PacketType.DELTAS:return r.encodeSigned(e,values(t));case PacketType_1.PacketType.VARINT:return encodeIntegerVarints(t,!0);case PacketType_1.PacketType.UBYTES:case PacketType_1.PacketType.USHORTS:return r.encodeUnsigned(e,values(t));case PacketType_1.PacketType.UVARINT:return encodeIntegerVarints(t,!1);case PacketType_1.PacketType.FLOATS:case PacketType_1.PacketType.DOUBLES:return r.encodeFloats(e,values(t));case PacketType_1.PacketType.STRINGS_ASCII:case PacketType_1.PacketType.STRINGS_UTF16:return r.encodeStrings(e,values(t));case PacketType_1.PacketType.BOOLEANS:return r.encodeBooleans(values(t));case PacketType_1.PacketType.HEX:{const e=Array.isArray(t)?t[0]:t;if("string"!=typeof e)throw new TypeError("HEX requires one string");return r.encodeHex(e)}case PacketType_1.PacketType.ENUMS:if(!a)throw new Error("ENUMS requires an EnumPackage");return Uint8Array.from(values(t).map(e=>enumIndex(a,e)));default:throw new Error(`Unknown packet type: ${e}`)}}function decodeNative(e,t,a=4294967295,r,n=loadNativeCore()){const o=buffer(t);switch(e){case PacketType_1.PacketType.NONE:if(0!==t.byteLength)throw new Error("NONE packet contains data");return;case PacketType_1.PacketType.RAW:case PacketType_1.PacketType.JSON:return n.decodeRaw(o);case PacketType_1.PacketType.BYTES:case PacketType_1.PacketType.SHORTS:case PacketType_1.PacketType.VARINT:case PacketType_1.PacketType.DELTAS:return n.decodeSigned(e,o);case PacketType_1.PacketType.UBYTES:case PacketType_1.PacketType.USHORTS:case PacketType_1.PacketType.UVARINT:return n.decodeUnsigned(e,o);case PacketType_1.PacketType.FLOATS:case PacketType_1.PacketType.DOUBLES:return n.decodeFloats(e,o);case PacketType_1.PacketType.STRINGS_ASCII:case PacketType_1.PacketType.STRINGS_UTF16:return n.decodeStrings(e,o);case PacketType_1.PacketType.BOOLEANS:return n.decodeBooleans(o,a);case PacketType_1.PacketType.HEX:return n.decodeHex(o);case PacketType_1.PacketType.ENUMS:if(!r)throw new Error("ENUMS requires an EnumPackage");return[...t].map(e=>{if(e>=r.values.length)throw new Error(`Enum index ${e} is out of range`);return r.values[e]});default:throw new Error(`Unknown packet type: ${e}`)}}function validateNative(e,t,a,r,n={},o=loadNativeCore()){if(e!==PacketType_1.PacketType.ENUMS)o.validateEncoded(e,buffer(t),a,r,n.compressed??!1,n.batched??!1,n.maxBatchSize);else{if(!n.enumData)throw new Error("ENUMS requires an EnumPackage");o.validateEnum(buffer(t),n.enumData.values.length,a,r)}}function encodeNativeObject(e,t,a=loadNativeCore()){if(t.length!==e.types.length)throw new Error("Object field count does not match schema");let r=0;const n=e.types.map((n,o)=>encodeNative(n,t[o],n===PacketType_1.PacketType.ENUMS?e.enumData?.[r++]:void 0,a));return a.frameObject(n)}function decodeNativeObject(e,t,a=loadNativeCore()){const r=a.unframeObject(buffer(t),e.types.length);let n=0;return r.map((t,r)=>decodeNative(e.types[r],t,e.dataMaxes[r],e.types[r]===PacketType_1.PacketType.ENUMS?e.enumData?.[n++]:void 0,a))}function validateNativeObject(e,t,a=loadNativeCore()){a.validateObject(buffer(t),[...e.types],[...e.dataMins],[...e.dataMaxes],(e.enumData??[]).map(e=>e.values.length))}function encodeNativeBatch(e,t,a=loadNativeCore()){if(!t){let t=0;for(const a of e){let e=a.byteLength;for(t+=e+1;e>=128;)t++,e=Math.floor(e/128)}const a=new Uint8Array(t);let r=0;for(const t of e){let e=t.byteLength;do{let t=e%128;e=Math.floor(e/128),0!==e&&(t|=128),a[r++]=t}while(0!==e);a.set(t,r),r+=t.byteLength}return a}return a.encodeBatch(e.map(buffer),t)}function decodeNativeBatch(e,t,a=0,r,n=loadNativeCore()){const o=Number.isFinite(a)&&a>0?a:0;return n.decodeBatch(buffer(e),t,o,r)}function deflateNative(e,t=loadNativeCore()){return t.deflateRaw(buffer(e))}function inflateNative(e,t,a=loadNativeCore()){return a.inflateRaw(buffer(e),t)}
@@ -72,8 +72,8 @@ export interface IConnection<T> extends IMiddlewareHolder<ConnectionMiddleware>
72
72
  }
73
73
  export declare abstract class Connection<T extends {
74
74
  readyState: number;
75
- send: (u: Uint8Array) => void;
76
- close: (c: number, d: string | undefined) => void;
75
+ send: (data: Uint8Array) => void;
76
+ close: (code: number, reason: string | undefined) => void;
77
77
  }, K> extends MiddlewareHolder<ConnectionMiddleware> implements IConnection<K> {
78
78
  protected listeners: Record<string, Array<(...data: any[]) => void>>;
79
79
  private rawSendListeners;
@@ -88,6 +88,8 @@ export declare abstract class Connection<T extends {
88
88
  id: number;
89
89
  /** Mutable application-owned state scoped to this connection. */
90
90
  state: Record<string, unknown>;
91
+ private volatileAtBytes;
92
+ private closeAtBytes;
91
93
  constructor(socket: T, id: number, name: string, addListener: Function, removeListener: Function);
92
94
  /** Rebinds this connection object to a replacement transport. */
93
95
  protected replaceTransport(socket: T, addListener: Function, removeListener: Function): void;
@@ -96,6 +98,14 @@ export declare abstract class Connection<T extends {
96
98
  clearTimeout(id: number): void;
97
99
  clearInterval(id: number): void;
98
100
  raw_send(data: Uint8Array): void;
101
+ /** Bytes currently queued by the underlying transport. */
102
+ getBufferedAmount(): number;
103
+ /** Configures volatile-drop and forced-close outbound buffer thresholds. */
104
+ setBackpressureLimits(options: {
105
+ volatileAtBytes?: number;
106
+ closeAtBytes?: number;
107
+ }): void;
108
+ protected canSendVolatile(): boolean;
99
109
  raw_onmessage(listener: (data: K) => void): void;
100
110
  raw_onsend(listener: (data: Uint8Array) => void): void;
101
111
  close(code?: number, reason?: string | Buffer): void;
@@ -112,6 +122,7 @@ export declare enum CloseCodes {
112
122
  REPEATED_HANDSHAKE = 4005,
113
123
  DISABLED_PACKET = 4006,
114
124
  MIDDLEWARE = 4007,
115
- MANUAL_SHUTDOWN = 4008
125
+ MANUAL_SHUTDOWN = 4008,
126
+ BACKPRESSURE = 4009
116
127
  }
117
128
  export declare function getClosureCause(id: number): string;
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.CloseCodes=exports.Connection=void 0,exports.getClosureCause=function(e){if(e>=4e3)return o[e]??"UNKNOWN";switch(e){case 1e3:return"NORMAL_CLOSURE";case 1001:return"GOING_AWAY";case 1002:return"PROTOCOL_ERROR";case 1003:return"UNSUPPORTED_DATA";case 1006:return"ABNORMAL_CLOSURE";default:return"UNKNOWN"}};const e=require("./PacketProcessor"),t=require("./util/packets/BatchHelper");class s extends e.MiddlewareHolder{listeners;rawSendListeners=[];name;closed=!1;socket;_timers={};batcher;_on;_off;id;state={};constructor(e,s,o,r,i){super(),this.id=s,this.listeners={},this.name=o,this.socket=e,this.batcher=new t.BatchHelper,this._on=r,this._off=i,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0;for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0)}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}replaceTransport(e,t,s){this.socket=e,this._on=t,this._off=s,this.closed=!1,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}setTimeout(e,t,s=!1){const o=setTimeout(()=>{e(),this.clearTimeout(o)},t);return this._timers[o]=[o,e,s],o}setInterval(e,t,s=!1){const o=setInterval(e,t);return this._timers[o]=[o,e,s],o}clearTimeout(e){clearTimeout(e),delete this._timers[e]}clearInterval(e){this.clearTimeout(e)}raw_send(e){this.socket.send(e);for(const t of this.rawSendListeners)t(e)}raw_onmessage(e){this._on("message",e)}raw_onsend(e){this.rawSendListeners.push(e)}close(e=1e3,t){this.closed=!0,this.socket.close(e,t?.toString())}isClosed(){return this.closed||this.socket.readyState==WebSocket.CLOSED}async setName(e){await this.callMiddleware("onNameChange",e)||(this.name=e)}getName(){return this.name}}var o;exports.Connection=s,function(e){e[e.RATELIMIT=4e3]="RATELIMIT",e[e.SMALL=4001]="SMALL",e[e.INVALID_KEY=4002]="INVALID_KEY",e[e.INVALID_PACKET=4003]="INVALID_PACKET",e[e.INVALID_DATA=4004]="INVALID_DATA",e[e.REPEATED_HANDSHAKE=4005]="REPEATED_HANDSHAKE",e[e.DISABLED_PACKET=4006]="DISABLED_PACKET",e[e.MIDDLEWARE=4007]="MIDDLEWARE",e[e.MANUAL_SHUTDOWN=4008]="MANUAL_SHUTDOWN"}(o||(exports.CloseCodes=o={}));
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.CloseCodes=exports.Connection=void 0,exports.getClosureCause=function(e){if(e>=4e3)return i[e]??"UNKNOWN";switch(e){case 1e3:return"NORMAL_CLOSURE";case 1001:return"GOING_AWAY";case 1002:return"PROTOCOL_ERROR";case 1003:return"UNSUPPORTED_DATA";case 1006:return"ABNORMAL_CLOSURE";default:return"UNKNOWN"}};const e=require("./PacketProcessor"),t=require("./util/packets/BatchHelper");class s extends e.MiddlewareHolder{listeners;rawSendListeners=[];name;closed=!1;socket;_timers={};batcher;_on;_off;id;state={};volatileAtBytes=1048576;closeAtBytes=16777216;constructor(e,s,i,o,r){super(),this.id=s,this.listeners={},this.name=i,this.socket=e,this.batcher=new t.BatchHelper,this._on=o,this._off=r,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0;for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0)}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}replaceTransport(e,t,s){this.socket=e,this._on=t,this._off=s,this.closed=!1,this._on("close",()=>{this.callMiddleware("onStatusChange",WebSocket.CLOSED),this.closed=!0}),this._on("open",()=>this.callMiddleware("onStatusChange",WebSocket.OPEN))}setTimeout(e,t,s=!1){const i=setTimeout(()=>{e(),this.clearTimeout(i)},t);return this._timers[i]=[i,e,s],i}setInterval(e,t,s=!1){const i=setInterval(e,t);return this._timers[i]=[i,e,s],i}clearTimeout(e){clearTimeout(e),delete this._timers[e]}clearInterval(e){this.clearTimeout(e)}raw_send(e){if(this.getBufferedAmount()>=this.closeAtBytes)throw this.close(i.BACKPRESSURE,"Outbound buffer exceeded the configured limit"),new Error("SonicWS outbound backpressure limit exceeded");this.socket.send(e);for(const t of this.rawSendListeners)t(e)}getBufferedAmount(){const e=this.socket.bufferedAmount;return"number"==typeof e&&Number.isFinite(e)?e:0}setBackpressureLimits(e){const t=e.volatileAtBytes??this.volatileAtBytes,s=e.closeAtBytes??this.closeAtBytes;if(!Number.isFinite(t)||!Number.isFinite(s)||t<0||s<=0||t>s)throw new Error("Invalid SonicWS backpressure limits");this.volatileAtBytes=t,this.closeAtBytes=s}canSendVolatile(){return this.getBufferedAmount()<this.volatileAtBytes}raw_onmessage(e){this._on("message",e)}raw_onsend(e){this.rawSendListeners.push(e)}close(e=1e3,t){this.closed=!0,this.socket.close(e,t?.toString())}isClosed(){return this.closed||this.socket.readyState===WebSocket.CLOSED}async setName(e){await this.callMiddleware("onNameChange",e)||(this.name=e)}getName(){return this.name}}var i;exports.Connection=s,function(e){e[e.RATELIMIT=4e3]="RATELIMIT",e[e.SMALL=4001]="SMALL",e[e.INVALID_KEY=4002]="INVALID_KEY",e[e.INVALID_PACKET=4003]="INVALID_PACKET",e[e.INVALID_DATA=4004]="INVALID_DATA",e[e.REPEATED_HANDSHAKE=4005]="REPEATED_HANDSHAKE",e[e.DISABLED_PACKET=4006]="DISABLED_PACKET",e[e.MIDDLEWARE=4007]="MIDDLEWARE",e[e.MANUAL_SHUTDOWN=4008]="MANUAL_SHUTDOWN",e[e.BACKPRESSURE=4009]="BACKPRESSURE"}(i||(exports.CloseCodes=i={}));
@@ -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.MiddlewareHolder=void 0;exports.MiddlewareHolder=class{middlewares=[];addMiddleware(e){this.middlewares.push(e);const r=e;try{"function"==typeof r.init&&r.init(this)}catch(e){console.warn("Middleware init threw an error:",e)}}async callMiddleware(e,...r){let t=!1;for(const d of this.middlewares){const i=d[e];if(i)try{await i(...r)&&(t=!0)}catch(r){console.warn(`Middleware ${String(e)} threw an error:`,r)}}return t}};
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.MiddlewareHolder=void 0;exports.MiddlewareHolder=class{middlewares=[];addMiddleware(e){this.middlewares.push(e);const r=e;try{r.init?.(this)}catch(e){console.warn("Middleware init threw an error",e)}}async callMiddleware(e,...r){let d=!1;for(const a of this.middlewares){const t=a[e];if(t)try{await t.call(a,...r)&&(d=!0)}catch(r){console.warn(`Middleware ${String(e)} threw an error`,r)}}return d}};
@@ -5,7 +5,6 @@
5
5
  */
6
6
  import { PacketHolder } from "../../util/packets/PacketHolder";
7
7
  import { Connection } from "../../Connection";
8
- import { AsyncPQ, ClientPQ, PacketQueue } from "../../PacketProcessor";
9
8
  export type ReconnectOptions = {
10
9
  enabled?: boolean;
11
10
  attempts?: number;
@@ -19,11 +18,12 @@ type TransportBinding<T, K> = {
19
18
  on: Function;
20
19
  off: Function;
21
20
  };
22
- export declare abstract class SonicWSCore<T extends {
21
+ type ClientTransport = {
23
22
  readyState: number;
24
- send: (u: Uint8Array<ArrayBufferLike>) => void;
25
- close: (c: number, d: string | undefined) => void;
26
- }, K> extends Connection<T, K> {
23
+ send: (data: Uint8Array<ArrayBufferLike>) => void;
24
+ close: (code: number, reason: string | undefined) => void;
25
+ };
26
+ export declare abstract class SonicWSCore<T extends ClientTransport, K> extends Connection<T, K> {
27
27
  protected preListen: {
28
28
  [key: string]: Array<(data: any[]) => void>;
29
29
  } | null;
@@ -51,7 +51,11 @@ export declare abstract class SonicWSCore<T extends {
51
51
  private lastReplaySequence;
52
52
  private recoveredListeners;
53
53
  private pendingResumeSession?;
54
+ private handshakeTimeoutMs;
55
+ private handshakeTimer?;
54
56
  constructor(ws: T, bufferHandler: (val: K) => Promise<Uint8Array>, on: Function, off: Function);
57
+ protected configureHandshakeTimeout(milliseconds: number): void;
58
+ private armHandshakeTimeout;
55
59
  protected configureReconnect(factory: () => TransportBinding<T, K>, options?: ReconnectOptions): void;
56
60
  private attachClientTransport;
57
61
  private scheduleReconnect;
@@ -62,10 +66,9 @@ export declare abstract class SonicWSCore<T extends {
62
66
  private invalidPacket;
63
67
  private listenLock;
64
68
  private packetQueue;
65
- listenPacket(data: string | [any[], boolean], tag: string, packetQueue: PacketQueue<ClientPQ>, isAsync: boolean, asyncData: AsyncPQ<ClientPQ>): Promise<void>;
69
+ private deliverPacket;
70
+ private releasePacketLock;
66
71
  private isAsync;
67
- private enqueuePacket;
68
- private triggerNextPacket;
69
72
  private dataHandler;
70
73
  private handleControl;
71
74
  private messageHandler;
@@ -83,6 +86,10 @@ export declare abstract class SonicWSCore<T extends {
83
86
  /** Registers the client-side responder for server requests using this packet tag. */
84
87
  respond(tag: string, handler: (...values: any[]) => any): void;
85
88
  sendSafe(tag: string, ...values: any[]): Promise<boolean>;
89
+ /** Drops this update before encoding when the transport is backpressured. */
90
+ sendVolatile(tag: string, ...values: any[]): Promise<boolean>;
91
+ /** Sends a packet without applying the volatile backpressure drop policy. */
92
+ sendReliable(tag: string, ...values: any[]): Promise<void>;
86
93
  /**
87
94
  * Listens for when the client connects
88
95
  * @param listener Callback on connection
@@ -93,12 +100,16 @@ export declare abstract class SonicWSCore<T extends {
93
100
  * @param listener Callback on close with close event
94
101
  */
95
102
  on_close(listener: (...args: any[]) => void): void;
103
+ /** Listens for reconnect attempts before a replacement transport opens. */
96
104
  on_reconnecting(listener: (event: {
97
105
  attempt: number;
98
106
  delayMs: number;
99
107
  }) => void): void;
108
+ /** Listens for successful reconnection and schema negotiation. */
100
109
  on_reconnect(listener: () => void): void;
110
+ /** Listens for reconnect exhaustion. */
101
111
  on_reconnect_failed(listener: () => void): void;
112
+ /** Listens for the result of connection-state recovery. */
102
113
  on_recovered(listener: (event: {
103
114
  recovered: boolean;
104
115
  replayed: number;
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSCore=void 0;const e=require("../../util/packets/PacketHolder"),t=require("../../util/packets/CompressionUtil"),s=require("../../../native/wrapper"),n=require("../../util/packets/PacketUtils"),i=require("../../../version"),r=require("../../packets/Packets"),a=require("../../util/packets/BatchHelper"),c=require("../../util/BufferUtil"),o=require("../../Connection"),h=require("../../util/StringUtil"),l=require("../../util/packets/ControlProtocol");class d extends o.Connection{preListen;clientPackets=new e.PacketHolder;serverPackets=new e.PacketHolder;pastKeys=!1;readyListeners=[];bufferHandler;_timers={};asyncData={};asyncMap={};reconnectFactory;reconnectOptions={enabled:!1,attempts:1/0,minDelayMs:500,maxDelayMs:1e4,jitter:.25};reconnectAttempt=0;reconnectTimer;intentionalClose=!1;connectedOnce=!1;reconnectingListeners=[];reconnectListeners=[];reconnectFailedListeners=[];nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;lastReplaySequence=0;recoveredListeners=[];pendingResumeSession;constructor(e,t,s,n){super(e,-1,"LocalSocket",s,n),this.socket=e,this.preListen={},this.invalidPacket=this.invalidPacket.bind(this),this.serverKeyHandler=this.serverKeyHandler.bind(this),this.messageHandler=this.messageHandler.bind(this),this.attachClientTransport(),this.bufferHandler=t}configureReconnect(e,t={}){if(this.reconnectFactory=e,this.reconnectOptions={enabled:t.enabled??!0,attempts:t.attempts??1/0,minDelayMs:t.minDelayMs??500,maxDelayMs:t.maxDelayMs??1e4,jitter:t.jitter??.25},this.reconnectOptions.attempts<0||this.reconnectOptions.minDelayMs<0||this.reconnectOptions.maxDelayMs<this.reconnectOptions.minDelayMs)throw new Error("Invalid reconnect timing options");if(this.reconnectOptions.jitter<0||this.reconnectOptions.jitter>1)throw new Error("Reconnect jitter must be between 0 and 1")}attachClientTransport(){this._on("message",this.serverKeyHandler),this._on("close",(...e)=>{for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0);for(const e of this.clientPackets.getPackets())delete e.lastSent[0],e.clearQuantizationState(0);for(const e of this.serverPackets.getPackets())delete e.lastReceived[0];const t=e[0],s="number"==typeof t?t:t?.code;this.intentionalClose||1e3===s||this.scheduleReconnect()})}scheduleReconnect(){if(!this.reconnectFactory||!this.reconnectOptions.enabled||this.reconnectTimer)return;if(this.reconnectAttempt>=this.reconnectOptions.attempts)return void this.reconnectFailedListeners.forEach(e=>e());const e=++this.reconnectAttempt,t=Math.min(this.reconnectOptions.maxDelayMs,this.reconnectOptions.minDelayMs*2**(e-1)),s=t*this.reconnectOptions.jitter,n=Math.max(0,Math.round(t-s+Math.random()*s*2));this.reconnectingListeners.forEach(t=>t({attempt:e,delayMs:n})),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0,this.beginReconnect()},n)}beginReconnect(){try{const t=this.reconnectFactory();this.clientPackets=new e.PacketHolder,this.serverPackets=new e.PacketHolder,this.asyncData={},this.asyncMap={},this.reading=!1,this.readQueue=[],this.pastKeys=!1,this.preListen={},this.bufferHandler=t.bufferHandler,this.batcher=new a.BatchHelper,this.replaceTransport(t.socket,t.on,t.off),this.attachClientTransport()}catch{this.scheduleReconnect()}}reading=!1;readQueue=[];async serverKeyHandler(e){if(this.reading)return this.readQueue.push(e);this.reading=!0;const n=await this.bufferHandler(e);if(n.length<3||(0,h.as8String)(n.slice(0,3))!=i.SERVER_SUFFIX)throw this.close(1e3),new Error("The server requested is not a Sonic WS server.");const a=n[3];if(a!=i.VERSION)throw this.close(1e3),new Error(`Version mismatch: ${a>i.VERSION?"client":"server"} is outdated (server: ${a}, client: ${i.VERSION})`);const c=(0,s.inflateNative)(n.subarray(4,n.length)),o=this.sessionId,[d,u]=(0,t.readVarInt)(c,0);this.id=u;const[p,y]=(0,t.readVarInt)(c,d);this.sessionId=(new TextDecoder).decode(c.slice(p,p+y));const[k,f]=(0,t.readVarInt)(c,p+y),v=c.subarray(k,k+f);this.clientPackets.holdPackets(r.Packet.deserializeAll(v,!0));const g=c.subarray(k+f,c.length);this.serverPackets.holdPackets(r.Packet.deserializeAll(g,!0)),this.batcher.registerSendPackets(this.clientPackets,this);for(const e of this.serverPackets.getPackets()){const t=this.serverPackets.getKey(e.tag);this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}Object.keys(this.preListen??{}).forEach(e=>this.preListen[e].forEach(t=>{if(!this.serverPackets.hasTag(e))return console.error(new Error(`The server does not send the packet with tag "${e}"!`));this.listen(e,t)})),this.preListen=null,this.pastKeys=!0,this.connectedOnce&&(this.reconnectAttempt=0,this.reconnectListeners.forEach(e=>e())),this.connectedOnce=!0,this.readyListeners?.forEach(e=>e()),this.readyListeners=null,this._off("message",this.serverKeyHandler),this._on("message",this.messageHandler),o&&o!==this.sessionId&&(this.pendingResumeSession=o,this.raw_send((0,l.encodeResume)(o,this.lastReplaySequence))),this.readQueue.forEach(e=>this.messageHandler(e)),this.readQueue=[]}invalidPacket(e){throw console.error(e),new Error("An error occured with data from the server!! This is probably my fault.. make an issue at https://github.com/liwybloc/sonic-ws")}listenLock=!1;packetQueue=[];async listenPacket(e,t,s,i,r){const a=this.listeners[t],c=this.serverPackets.getPacket(t),o=c.parent?this.listeners[c.parent]:void 0;if(!a&&!o)return console.warn("Warn: No listener for packet "+t),void await this.triggerNextPacket(s,i,r);if(a&&await(0,n.listenPacket)(e,a,this.invalidPacket),"string"!=typeof e&&c.parent&&c.variant)for(const t of o??[])await t({variant:c.variant,payload:e[0]});await this.triggerNextPacket(s,i,r),await this.callMiddleware("onReceive_post",t,"string"==typeof e?[e]:e[0])}isAsync(e){return this.asyncMap[e]}async enqueuePacket(e,t,s,i,r){await(0,n.listenPacket)(e,t,this.invalidPacket),await this.triggerNextPacket(s,i,r)}async triggerNextPacket(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.dataHandler(e.shift())}async dataHandler(e){if(0===e[0])return this.handleControl(e);const t=e[0],s=e.slice(1),n=this.isAsync(t);let i,r,c;if(n?(c=this.asyncData[t],i=c[0],r=c[1]):(i=this.listenLock,r=this.packetQueue),i)return void r.push(e);n?c[0]=!0:this.listenLock=!0;const o=this.serverPackets.getTag(t),h=this.serverPackets.getPacket(o);if(await this.callMiddleware("onReceive_pre",h.tag,e,e.length))return;if(h.rereference&&0==s.length)return void 0===h.lastReceived[0]?this.invalidPacket("No previous value to rereference"):void this.listenPacket(h.lastReceived[0],o,r,n,c);if(0==h.dataBatching){const e=h.lastReceived[0]=await h.listen(s,null);return void this.listenPacket(e,o,r,n,c)}const l=await a.BatchHelper.unravelBatch(h,s,null);if("string"==typeof l)return this.invalidPacket(l);l.forEach(e=>this.listenPacket(e,o,r,n,c))}async handleControl(e){const t=(0,l.decodeControl)(e);if(t.type===l.ControlType.REPLAY){if(t.sequence<=this.lastReplaySequence)return;return this.lastReplaySequence=t.sequence,void await this.dataHandler(t.payload)}if(t.type===l.ControlType.RESUMED)return t.recovered&&this.pendingResumeSession&&(this.sessionId=this.pendingResumeSession),this.pendingResumeSession=void 0,this.recoveredListeners.forEach(e=>e({recovered:t.recovered,replayed:t.replayed})),void(t.recovered||(this.lastReplaySequence=0));if(t.type===l.ControlType.RESUME)throw new Error("A client cannot receive a recovery request");if(t.type===l.ControlType.RESPONSE){const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}try{const e=this.serverPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const n=await this.serverPackets.getPacket(e).listen(t.payload,null);if("string"==typeof n)throw new Error(n);const[i,r]=n,a=r?await s(...i):await s(i);this.raw_send((0,l.encodeControlResponse)(t.id,!0,a??null))}catch(e){this.raw_send((0,l.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}async messageHandler(e){const t=await this.bufferHandler(e);t.length<1||await this.dataHandler(t)}listen(e,t){if(!this.serverPackets.hasTag(e))return void console.log("Tag is not available on server: "+e);const s=this.serverPackets.resolveTag(e);(this.listeners[s]??=[]).push(t)}sendQueue=[!1,[],void 0];async send(e,...t){if(await this.callMiddleware("onSend_pre",e,t,Date.now(),performance.now()))return;const[s,i,r]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0);0==r.dataBatching?this.raw_send((0,c.toPacketBuffer)(s,i)):this.batcher.batchPacket(s,i),await this.callMiddleware("onSend_post",e,i,i.length)}sendVariant(e,t,...s){return this.send(this.clientPackets.getVariantTag(e,t),...s)}async request(e,...t){const s=t.at(-1),i=t.length>1&&s&&"object"==typeof s&&!Array.isArray(s)&&Object.keys(s).every(e=>"timeoutMs"===e)?t.pop():{},[r,a]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0),c=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((t,s)=>{const n=setTimeout(()=>{this.pendingRequests.delete(c),s(new Error(`RPC request "${e}" timed out`))},i.timeoutMs??5e3);this.pendingRequests.set(c,{resolve:t,reject:s,timer:n}),this.raw_send((0,l.encodeControlRequest)(c,r,a))})}respond(e,t){const s=this.serverPackets.resolveTag(e);this.responders.set(s,t)}async sendSafe(e,...t){try{return await this.send(e,...t),!0}catch(t){return console.error(`Failed to send packet "${e}"`,t),!1}}on_ready(e){this.pastKeys?e():(this.readyListeners??=[]).push(e)}on_close(e){this._on("close",e)}on_reconnecting(e){this.reconnectingListeners.push(e)}on_reconnect(e){this.reconnectListeners.push(e)}on_reconnect_failed(e){this.reconnectFailedListeners.push(e)}on_recovered(e){this.recoveredListeners.push(e)}close(e=1e3,t){this.intentionalClose=!0,this.reconnectTimer&&clearTimeout(this.reconnectTimer),super.close(e,t)}on(e,t){if(this.socket.readyState!==WebSocket.OPEN){const s=this.preListen??={};return s[e]||(s[e]=[]),void s[e].push(t)}this.listen(e,t)}}exports.SonicWSCore=d;
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSCore=void 0;const e=require("../../util/packets/PacketHolder"),t=require("../../util/packets/CompressionUtil"),s=require("../../../native/wrapper"),n=require("../../util/packets/PacketUtils"),i=require("../../../version"),r=require("../../packets/Packets"),a=require("../../util/packets/BatchHelper"),o=require("../../util/BufferUtil"),c=require("../../Connection"),h=require("../../util/StringUtil"),l=require("../../util/packets/ControlProtocol");class d extends c.Connection{preListen;clientPackets=new e.PacketHolder;serverPackets=new e.PacketHolder;pastKeys=!1;readyListeners=[];bufferHandler;_timers={};asyncData={};asyncMap={};reconnectFactory;reconnectOptions={enabled:!1,attempts:1/0,minDelayMs:500,maxDelayMs:1e4,jitter:.25};reconnectAttempt=0;reconnectTimer;intentionalClose=!1;connectedOnce=!1;reconnectingListeners=[];reconnectListeners=[];reconnectFailedListeners=[];nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;lastReplaySequence=0;recoveredListeners=[];pendingResumeSession;handshakeTimeoutMs=1e4;handshakeTimer;constructor(e,t,s,n){super(e,-1,"LocalSocket",s,n),this.socket=e,this.preListen={},this.invalidPacket=this.invalidPacket.bind(this),this.serverKeyHandler=this.serverKeyHandler.bind(this),this.messageHandler=this.messageHandler.bind(this),this.attachClientTransport(),this.armHandshakeTimeout(),this.bufferHandler=t}configureHandshakeTimeout(e){if(!Number.isFinite(e)||e<=0)throw new Error("Handshake timeout must be positive");this.handshakeTimeoutMs=e,this.armHandshakeTimeout()}armHandshakeTimeout(){this.handshakeTimer&&clearTimeout(this.handshakeTimer),this.handshakeTimer=setTimeout(()=>{this.pastKeys||this.socket.close(c.CloseCodes.INVALID_DATA,"SonicWS schema handshake timed out")},this.handshakeTimeoutMs)}configureReconnect(e,t={}){if(this.reconnectFactory=e,this.reconnectOptions={enabled:t.enabled??!0,attempts:t.attempts??1/0,minDelayMs:t.minDelayMs??500,maxDelayMs:t.maxDelayMs??1e4,jitter:t.jitter??.25},this.reconnectOptions.attempts<0||this.reconnectOptions.minDelayMs<0||this.reconnectOptions.maxDelayMs<this.reconnectOptions.minDelayMs)throw new Error("Invalid reconnect timing options");if(this.reconnectOptions.jitter<0||this.reconnectOptions.jitter>1)throw new Error("Reconnect jitter must be between 0 and 1")}attachClientTransport(){this._on("message",this.serverKeyHandler),this._on("close",(...e)=>{for(const[e,t,s]of Object.values(this._timers))this.clearTimeout(e),s&&t(!0);for(const e of this.clientPackets.getPackets())delete e.lastSent[0],e.clearQuantizationState(0);for(const e of this.serverPackets.getPackets())delete e.lastReceived[0];const t=e[0],s="number"==typeof t?t:t?.code;this.intentionalClose||1e3===s||this.scheduleReconnect()})}scheduleReconnect(){if(!this.reconnectFactory||!this.reconnectOptions.enabled||this.reconnectTimer)return;if(this.reconnectAttempt>=this.reconnectOptions.attempts)return void this.reconnectFailedListeners.forEach(e=>e());const e=++this.reconnectAttempt,t=Math.min(this.reconnectOptions.maxDelayMs,this.reconnectOptions.minDelayMs*2**(e-1)),s=t*this.reconnectOptions.jitter,n=Math.max(0,Math.round(t-s+Math.random()*s*2));this.reconnectingListeners.forEach(t=>t({attempt:e,delayMs:n})),this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=void 0,this.beginReconnect()},n)}beginReconnect(){try{const t=this.reconnectFactory();this.clientPackets=new e.PacketHolder,this.serverPackets=new e.PacketHolder,this.asyncData={},this.asyncMap={},this.reading=!1,this.readQueue=[],this.pastKeys=!1,this.preListen={},this.bufferHandler=t.bufferHandler,this.batcher=new a.BatchHelper,this.replaceTransport(t.socket,t.on,t.off),this.attachClientTransport(),this.armHandshakeTimeout()}catch{this.scheduleReconnect()}}reading=!1;readQueue=[];async serverKeyHandler(e){if(this.reading)return void this.readQueue.push(e);this.reading=!0;const n=await this.bufferHandler(e);if(n.length<3||(0,h.as8String)(n.slice(0,3))!==i.SERVER_SUFFIX)throw this.close(1e3),new Error("The requested server does not use the SonicWS protocol");const a=n[3];if(a!==i.VERSION)throw this.close(1e3),new Error(`Protocol mismatch: ${a>i.VERSION?"client":"server"} is outdated (server: ${a}, client: ${i.VERSION})`);const o=(0,s.inflateNative)(n.subarray(4)),c=this.sessionId,[d,u]=(0,t.readVarInt)(o,0);this.id=u;const[p,k]=(0,t.readVarInt)(o,d);this.sessionId=(new TextDecoder).decode(o.slice(p,p+k));const[m,v]=(0,t.readVarInt)(o,p+k),y=o.subarray(m,m+v);this.clientPackets.holdPackets(r.Packet.deserializeAll(y,!0));const f=o.subarray(m+v);this.serverPackets.holdPackets(r.Packet.deserializeAll(f,!0)),this.batcher.registerSendPackets(this.clientPackets,this);for(const e of this.serverPackets.getPackets()){const t=this.serverPackets.getKey(e.tag);this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}for(const[e,t]of Object.entries(this.preListen??{}))if(this.serverPackets.hasTag(e))for(const s of t)this.listen(e,s);else console.error(new Error(`The server does not define packet tag "${e}"`));this.preListen=null,this.pastKeys=!0,this.handshakeTimer&&clearTimeout(this.handshakeTimer),this.connectedOnce&&(this.reconnectAttempt=0,this.reconnectListeners.forEach(e=>e())),this.connectedOnce=!0,this.readyListeners?.forEach(e=>e()),this.readyListeners=null,this._off("message",this.serverKeyHandler),this._on("message",this.messageHandler),c&&c!==this.sessionId&&(this.pendingResumeSession=c,this.raw_send((0,l.encodeResume)(c,this.lastReplaySequence))),this.readQueue.forEach(e=>{this.messageHandler(e)}),this.readQueue=[]}invalidPacket(e){throw console.error(e),new Error("SonicWS rejected data from the server. Report reproducible codec failures at https://github.com/liwybloc/sonic-ws")}listenLock=!1;packetQueue=[];async deliverPacket(e,t,s,i,r){if(this.closed)return void this.releasePacketLock(s,i,r);const a=this.listeners[t],o=this.serverPackets.getPacket(t),c=o.parent?this.listeners[o.parent]:void 0;if(!a&&!c)return console.warn(`No listener is registered for packet "${t}"`),void this.releasePacketLock(s,i,r);if(a&&await(0,n.listenPacket)(e,a,this.invalidPacket),"string"!=typeof e&&o.parent&&o.variant)for(const t of c??[])await t({variant:o.variant,payload:e[0]});await this.callMiddleware("onReceive_post",t,"string"==typeof e?[e]:e[0]),this.releasePacketLock(s,i,r)}releasePacketLock(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.dataHandler(e.shift())}isAsync(e){return this.asyncMap[e]}async dataHandler(e){if(0===e[0])return void await this.handleControl(e);const t=e[0],s=e.slice(1),n=this.isAsync(t);let i,r,o;if(n?(o=this.asyncData[t],i=o[0],r=o[1]):(i=this.listenLock,r=this.packetQueue),i)return void r.push(e);n?o[0]=!0:this.listenLock=!0;const c=this.serverPackets.getTag(t),h=this.serverPackets.getPacket(c);if(await this.callMiddleware("onReceive_pre",h.tag,e,e.length))return void this.releasePacketLock(r,n,o);if(h.rereference&&0===s.length)return void 0===h.lastReceived[0]&&this.invalidPacket("No previous value to rereference"),void this.deliverPacket(h.lastReceived[0],c,r,n,o);if(0===h.dataBatching){const e=h.lastReceived[0]=await h.listen(s,null);return void this.deliverPacket(e,c,r,n,o)}const l=await a.BatchHelper.unravelBatch(h,s,null);"string"==typeof l&&this.invalidPacket(l),l.forEach(e=>{this.deliverPacket(e,c,r,n,o)})}async handleControl(e){let t;try{t=(0,l.decodeControl)(e)}catch{return void this.close(c.CloseCodes.INVALID_DATA,"Malformed SonicWS control frame")}switch(t.type){case l.ControlType.REPLAY:if(t.sequence<=this.lastReplaySequence)return;return this.lastReplaySequence=t.sequence,void await this.dataHandler(t.payload);case l.ControlType.RESUMED:return t.recovered&&this.pendingResumeSession&&(this.sessionId=this.pendingResumeSession),this.pendingResumeSession=void 0,this.recoveredListeners.forEach(e=>e({recovered:t.recovered,replayed:t.replayed})),void(t.recovered||(this.lastReplaySequence=0));case l.ControlType.RESUME:throw new Error("A client cannot receive a recovery request");case l.ControlType.RESPONSE:{const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}default:try{const e=this.serverPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const n=await this.serverPackets.getPacket(e).listen(t.payload,null);if("string"==typeof n)throw new Error(n);const[i,r]=n,a=r?await s(...i):await s(i);this.raw_send((0,l.encodeControlResponse)(t.id,!0,a??null))}catch(e){this.raw_send((0,l.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}}async messageHandler(e){const t=await this.bufferHandler(e);t.length<1||await this.dataHandler(t)}listen(e,t){if(!this.serverPackets.hasTag(e))return void console.warn(`Packet tag "${e}" is not available on the server`);const s=this.serverPackets.resolveTag(e);(this.listeners[s]??=[]).push(t)}sendQueue=[!1,[],void 0];async send(e,...t){if(await this.callMiddleware("onSend_pre",e,t,Date.now(),performance.now()))return;const[s,i,r]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0);0===r.dataBatching?this.raw_send((0,o.toPacketBuffer)(s,i)):this.batcher.batchPacket(s,i),await this.callMiddleware("onSend_post",e,i,i.length)}sendVariant(e,t,...s){return this.send(this.clientPackets.getVariantTag(e,t),...s)}async request(e,...t){const s=t.at(-1),i=t.length>1&&s&&"object"==typeof s&&!Array.isArray(s)&&Object.keys(s).every(e=>"timeoutMs"===e)?t.pop():{},[r,a]=await(0,n.processPacket)(this.clientPackets,e,t,this.sendQueue,0),o=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((t,s)=>{const n=setTimeout(()=>{this.pendingRequests.delete(o),s(new Error(`RPC request "${e}" timed out`))},i.timeoutMs??5e3);this.pendingRequests.set(o,{resolve:t,reject:s,timer:n}),this.raw_send((0,l.encodeControlRequest)(o,r,a))})}respond(e,t){const s=this.serverPackets.resolveTag(e);this.responders.set(s,t)}async sendSafe(e,...t){try{return await this.send(e,...t),!0}catch(t){return console.error(`Failed to send packet "${e}"`,t),!1}}async sendVolatile(e,...t){return!!this.canSendVolatile()&&(await this.send(e,...t),!0)}sendReliable(e,...t){return this.send(e,...t)}on_ready(e){this.pastKeys?e():(this.readyListeners??=[]).push(e)}on_close(e){this._on("close",e)}on_reconnecting(e){this.reconnectingListeners.push(e)}on_reconnect(e){this.reconnectListeners.push(e)}on_reconnect_failed(e){this.reconnectFailedListeners.push(e)}on_recovered(e){this.recoveredListeners.push(e)}close(e=1e3,t){this.intentionalClose=!0,this.reconnectTimer&&clearTimeout(this.reconnectTimer),super.close(e,t)}on(e,t){if(this.socket.readyState!==WebSocket.OPEN){const s=this.preListen??={};return s[e]??=[],void s[e].push(t)}this.listen(e,t)}}exports.SonicWSCore=d;
@@ -7,15 +7,16 @@ import WS from 'ws';
7
7
  import { ReconnectOptions, SonicWSCore } from "../core/ClientCore";
8
8
  export type SonicConnectOptions = WS.ClientOptions & {
9
9
  reconnect?: ReconnectOptions;
10
+ readyTimeoutMs?: number;
10
11
  };
11
- /** Class to connect to a SonicWS server */
12
+ /** Connects a Node.js process to a SonicWS server. */
12
13
  export declare class SonicWS extends SonicWSCore<WS.WebSocket, Buffer> {
13
14
  /**
14
15
  * Creates a connection to the url
15
16
  * @param url The url to connect to
16
17
  * @param options The websocket options
17
18
  */
18
- constructor(url: string, options?: WS.ClientOptions, reconnect?: ReconnectOptions);
19
+ constructor(url: string, options?: WS.ClientOptions, reconnect?: ReconnectOptions, readyTimeoutMs?: number);
19
20
  /** Creates a client and resolves after WASM loading and schema negotiation. */
20
21
  static connect(url: string, options?: SonicConnectOptions): Promise<SonicWS>;
21
22
  }
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- var e=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWS=void 0;const n=e(require("ws")),o=require("../core/ClientCore");class r extends o.SonicWSCore{constructor(e,o,r){const t=new n.default.WebSocket(e,o);t.on("error",()=>{}),super(t,e=>Promise.resolve(new Uint8Array(e)),t.on.bind(t),t.off.bind(t)),r?.enabled&&this.configureReconnect(()=>{const r=new n.default.WebSocket(e,o);return r.on("error",()=>{}),{socket:r,bufferHandler:e=>Promise.resolve(new Uint8Array(e)),on:r.on.bind(r),off:r.off.bind(r)}},r)}static async connect(e,n={}){const{reconnect:o,...t}=n,c=new r(e,t,o);return await new Promise((e,n)=>{c.on_ready(e),c.on_close((e,o)=>{c.clientPackets.getPackets()?.length||n(new Error(`SonicWS connection closed before ready (${e}: ${o})`))})}),c}}exports.SonicWS=r;
7
+ var e=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWS=void 0;const o=e(require("ws")),n=require("../core/ClientCore");class r extends n.SonicWSCore{constructor(e,n,r,t=1e4){const c=new o.default.WebSocket(e,n);c.on("error",()=>{}),super(c,e=>Promise.resolve(new Uint8Array(e)),c.on.bind(c),c.off.bind(c)),this.configureHandshakeTimeout(t),r?.enabled&&this.configureReconnect(()=>{const r=new o.default.WebSocket(e,n);return r.on("error",()=>{}),{socket:r,bufferHandler:e=>Promise.resolve(new Uint8Array(e)),on:r.on.bind(r),off:r.off.bind(r)}},r)}static async connect(e,o={}){const{reconnect:n,readyTimeoutMs:t,...c}=o,s=new r(e,c,n,t);return await new Promise((e,o)=>{s.on_ready(e),s.on_close((e,n)=>{s.clientPackets.getPackets()?.length||o(new Error(`SonicWS connection closed before ready (${e}: ${n})`))})}),s}}exports.SonicWS=r;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Copyright (c) 2026 Lily (liwybloc)
3
+ * License-Identifier: LicenseRef-Lily-Personal-NonCommercial-2026
4
+ * See LICENSE for personal, non-commercial license terms.
5
+ */
6
+ import type { ConnectionMiddleware } from "../PacketProcessor";
7
+ export type PacketLogEntry = {
8
+ direction: "send" | "receive";
9
+ tag: string;
10
+ values: unknown;
11
+ bytes: number;
12
+ timestamp: number;
13
+ };
14
+ export type PacketLoggerOptions = {
15
+ logger?: (entry: PacketLogEntry) => void;
16
+ includeValues?: boolean;
17
+ };
18
+ /** Readable packet logging implemented through the public middleware hooks. */
19
+ export declare class PacketLogger implements ConnectionMiddleware {
20
+ private readonly logger;
21
+ private readonly includeValues;
22
+ private readonly sends;
23
+ private readonly receiveSizes;
24
+ constructor(options?: PacketLoggerOptions);
25
+ onSend_pre: (tag: string, values: unknown[]) => void;
26
+ onSend_post: (tag: string, _data: Uint8Array, size: number) => void;
27
+ onReceive_pre: (tag: string, _data: Uint8Array, size: number) => void;
28
+ onReceive_post: (tag: string, values: unknown) => void;
29
+ private emit;
30
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Copyright (c) 2026 Lily (liwybloc)
3
+ * License-Identifier: LicenseRef-Lily-Personal-NonCommercial-2026
4
+ * See LICENSE for personal, non-commercial license terms.
5
+ */
6
+
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketLogger=void 0;exports.PacketLogger=class{logger;includeValues;sends=new Map;receiveSizes=new Map;constructor(e={}){this.includeValues=e.includeValues??!0,this.logger=e.logger??(e=>{const s="send"===e.direction?"→":"←";console.debug(`${s} ${e.tag}`,e.values,`${e.bytes} bytes`)})}onSend_pre=(e,s)=>{this.sends.set(e,s)};onSend_post=(e,s,t)=>{this.emit("send",e,this.sends.get(e),t+1),this.sends.delete(e)};onReceive_pre=(e,s,t)=>{const i=this.receiveSizes.get(e)??[];i.push(t+1),this.receiveSizes.set(e,i)};onReceive_post=(e,s)=>{const t=this.receiveSizes.get(e);this.emit("receive",e,s,t?.shift()??0),0===t?.length&&this.receiveSizes.delete(e)};emit(e,s,t,i){this.logger({direction:e,tag:s,values:this.includeValues?t:void 0,bytes:i,timestamp:Date.now()})}};
@@ -39,6 +39,9 @@ export declare enum PacketType {
39
39
  BOOLEANS = 14,
40
40
  /** TypeScript-side JSONUtil codec carried as opaque RAW bytes through reserved wire type 16. */
41
41
  JSON = 16,
42
- /** Hex bytes, e.g. 0xFFFFFF - the result will always be returned in lowercase. This can only hold 1 hex string, since it's the same as UBYTES but auto-parses. */
42
+ /**
43
+ * Hex bytes returned in lowercase.
44
+ * This carries one string and behaves like automatically parsed UBYTES.
45
+ */
43
46
  HEX = 17
44
47
  }
@@ -42,15 +42,18 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
42
42
  readonly object: boolean;
43
43
  readonly client: boolean;
44
44
  processReceive: (data: Uint8Array, validationResult: any) => any;
45
- processSend: (data: any[]) => Promise<Uint8Array>;
45
+ processSend: (data: any[]) => Uint8Array | Promise<Uint8Array>;
46
46
  validate: (data: Uint8Array) => Promise<[Uint8Array, boolean]>;
47
47
  customValidator: ((socket: SonicWSConnection, ...values: any[]) => boolean) | null;
48
48
  lastReceived: Record<number, any>;
49
49
  lastSent: Record<number, number | bigint>;
50
50
  private quantizationErrors;
51
+ private readonly recordValues?;
51
52
  constructor(tag: string, schema: PacketSchema<T>, customValidator: ValidatorFunction, enabled: boolean, client: boolean);
53
+ private static compileRecordValues;
52
54
  private assertRecord;
53
55
  private construct;
56
+ private logicalValue;
54
57
  private logical;
55
58
  /** Converts ergonomic application values into the existing positional wire model. */
56
59
  prepareSend(values: any[], stateKey?: number): any[];
@@ -59,6 +62,7 @@ export declare class Packet<T extends (PacketType | readonly PacketType[])> {
59
62
  /** Converts decoded positional data into schema objects and application-level numbers. */
60
63
  finishReceive(decoded: any): any;
61
64
  listen(value: Uint8Array, socket: SonicWSConnection | null): Promise<[processed: any, flatten: boolean] | string>;
65
+ /** Serializes this packet definition for schema negotiation. */
62
66
  serialize(): number[];
63
67
  private static readVarInts;
64
68
  static deserialize(data: Uint8Array, offset: number, client: boolean): [packet: Packet<any>, offset: number];
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketSchema=exports.Packet=void 0;const t=require("../util/enums/EnumHandler"),e=require("../util/enums/EnumType"),i=require("../util/packets/CompressionUtil"),a=require("../util/packets/PacketUtils"),s=require("./PacketType"),r=require("../util/StringUtil"),n=require("../../native/wrapper"),o=require("../util/packets/JSONUtil"),h=require("../util/packets/ConstructorRegistry");class c{defaultEnabled;tag;maxSize;minSize;type;enumData;dataMax;dataMin;dataBatching;maxBatchSize;dontSpread;autoFlatten;fields;quantized;valueMin;valueMax;parent;variant;isParent;constructorName;replay;rateLimit;async;rereference;gzipCompression;object;client;processReceive;processSend;validate;customValidator;lastReceived={};lastSent={};quantizationErrors={};constructor(t,e,i,a,r){if(this.tag=t,this.defaultEnabled=a,this.client=r,this.async=e.async,this.enumData=e.enumData,this.rateLimit=e.rateLimit,this.dontSpread=e.dontSpread,this.autoFlatten=e.autoFlatten,this.rereference=e.rereference,this.dataBatching=e.dataBatching,this.maxBatchSize=r?1/0:e.maxBatchSize,this.gzipCompression=e.gzipCompression,this.fields=e.fields,this.quantized=e.quantized?{...e.quantized,trackError:e.quantized.trackError??!0}:void 0,this.valueMin=e.valueMin,this.valueMax=e.valueMax,this.parent=e.group?.parent,this.variant=e.group?.variant,this.isParent=e.group?.isParent??!1,this.constructorName=e.constructorName,this.replay=e.replay,this.object=e.object,this.type=e.type,this.dataMax=e.dataMax,this.dataMin=e.dataMin,e.testObject(this)){this.maxSize=this.minSize=this.type.length;for(let t=0;t<this.type.length;t++)this.type[t]==s.PacketType.NONE&&(this.dataMax[t]=this.dataMin[t]=0);const t={types:this.type,dataMins:this.dataMin,dataMaxes:this.dataMax,enumData:this.enumData};this.processReceive=e=>(0,n.decodeNativeObject)(t,e).map((t,e)=>this.type[e]===s.PacketType.JSON?(0,o.decompressJSON)(t):t),this.processSend=async e=>{let i=0;const a=e.map((t,e)=>{if(this.type[e]===s.PacketType.JSON)return(0,o.compressJSON)(t);if(this.type[e]!==s.PacketType.ENUMS)return t;const a=this.enumData[i++];return(Array.isArray(t)?t:[t]).map(t=>{if(!Number.isInteger(t)||t<0||t>=a.values.length)throw new Error(`Invalid wrapped enum index: ${t}`);return a.values[t]})});return(0,n.encodeNativeObject)(t,a)},this.validate=async e=>{(0,n.validateNativeObject)(t,e);return(0,n.decodeNativeObject)(t,e).forEach((t,e)=>{if(this.type[e]!==s.PacketType.JSON)return;const i=(0,o.decompressJSON)(t),a=Array.isArray(i)?i.length:1;if(a<this.dataMin[e]||a>this.dataMax[e])throw new Error("JSON value count is outside schema limits")}),[e,!0]}}else{{this.maxSize=this.dataMax,this.minSize=this.dataMin,this.type==s.PacketType.NONE&&(this.dataMax=this.dataMin=0);const t=this.enumData[0];this.processReceive=e=>this.type===s.PacketType.JSON?(0,o.decompressJSON)(e):(0,n.decodeNative)(this.type,e,this.dataMax,t),this.processSend=async e=>{let i;if(this.type===s.PacketType.JSON)i=(0,o.compressJSON)(e);else if(this.type===s.PacketType.ENUMS)i=Uint8Array.from(e);else{const a=this.type===s.PacketType.RAW&&1===e.length&&e[0]instanceof Uint8Array?e[0]:e;i=(0,n.encodeNative)(this.type,a,t)}return this.gzipCompression&&0===this.dataBatching?(0,n.deflateNative)(i):i},this.validate=async e=>{const i=this.gzipCompression&&0===this.dataBatching?(0,n.inflateNative)(e):e;if(this.type===s.PacketType.JSON){const t=(0,o.decompressJSON)(i),e=Array.isArray(t)?t.length:1;if(e<this.dataMin||e>this.dataMax)throw new Error("JSON value count is outside schema limits")}else(0,n.validateNative)(this.type,i,this.dataMin,this.dataMax,{enumData:t});return[i,!0]}}}this.customValidator=i}assertRecord(t,e){if(!this.fields)throw new Error(`Packet "${this.tag}" ${e} requires schema`);if(null===t||"object"!=typeof t||Array.isArray(t))throw new Error(`Packet "${this.tag}" ${e} requires an object record`);const i=this.fields.filter(e=>!Object.prototype.hasOwnProperty.call(t,e));if(i.length)throw new Error(`Packet "${this.tag}" is missing schema field(s): ${i.join(", ")}`);const a=Object.keys(t).filter(t=>!this.fields.includes(t));if(a.length)throw new Error(`Packet "${this.tag}" has unknown schema field(s): ${a.join(", ")}`);return this.fields.map(e=>t[e])}construct(t){return this.constructorName?new((0,h.resolvePacketConstructor)(this.constructorName))(t):t}logical(t,e,i=-1){const a=this.quantized?.scale;return t.map(t=>{if("number"!=typeof t||!Number.isFinite(t))throw new Error(`Packet "${this.tag}" ${e} value must be a finite number`);const s="receive"===e&&a?t/a:t;if(void 0!==this.valueMin&&s<this.valueMin)throw new Error(`Packet "${this.tag}" value ${s} is below minimum ${this.valueMin}`);if(void 0!==this.valueMax&&s>this.valueMax)throw new Error(`Packet "${this.tag}" value ${s} exceeds maximum ${this.valueMax}`);if("send"===e&&a){const t=s*a+(this.quantized?.trackError?this.quantizationErrors[i]??0:0),e=Math.round(t);return this.quantized?.trackError&&(this.quantizationErrors[i]=t-e),e}return s})}prepareSend(t,e=-1){if(this.object){if(this.autoFlatten&&this.fields){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoTranspose requires one array of records`);const e=t[0].map(t=>this.assertRecord(t,"autoTranspose"));return this.fields.map((t,i)=>e.map(t=>t[i]))}return t}let i=t;if(this.autoFlatten){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoFlatten requires one array of records`);if(i=t[0].flatMap(t=>this.assertRecord(t,"autoFlatten")),this.fields&&i.length%this.fields.length!==0)throw new Error(`Packet "${this.tag}" flat value count must be divisible by schema length ${this.fields.length}`)}else this.fields&&1===t.length&&null!==t[0]&&"object"==typeof t[0]&&!Array.isArray(t[0])&&(i=this.assertRecord(t[0],"schema mapping"));return this.quantized||void 0!==this.valueMin||void 0!==this.valueMax?this.logical(i,"send",e):i}clearQuantizationState(t){delete this.quantizationErrors[t]}finishReceive(t){if(this.object){if(this.autoFlatten&&this.fields){const e=t,i=e[0]?.length??0;if(e.some(t=>t.length!==i))throw new Error(`Packet "${this.tag}" autoTranspose columns have different lengths`);return Array.from({length:i},(t,i)=>this.construct(Object.fromEntries(this.fields.map((t,a)=>[t,e[a][i]]))))}return this.autoFlatten?(0,a.UnFlattenData)(t):t}let e=Array.isArray(t)?t:[t];if((this.quantized||void 0!==this.valueMin||void 0!==this.valueMax)&&(e=this.logical(e,"receive")),this.autoFlatten){const t=this.fields.length;if(e.length%t!==0)throw new Error(`Packet "${this.tag}" flat value count ${e.length} is not divisible by schema length ${t}`);return Array.from({length:e.length/t},(i,a)=>this.construct(Object.fromEntries(this.fields.map((i,s)=>[i,e[a*t+s]]))))}return this.fields?this.construct(Object.fromEntries(this.fields.map((t,i)=>[t,e[i]]))):t}async listen(t,e){try{const[i,a]=await this.validate(t);if(!this.client&&!1===a)return"Invalid packet";const s=this.processReceive(i,a),r=this.finishReceive(s);if(null!=this.customValidator)if(this.dontSpread||this.fields){if(!this.customValidator(e,r))return"Didn't pass custom validator"}else if(!this.customValidator(e,...r))return"Didn't pass custom validator";return[this.isParent?{variant:"",payload:r}:r,!this.isParent&&!this.fields&&!this.dontSpread]}catch(t){return console.error("There was an error processing the packet! This is probably my fault... report at https://github.com/liwybloc/sonic-ws",t),"Error: "+t}}serialize(){const t=(new TextEncoder).encode(JSON.stringify({schema:this.fields,quantized:this.quantized,min:this.valueMin,max:this.valueMax,group:void 0!==this.parent?{parent:this.parent,variant:this.variant??"",isParent:this.isParent}:void 0,constructor:this.constructorName,replay:this.replay||void 0})),e=[this.tag.length,...(0,r.processCharCodes)(this.tag),(0,i.compressBools)([this.dontSpread,this.async,this.object,this.autoFlatten,this.gzipCompression,this.rereference]),...(0,i.convertVarInt)(t.length),...t,this.dataBatching,this.enumData.length,...this.enumData.map(t=>t.serialize()).flat()];return this.object?[...e,this.maxSize,...this.dataMax.map(i.convertVarInt).flat(),...this.dataMin.map(i.convertVarInt).flat(),...this.type]:[...e,...(0,i.convertVarInt)(this.dataMax),...(0,i.convertVarInt)(this.dataMin),this.type]}static readVarInts(t,e,a){const s=[];for(let r=0;r<a;r++){const[a,r]=(0,i.readVarInt)(t,e);e=a,s.push(r)}return[s,e]}static deserialize(a,n,o){const h=n,l=a[n++],d=(0,r.as8String)(a.slice(n,n+=l)),[p,m,f,y,v,g]=(0,i.decompressBools)(a[n++]),[M,S]=(0,i.readVarInt)(a,n);n=M;const x=JSON.parse((new TextDecoder).decode(a.slice(n,n+=S))),z=Array.isArray(x.schema)?x.schema:void 0,N=x.quantized&&"number"==typeof x.quantized.scale?x.quantized:void 0,k="number"==typeof x.min?x.min:void 0,w="number"==typeof x.max?x.max:void 0,b=x.group&&"string"==typeof x.group.parent?x.group:void 0,P="string"==typeof x.constructor?x.constructor:void 0,E=a[n++],q=a[n++],O=[];for(let i=0;i<q;i++){const i=a[n++],s=(0,r.as8String)(a.slice(n,n+=i)),o=a[n++],h=[];for(let t=0;t<o;t++){const t=a[n++],i=a[n++],s=(0,r.as8String)(a.slice(n,n+=t));h.push(e.TYPE_CONVERSION_MAP[i](s))}O.push((0,t.DefineEnum)(s,h))}if(f){const t=a[n++],[e,i]=this.readVarInts(a,n,t);n=i;const[r,l]=this.readVarInts(a,n,t);n=l;const f=Array.from(a.slice(n,n+=t));let g=0;const M=f.map(t=>t==s.PacketType.ENUMS?O[g++]:t),S=new u(!0,M,m,r,e,-1,p,y,!1,E,-1,v,z,void 0,void 0,void 0,b,P,!0===x.replay);return[new c(d,S,null,!1,o),n-h]}const[j,A]=(0,i.readVarInt)(a,n);n=j;const[$,T]=(0,i.readVarInt)(a,n);n=$;const D=a[n++],V=D==s.PacketType.ENUMS?O[0]:D,J=new u(!1,V,m,T,A,-1,p,y,g,E,-1,v,z,N,k,w,b,P,!0===x.replay);return[new c(d,J,null,!1,o),n-h]}static deserializeAll(t,e){const i=[];let a=0;for(;a<t.length;){const[s,r]=this.deserialize(t,a,e);i.push(s),a+=r}return i}}exports.Packet=c;const l=(t,i)=>t instanceof e.EnumPackage?(i.push(t),s.PacketType.ENUMS):t;class u{type;enumData=[];dataMax;dataMin;dataBatching;maxBatchSize;rateLimit;dontSpread=!1;autoFlatten=!1;async=!1;rereference=!1;gzipCompression=!1;object;fields;quantized;valueMin;valueMax;group;constructorName;replay;constructor(t,e,i,a,s,r,n,o,h,c,u,d,p,m,f,y,v,g,M=!1){this.object=t,this.async=i,this.dataMin=a,this.dataMax=s,this.rateLimit=r,this.dontSpread=n,this.autoFlatten=o,this.rereference=h,this.dataBatching=c,this.maxBatchSize=u,this.gzipCompression=d,this.fields=p?[...p]:void 0,this.quantized=m?{...m}:void 0,this.valueMin=f,this.valueMax=y,this.group=v?{...v}:void 0,this.constructorName=g,this.replay=M,this.type=t?e.map(t=>l(t,this.enumData)):l(e,this.enumData)}testObject(t){return this.object}}exports.PacketSchema=u;
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.PacketSchema=exports.Packet=void 0;const t=require("../util/enums/EnumHandler"),e=require("../util/enums/EnumType"),i=require("../util/packets/CompressionUtil"),a=require("../util/packets/PacketUtils"),s=require("./PacketType"),r=require("../util/StringUtil"),n=require("../../native/wrapper"),o=require("../util/packets/JSONUtil"),c=require("../util/packets/ConstructorRegistry");class h{defaultEnabled;tag;maxSize;minSize;type;enumData;dataMax;dataMin;dataBatching;maxBatchSize;dontSpread;autoFlatten;fields;quantized;valueMin;valueMax;parent;variant;isParent;constructorName;replay;rateLimit;async;rereference;gzipCompression;object;client;processReceive;processSend;validate;customValidator;lastReceived={};lastSent={};quantizationErrors={};recordValues;constructor(t,e,i,a,r){if(this.tag=t,this.defaultEnabled=a,this.client=r,this.async=e.async,this.enumData=e.enumData,this.rateLimit=e.rateLimit,this.dontSpread=e.dontSpread,this.autoFlatten=e.autoFlatten,this.rereference=e.rereference,this.dataBatching=e.dataBatching,this.maxBatchSize=r?1/0:e.maxBatchSize,this.gzipCompression=e.gzipCompression,this.fields=e.fields,this.recordValues=this.fields?h.compileRecordValues(this.fields):void 0,this.quantized=e.quantized?{...e.quantized,trackError:e.quantized.trackError??!0}:void 0,this.valueMin=e.valueMin,this.valueMax=e.valueMax,this.parent=e.group?.parent,this.variant=e.group?.variant,this.isParent=e.group?.isParent??!1,this.constructorName=e.constructorName,this.replay=e.replay,this.object=e.object,this.type=e.type,this.dataMax=e.dataMax,this.dataMin=e.dataMin,e.testObject(this)){this.maxSize=this.minSize=this.type.length;for(let t=0;t<this.type.length;t++)this.type[t]==s.PacketType.NONE&&(this.dataMax[t]=this.dataMin[t]=0);const t={types:this.type,dataMins:this.dataMin,dataMaxes:this.dataMax,enumData:this.enumData};this.processReceive=e=>(0,n.decodeNativeObject)(t,e).map((t,e)=>this.type[e]===s.PacketType.JSON?(0,o.decompressJSON)(t):t),this.processSend=e=>{let i=0;const a=e.map((t,e)=>{if(this.type[e]===s.PacketType.JSON)return(0,o.compressJSON)(t);if(this.type[e]!==s.PacketType.ENUMS)return t;const a=this.enumData[i++];return(Array.isArray(t)?t:[t]).map(t=>{if(!Number.isInteger(t)||t<0||t>=a.values.length)throw new Error(`Invalid wrapped enum index: ${t}`);return a.values[t]})});return(0,n.encodeNativeObject)(t,a)},this.validate=async e=>{(0,n.validateNativeObject)(t,e);return(0,n.decodeNativeObject)(t,e).forEach((t,e)=>{if(this.type[e]!==s.PacketType.JSON)return;const i=(0,o.decompressJSON)(t),a=Array.isArray(i)?i.length:1;if(a<this.dataMin[e]||a>this.dataMax[e])throw new Error("JSON value count is outside schema limits")}),[e,!0]}}else{{this.maxSize=this.dataMax,this.minSize=this.dataMin,this.type==s.PacketType.NONE&&(this.dataMax=this.dataMin=0);const t=this.enumData[0];this.processReceive=e=>this.type===s.PacketType.JSON?(0,o.decompressJSON)(e):(0,n.decodeNative)(this.type,e,this.dataMax,t),this.processSend=e=>{let i;if(this.type===s.PacketType.JSON)i=(0,o.compressJSON)(e);else if(this.type===s.PacketType.ENUMS)i=Uint8Array.from(e);else{const a=this.type===s.PacketType.RAW&&1===e.length&&e[0]instanceof Uint8Array?e[0]:e;i=(0,n.encodeNative)(this.type,a,t)}return this.gzipCompression&&0===this.dataBatching?(0,n.deflateNative)(i):i},this.validate=async e=>{const i=this.gzipCompression&&0===this.dataBatching?(0,n.inflateNative)(e):e;if(this.type===s.PacketType.JSON){const t=(0,o.decompressJSON)(i),e=Array.isArray(t)?t.length:1;if(e<this.dataMin||e>this.dataMax)throw new Error("JSON value count is outside schema limits")}else(0,n.validateNative)(this.type,i,this.dataMin,this.dataMax,{enumData:t});return[i,!0]}}}this.customValidator=i}static compileRecordValues(t){switch(t.length){case 0:return()=>[];case 1:{const[e]=t;return t=>[t[e]]}case 2:{const[e,i]=t;return t=>[t[e],t[i]]}case 3:{const[e,i,a]=t;return t=>[t[e],t[i],t[a]]}case 4:{const[e,i,a,s]=t;return t=>[t[e],t[i],t[a],t[s]]}case 5:{const[e,i,a,s,r]=t;return t=>[t[e],t[i],t[a],t[s],t[r]]}case 6:{const[e,i,a,s,r,n]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n]]}case 7:{const[e,i,a,s,r,n,o]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n],t[o]]}case 8:{const[e,i,a,s,r,n,o,c]=t;return t=>[t[e],t[i],t[a],t[s],t[r],t[n],t[o],t[c]]}default:return e=>{const i=new Array(t.length);for(let a=0;a<t.length;a++)i[a]=e[t[a]];return i}}}assertRecord(t,e){if(!this.fields)throw new Error(`Packet "${this.tag}" ${e} requires schema`);if(null===t||"object"!=typeof t||Array.isArray(t))throw new Error(`Packet "${this.tag}" ${e} requires an object record`);for(const e of this.fields)if(!Object.prototype.hasOwnProperty.call(t,e))throw new Error(`Packet "${this.tag}" is missing schema field(s): ${e}`);const i=Object.keys(t);if(i.length!==this.fields.length){const t=i.filter(t=>!this.fields.includes(t));if(t.length)throw new Error(`Packet "${this.tag}" has unknown schema field(s): ${t.join(", ")}`)}return this.recordValues(t)}construct(t){return this.constructorName?new((0,c.resolvePacketConstructor)(this.constructorName))(t):t}logicalValue(t,e,i){const a=this.quantized?.scale;if("number"!=typeof t||!Number.isFinite(t))throw new Error(`Packet "${this.tag}" ${e} value must be a finite number`);const s="receive"===e&&a?t/a:t;if(void 0!==this.valueMin&&s<this.valueMin)throw new Error(`Packet "${this.tag}" value ${s} is below minimum ${this.valueMin}`);if(void 0!==this.valueMax&&s>this.valueMax)throw new Error(`Packet "${this.tag}" value ${s} exceeds maximum ${this.valueMax}`);if("send"===e&&a){const t=s*a+(this.quantized?.trackError?this.quantizationErrors[i]??0:0),e=Math.round(t);return this.quantized?.trackError&&(this.quantizationErrors[i]=t-e),e}return s}logical(t,e,i=-1){const a=new Array(t.length);for(let s=0;s<t.length;s++)a[s]=this.logicalValue(t[s],e,i);return a}prepareSend(t,e=-1){if(this.object){if(this.autoFlatten&&this.fields){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoTranspose requires one array of records`);const e=t[0].map(t=>this.assertRecord(t,"autoTranspose"));return this.fields.map((t,i)=>e.map(t=>t[i]))}return t}let i=t;if(this.autoFlatten){if(1!==t.length||!Array.isArray(t[0]))throw new Error(`Packet "${this.tag}" autoFlatten requires one array of records`);const a=t[0],s=this.fields.length;i=new Array(a.length*s);let r=0;const n=this.quantized||void 0!==this.valueMin||void 0!==this.valueMax;for(const t of a){const a=this.assertRecord(t,"autoFlatten");for(let t=0;t<s;t++){const s=a[t];i[r++]=n?this.logicalValue(s,"send",e):s}}if(this.fields&&i.length%this.fields.length!==0)throw new Error(`Packet "${this.tag}" flat value count must be divisible by schema length ${this.fields.length}`);if(n)return i}else this.fields&&1===t.length&&null!==t[0]&&"object"==typeof t[0]&&!Array.isArray(t[0])&&(i=this.assertRecord(t[0],"schema mapping"));return this.quantized||void 0!==this.valueMin||void 0!==this.valueMax?this.logical(i,"send",e):i}clearQuantizationState(t){delete this.quantizationErrors[t]}finishReceive(t){if(this.object){if(this.autoFlatten&&this.fields){const e=t,i=e[0]?.length??0;if(e.some(t=>t.length!==i))throw new Error(`Packet "${this.tag}" autoTranspose columns have different lengths`);return Array.from({length:i},(t,i)=>{const a=this.fields.map((t,a)=>[t,e[a][i]]);return this.construct(Object.fromEntries(a))})}return this.autoFlatten?(0,a.UnFlattenData)(t):t}let e=Array.isArray(t)?t:[t];if((this.quantized||void 0!==this.valueMin||void 0!==this.valueMax)&&(e=this.logical(e,"receive")),this.autoFlatten){const t=this.fields.length;if(e.length%t!==0)throw new Error(`Packet "${this.tag}" flat value count ${e.length} is not divisible by schema length ${t}`);return Array.from({length:e.length/t},(i,a)=>this.construct(Object.fromEntries(this.fields.map((i,s)=>[i,e[a*t+s]]))))}if(this.fields){const t=this.fields.map((t,i)=>[t,e[i]]);return this.construct(Object.fromEntries(t))}return t}async listen(t,e){try{const[i,a]=await this.validate(t);if(!this.client&&!1===a)return"Invalid packet";const s=this.processReceive(i,a),r=this.finishReceive(s);if(null!=this.customValidator)if(this.dontSpread||this.fields){if(!this.customValidator(e,r))return"Didn't pass custom validator"}else if(!this.customValidator(e,...r))return"Didn't pass custom validator";return[this.isParent?{variant:"",payload:r}:r,!this.isParent&&!this.fields&&!this.dontSpread]}catch(t){return console.error("SonicWS failed to process a packet. Report reproducible codec failures at https://github.com/liwybloc/sonic-ws",t),`Error: ${String(t)}`}}serialize(){const t=(new TextEncoder).encode(JSON.stringify({schema:this.fields,quantized:this.quantized,min:this.valueMin,max:this.valueMax,group:void 0!==this.parent?{parent:this.parent,variant:this.variant??"",isParent:this.isParent}:void 0,constructor:this.constructorName,replay:this.replay||void 0})),e=[this.tag.length,...(0,r.processCharCodes)(this.tag),(0,i.compressBools)([this.dontSpread,this.async,this.object,this.autoFlatten,this.gzipCompression,this.rereference]),...(0,i.convertVarInt)(t.length),...t,this.dataBatching,this.enumData.length,...this.enumData.flatMap(t=>t.serialize())];return this.object?[...e,this.maxSize,...this.dataMax.flatMap(i.convertVarInt),...this.dataMin.flatMap(i.convertVarInt),...this.type]:[...e,...(0,i.convertVarInt)(this.dataMax),...(0,i.convertVarInt)(this.dataMin),this.type]}static readVarInts(t,e,a){const s=[];for(let r=0;r<a;r++){const[a,r]=(0,i.readVarInt)(t,e);e=a,s.push(r)}return[s,e]}static deserialize(a,n,o){const c=n,l=a[n++],d=(0,r.as8String)(a.slice(n,n+=l)),[p,m,f,y,v,g]=(0,i.decompressBools)(a[n++]),[M,S]=(0,i.readVarInt)(a,n);n=M;const x=a.slice(n,n+=S),w=JSON.parse((new TextDecoder).decode(x)),z=Array.isArray(w.schema)?w.schema:void 0,N=w.quantized&&"number"==typeof w.quantized.scale?w.quantized:void 0,k="number"==typeof w.min?w.min:void 0,P="number"==typeof w.max?w.max:void 0,b=w.group&&"string"==typeof w.group.parent?w.group:void 0,E="string"==typeof w.constructor?w.constructor:void 0,q=a[n++],O=a[n++],A=[];for(let i=0;i<O;i++){const i=a[n++],s=(0,r.as8String)(a.slice(n,n+=i)),o=a[n++],c=[];for(let t=0;t<o;t++){const t=a[n++],i=a[n++],s=(0,r.as8String)(a.slice(n,n+=t));c.push(e.TYPE_CONVERSION_MAP[i](s))}A.push((0,t.DefineEnum)(s,c))}if(f){const t=a[n++],[e,i]=this.readVarInts(a,n,t);n=i;const[r,l]=this.readVarInts(a,n,t);n=l;const f=Array.from(a.slice(n,n+=t));let g=0;const M=f.map(t=>t===s.PacketType.ENUMS?A[g++]:t),S=new u(!0,M,m,r,e,-1,p,y,!1,q,-1,v,z,void 0,void 0,void 0,b,E,!0===w.replay);return[new h(d,S,null,!1,o),n-c]}const[j,V]=(0,i.readVarInt)(a,n);n=j;const[$,T]=(0,i.readVarInt)(a,n);n=$;const D=a[n++],J=D===s.PacketType.ENUMS?A[0]:D,R=new u(!1,J,m,T,V,-1,p,y,g,q,-1,v,z,N,k,P,b,E,!0===w.replay);return[new h(d,R,null,!1,o),n-c]}static deserializeAll(t,e){const i=[];let a=0;for(;a<t.length;){const[s,r]=this.deserialize(t,a,e);i.push(s),a+=r}return i}}function l(t,i){return t instanceof e.EnumPackage?(i.push(t),s.PacketType.ENUMS):t}exports.Packet=h;class u{type;enumData=[];dataMax;dataMin;dataBatching;maxBatchSize;rateLimit;dontSpread=!1;autoFlatten=!1;async=!1;rereference=!1;gzipCompression=!1;object;fields;quantized;valueMin;valueMax;group;constructorName;replay;constructor(t,e,i,a,s,r,n,o,c,h,u,d,p,m,f,y,v,g,M=!1){this.object=t,this.async=i,this.dataMin=a,this.dataMax=s,this.rateLimit=r,this.dontSpread=n,this.autoFlatten=o,this.rereference=c,this.dataBatching=h,this.maxBatchSize=u,this.gzipCompression=d,this.fields=p?[...p]:void 0,this.quantized=m?{...m}:void 0,this.valueMin=f,this.valueMax=y,this.group=v?{...v}:void 0,this.constructorName=g,this.replay=M,this.type=t?e.map(t=>l(t,this.enumData)):l(e,this.enumData)}testObject(t){return this.object}}exports.PacketSchema=u;
@@ -4,6 +4,7 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
  import * as WS from 'ws';
7
+ import type { IncomingMessage } from 'node:http';
7
8
  import { SonicWSServer } from "./SonicWSServer";
8
9
  import { Packet } from "../packets/Packets";
9
10
  import { Connection } from "../Connection";
@@ -24,7 +25,9 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
24
25
  private pendingRequests;
25
26
  private responders;
26
27
  sessionId: string;
27
- constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number, sessionId: string, state: Record<string, unknown>);
28
+ /** Provides the HTTP upgrade request for authentication headers and cookies. */
29
+ readonly upgradeRequest: IncomingMessage;
30
+ constructor(socket: WS.WebSocket, host: SonicWSServer, id: number, handshakePacket: string | null, clientRateLimit: number, serverRateLimit: number, sessionId: string, state: Record<string, unknown>, handshakeTimeoutMs: number, upgradeRequest: IncomingMessage);
28
31
  private parseData;
29
32
  private isControl;
30
33
  private routeMessage;
@@ -34,7 +37,8 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
34
37
  private isAsync;
35
38
  private listenLock;
36
39
  private packetQueue;
37
- private listenPacket;
40
+ private deliverPacket;
41
+ private releasePacketLock;
38
42
  private messageHandler;
39
43
  /**
40
44
  * Enables a packet for the client.
@@ -74,6 +78,10 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
74
78
  /** Registers the server-side responder for client requests using this packet tag. */
75
79
  respond(tag: string, handler: (...values: any[]) => any): void;
76
80
  sendSafe(tag: string, ...values: any[]): Promise<boolean>;
81
+ /** Drops this update before encoding when the transport is backpressured. */
82
+ sendVolatile(tag: string, ...values: any[]): Promise<boolean>;
83
+ /** Sends a packet without applying the volatile backpressure drop policy. */
84
+ sendReliable(tag: string, ...values: any[]): Promise<void>;
77
85
  /**
78
86
  * Broadcasts a packet to all other users connected
79
87
  * @param tag The tag to send
@@ -86,8 +94,11 @@ export declare class SonicWSConnection extends Connection<WS.WebSocket, Buffer>
86
94
  * @param values The values to send
87
95
  */
88
96
  broadcast(tag: string, ...values: any[]): void;
97
+ /** Adds this connection to a server-side room. */
89
98
  join(room: string): void;
99
+ /** Removes this connection from a server-side room. */
90
100
  leave(room: string): void;
101
+ /** Returns the server-side rooms currently assigned to this connection. */
91
102
  getRooms(): ReadonlySet<string>;
92
103
  broadcastRoom(room: string, tag: string, ...values: any[]): Promise<void>;
93
104
  /**
@@ -4,4 +4,4 @@
4
4
  * See LICENSE for personal, non-commercial license terms.
5
5
  */
6
6
 
7
- Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSConnection=void 0;const e=require("../util/packets/PacketUtils"),t=require("../util/packets/BatchHelper"),s=require("../util/packets/RateHandler"),i=require("../util/BufferUtil"),a=require("../Connection"),r=require("../util/packets/ControlProtocol");class n extends a.Connection{host;print=!1;handshakePacket;handshakeLambda;messageLambda=e=>this.routeMessage(e);handshakedMessageLambda=e=>{if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);return null==t?this.socket.close(a.CloseCodes.INVALID_DATA):t[0]==this.handshakePacket?this.socket.close(a.CloseCodes.REPEATED_HANDSHAKE):void this.messageHandler(t)};rater;enabledPackets={};handshakeComplete=!1;asyncMap={};asyncData={};nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;constructor(e,t,i,a,r,n,o,h){super(e,i,"Socket "+i,e.addEventListener.bind(e),e.removeEventListener.bind(e)),this.host=t,this.sessionId=o,this.state=h,this.handshakePacket=a;for(const e of t.clientPackets.getPackets()){const t=e.tag;this.listeners[t]=[],e.lastReceived[this.id]=void 0,this.enabledPackets[t]=e.defaultEnabled,this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}this.setInterval=this.setInterval.bind(this),this.batcher.registerSendPackets(this.host.serverPackets,this),this.invalidPacket=this.invalidPacket.bind(this),this.rater=new s.RateHandler(this),this.rater.registerRate("C",r),this.rater.registerRate("S",n),this.rater.registerAll(t.clientPackets,"client"),this.rater.registerAll(t.serverPackets,"server"),this.rater.start(),null==this.handshakePacket?this.socket.addEventListener("message",this.messageLambda):(this.handshakeLambda=e=>this.handshakeHandler(e),this.socket.addEventListener("message",this.handshakeLambda)),this.socket.on("close",()=>{for(const e of t.clientPackets.getPackets())delete e.lastReceived[this.id];for(const e of t.serverPackets.getPackets())delete e.lastSent[this.id],e.clearQuantizationState(this.id)})}parseData(e){if(this.rater.trigger("C"))return null;if(!(e.data instanceof Buffer))return null;const t=new Uint8Array(e.data);if(this.print&&console.log(`⬇ (${this.id},${t.byteLength})`,t.length>0&&this.host.clientPackets.getTag(t[0])||"<INVALID>",(0,i.stringifyBuffer)(t)),t.byteLength<1)return this.socket.close(a.CloseCodes.SMALL),null;const s=t[0],r=t.slice(1);if(!this.host.clientPackets.hasKey(s))return this.socket.close(a.CloseCodes.INVALID_KEY),null;const n=this.host.clientPackets.getTag(s);return this.enabledPackets[n]?this.rater.trigger("client"+s)?null:[n,r]:(this.socket.close(a.CloseCodes.DISABLED_PACKET),null)}isControl(e){return e.data instanceof Buffer&&e.data.length>0&&0===e.data[0]}routeMessage(e){this.isControl(e)?this.handleControl(new Uint8Array(e.data)):this.messageHandler(this.parseData(e))}async handleControl(e){if(this.rater.trigger("C"))return;const t=(0,r.decodeControl)(e);if(t.type!==r.ControlType.RESUME)if(this.handshakeComplete||null===this.handshakePacket){if(t.type===r.ControlType.REPLAY||t.type===r.ControlType.RESUMED)throw new Error("A server cannot receive replay delivery frames");if(t.type===r.ControlType.RESPONSE){const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}try{const e=this.host.clientPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);if(!this.enabledPackets[e])return this.close(a.CloseCodes.DISABLED_PACKET,`Packet "${e}" is disabled`);if(this.rater.trigger("client"+t.packetKey))return this.close(a.CloseCodes.RATELIMIT,`Packet "${e}" exceeded its rate limit`);if(await this.callMiddleware("onReceive_pre",e,t.payload,t.payload.length))throw new Error(`Packet "${e}" was rejected by middleware`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const i=await this.host.clientPackets.getPacket(e).listen(t.payload,this);if("string"==typeof i)throw new Error(i);const[n,o]=i,h=o?await s(...n):await s(n);this.raw_send((0,r.encodeControlResponse)(t.id,!0,h??null))}catch(e){this.raw_send((0,r.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}else this.close(a.CloseCodes.INVALID_DATA,"Handshake required before control requests");else await this.host.resumeSession(this,t.sessionId,t.lastSequence)}handshakeHandler(e){if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);if(null==t)return this.socket.close(a.CloseCodes.INVALID_DATA);t[0]==this.handshakePacket?(this.messageHandler(t),this.socket.removeEventListener("message",this.handshakeLambda),this.socket.addEventListener("message",this.handshakedMessageLambda),this.handshakeComplete=!0):this.socket.close(a.CloseCodes.INVALID_DATA)}invalidPacket(e){console.log("Closure cause",e),this.socket.close(a.CloseCodes.INVALID_PACKET,e)}isAsync(e){return this.asyncMap[e]}listenLock=!1;packetQueue=[];async listenPacket(t,s,i,a,r){if(this.closed)return;await(0,e.listenPacket)(t,this.listeners[s],this.invalidPacket);const n=this.host.clientPackets.getPacket(s);if("string"!=typeof t&&n.parent&&n.variant)for(const e of this.listeners[n.parent]??[])await e({variant:n.variant,payload:t[0]});await this.callMiddleware("onReceive_post",s,"string"==typeof t?[t]:t[0]),a?r[0]=!1:this.listenLock=!1,0==i.length||this.messageHandler(i.shift())}async messageHandler(e,s=!1){if(null==e)return;const[i,a]=e,r=this.isAsync(i);let n,o,h;if(r?(h=this.asyncData[i],n=h[0],o=h[1]):(n=this.listenLock,o=this.packetQueue),n)return void o.push(e);r?h[0]=!0:this.listenLock=!0;const c=this.host.clientPackets.getPacket(i);if(!s&&await this.callMiddleware("onReceive_pre",c.tag,a,a.length))return;if(c.rereference&&0==a.length){const e=c.lastReceived[this.id];return void 0===e?this.invalidPacket("No previous value to rereference"):void await this.listenPacket(e,i,o,r,h)}if(0==c.dataBatching){const e=await c.listen(a,this);return c.lastReceived[this.id]=e,void await this.listenPacket(e,i,o,r,h)}const l=await t.BatchHelper.unravelBatch(c,a,this);if("string"==typeof l)return this.invalidPacket(l);for(const e of l)r?h[0]=!0:this.listenLock=!0,await this.listenPacket(e,i,o,r,h)}enablePacket(e){this.enabledPackets[e]=!0}disablePacket(e){this.enabledPackets[e]=!1}on_close(e){this.socket.on("close",(t,s)=>e(t,String(s)))}on(e,t){if(!this.host.clientPackets.hasTag(e))throw new Error(`Tag "${String(e)}" has not been created!`);const s=this.host.clientPackets.resolveTag(e);this.listeners[s]??=[],this.listeners[s].push(t)}send_processed(e,t,s){if(!this.rater.trigger("server"+e))if(0==s.dataBatching){const a=(0,i.toPacketBuffer)(e,t);this.raw_send(s.replay?this.host.replayFrame(this,a):a)}else this.batcher.batchPacket(e,t)}sendQueue=[!1,[],void 0];async send(t,...s){if(await this.callMiddleware("onSend_pre",t,s,Date.now(),performance.now()))return;const[i,a,r]=await(0,e.processPacket)(this.host.serverPackets,t,s,this.sendQueue,this.id);await this.callMiddleware("onSend_post",t,a,a.length)||this.send_processed(i,a,r)}sendVariant(e,t,...s){return this.send(this.host.serverPackets.getVariantTag(e,t),...s)}async request(t,...s){const i=s.at(-1),a=s.length>1&&i&&"object"==typeof i&&!Array.isArray(i)&&Object.keys(i).every(e=>"timeoutMs"===e)?s.pop():{},[n,o]=await(0,e.processPacket)(this.host.serverPackets,t,s,this.sendQueue,this.id);if(this.rater.trigger("server"+n))throw new Error(`Packet "${t}" exceeded its rate limit`);const h=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((e,s)=>{const i=setTimeout(()=>{this.pendingRequests.delete(h),s(new Error(`RPC request "${t}" timed out`))},a.timeoutMs??5e3);this.pendingRequests.set(h,{resolve:e,reject:s,timer:i}),this.raw_send((0,r.encodeControlRequest)(h,n,o))})}respond(e,t){this.responders.set(this.host.clientPackets.resolveTag(e),t)}async sendSafe(e,...t){try{return await this.send(e,...t),!0}catch(t){return this.host.handleSendError(t,{packetTag:e,connection:this}),!1}}broadcastFiltered(e,t,...s){this.host.broadcastFiltered(e,e=>e!=this&&t(e),...s)}broadcast(e,...t){this.broadcastFiltered(e,()=>!0,...t)}join(e){this.host.join(this,e)}leave(e){this.host.leave(this,e)}getRooms(){return this.host.rooms(this)}broadcastRoom(e,t,...s){return this.host.broadcastRoomExcept(this,e,t,...s)}togglePrint(){this.print=!this.print}raw_send(e){this.isClosed()?console.warn("WARN! Connection already closed when trying to send message!",this.id,(0,i.stringifyBuffer)(e)):this.rater.trigger("S")||(this.print&&console.log(`⬆ (${this.id},${e.byteLength})`,e.length>0&&this.host.serverPackets.getTag(e[0])||"<INVALID>",(0,i.stringifyBuffer)(e)),super.raw_send(e))}tag(e,t=!0){this.host.tag(this,e,t)}}exports.SonicWSConnection=n;
7
+ Object.defineProperty(exports,"__esModule",{value:!0}),exports.SonicWSConnection=void 0;const e=require("../util/packets/PacketUtils"),t=require("../util/packets/BatchHelper"),s=require("../util/packets/RateHandler"),i=require("../util/BufferUtil"),a=require("../Connection"),r=require("../util/packets/ControlProtocol");class n extends a.Connection{host;print=!1;handshakePacket;handshakeLambda;messageLambda=e=>this.routeMessage(e);handshakedMessageLambda=e=>{if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);return null==t?this.socket.close(a.CloseCodes.INVALID_DATA):t[0]===this.handshakePacket?this.socket.close(a.CloseCodes.REPEATED_HANDSHAKE):void this.messageHandler(t)};rater;enabledPackets={};handshakeComplete=!1;asyncMap={};asyncData={};nextRequestId=1;pendingRequests=new Map;responders=new Map;sessionId;upgradeRequest;constructor(e,t,i,r,n,o,h,c,l,d){super(e,i,`Socket ${i}`,e.addEventListener.bind(e),e.removeEventListener.bind(e)),this.host=t,this.sessionId=h,this.upgradeRequest=d,this.state=c,this.handshakePacket=r;for(const e of t.clientPackets.getPackets()){const t=e.tag;this.listeners[t]=[],e.lastReceived[this.id]=void 0,this.enabledPackets[t]=e.defaultEnabled,this.asyncMap[t]=e.async,e.async&&(this.asyncData[t]=[!1,[]])}this.setInterval=this.setInterval.bind(this),this.batcher.registerSendPackets(this.host.serverPackets,this),this.invalidPacket=this.invalidPacket.bind(this),this.rater=new s.RateHandler(this),this.rater.registerRate("C",n),this.rater.registerRate("S",o),this.rater.registerAll(t.clientPackets,"client"),this.rater.registerAll(t.serverPackets,"server"),this.rater.start(),null==this.handshakePacket?this.socket.addEventListener("message",this.messageLambda):(this.handshakeLambda=e=>this.handshakeHandler(e),this.socket.addEventListener("message",this.handshakeLambda),this.setTimeout(()=>{this.handshakeComplete||this.close(a.CloseCodes.INVALID_DATA,"Application handshake timed out")},l)),this.socket.on("close",()=>{for(const e of t.clientPackets.getPackets())delete e.lastReceived[this.id];for(const e of t.serverPackets.getPackets())delete e.lastSent[this.id],e.clearQuantizationState(this.id)})}parseData(e){if(this.rater.trigger("C"))return null;if(!(e.data instanceof Buffer))return null;const t=new Uint8Array(e.data);if(this.print&&console.log(`⬇ (${this.id},${t.byteLength})`,t.length>0&&this.host.clientPackets.getTag(t[0])||"<INVALID>",(0,i.stringifyBuffer)(t)),t.byteLength<1)return this.socket.close(a.CloseCodes.SMALL),null;const s=t[0],r=t.slice(1);if(!this.host.clientPackets.hasKey(s))return this.socket.close(a.CloseCodes.INVALID_KEY),null;const n=this.host.clientPackets.getTag(s);return this.enabledPackets[n]?this.rater.trigger(`client${s}`)?null:[n,r]:(this.socket.close(a.CloseCodes.DISABLED_PACKET),null)}isControl(e){return e.data instanceof Buffer&&e.data.length>0&&0===e.data[0]}routeMessage(e){this.isControl(e)?this.handleControl(new Uint8Array(e.data)):this.messageHandler(this.parseData(e))}async handleControl(e){if(this.rater.trigger("C"))return;let t;try{t=(0,r.decodeControl)(e)}catch{return void this.close(a.CloseCodes.INVALID_DATA,"Malformed SonicWS control frame")}if(t.type!==r.ControlType.RESUME)if(this.handshakeComplete||null===this.handshakePacket){if(t.type===r.ControlType.REPLAY||t.type===r.ControlType.RESUMED)throw new Error("A server cannot receive replay delivery frames");if(t.type===r.ControlType.RESPONSE){const e=this.pendingRequests.get(t.id);if(!e)return;return clearTimeout(e.timer),this.pendingRequests.delete(t.id),void(t.ok?e.resolve(t.value):e.reject(new Error(String(t.value))))}try{const e=this.host.clientPackets.getTag(t.packetKey);if(!e)throw new Error(`Unknown RPC packet key ${t.packetKey}`);if(!this.enabledPackets[e])return this.close(a.CloseCodes.DISABLED_PACKET,`Packet "${e}" is disabled`);if(this.rater.trigger(`client${t.packetKey}`))return this.close(a.CloseCodes.RATELIMIT,`Packet "${e}" exceeded its rate limit`);if(await this.callMiddleware("onReceive_pre",e,t.payload,t.payload.length))throw new Error(`Packet "${e}" was rejected by middleware`);const s=this.responders.get(e);if(!s)throw new Error(`No responder registered for packet "${e}"`);const i=await this.host.clientPackets.getPacket(e).listen(t.payload,this);if("string"==typeof i)throw new Error(i);const[n,o]=i,h=o?await s(...n):await s(n);this.raw_send((0,r.encodeControlResponse)(t.id,!0,h??null))}catch(e){this.raw_send((0,r.encodeControlResponse)(t.id,!1,e instanceof Error?e.message:String(e)))}}else this.close(a.CloseCodes.INVALID_DATA,"Handshake required before control requests");else await this.host.resumeSession(this,t.sessionId,t.lastSequence)}handshakeHandler(e){if(this.isControl(e))return void this.handleControl(new Uint8Array(e.data));const t=this.parseData(e);if(null==t)return this.socket.close(a.CloseCodes.INVALID_DATA);t[0]===this.handshakePacket?(this.messageHandler(t),this.socket.removeEventListener("message",this.handshakeLambda),this.socket.addEventListener("message",this.handshakedMessageLambda),this.handshakeComplete=!0):this.socket.close(a.CloseCodes.INVALID_DATA)}invalidPacket(e){console.warn("Closing connection after an invalid packet",e),this.socket.close(a.CloseCodes.INVALID_PACKET,e)}isAsync(e){return this.asyncMap[e]}listenLock=!1;packetQueue=[];async deliverPacket(t,s,i,a,r){if(this.closed)return;await(0,e.listenPacket)(t,this.listeners[s],this.invalidPacket);const n=this.host.clientPackets.getPacket(s);if("string"!=typeof t&&n.parent&&n.variant)for(const e of this.listeners[n.parent]??[])await e({variant:n.variant,payload:t[0]});await this.callMiddleware("onReceive_post",s,"string"==typeof t?[t]:t[0]),this.releasePacketLock(i,a,r)}releasePacketLock(e,t,s){t?s[0]=!1:this.listenLock=!1,e.length>0&&this.messageHandler(e.shift())}async messageHandler(e,s=!1){if(null==e)return;const[i,a]=e,r=this.isAsync(i);let n,o,h;if(r?(h=this.asyncData[i],n=h[0],o=h[1]):(n=this.listenLock,o=this.packetQueue),n)return void o.push(e);r?h[0]=!0:this.listenLock=!0;const c=this.host.clientPackets.getPacket(i);if(!s&&await this.callMiddleware("onReceive_pre",c.tag,a,a.length))return void this.releasePacketLock(o,r,h);if(c.rereference&&0===a.length){const e=c.lastReceived[this.id];return void 0===e?void this.invalidPacket("No previous value to rereference"):void await this.deliverPacket(e,i,o,r,h)}if(0===c.dataBatching){const e=await c.listen(a,this);return c.lastReceived[this.id]=e,void await this.deliverPacket(e,i,o,r,h)}const l=await t.BatchHelper.unravelBatch(c,a,this);if("string"!=typeof l)for(const e of l)r?h[0]=!0:this.listenLock=!0,await this.deliverPacket(e,i,o,r,h);else this.invalidPacket(l)}enablePacket(e){this.enabledPackets[e]=!0}disablePacket(e){this.enabledPackets[e]=!1}on_close(e){this.socket.on("close",(t,s)=>e(t,String(s)))}on(e,t){if(!this.host.clientPackets.hasTag(e))throw new Error(`Packet tag "${e}" has not been created`);const s=this.host.clientPackets.resolveTag(e);this.listeners[s]??=[],this.listeners[s].push(t)}send_processed(e,t,s){if(!this.rater.trigger(`server${e}`))if(0===s.dataBatching){const a=(0,i.toPacketBuffer)(e,t);this.raw_send(s.replay?this.host.replayFrame(this,a):a)}else this.batcher.batchPacket(e,t)}sendQueue=[!1,[],void 0];async send(t,...s){if(await this.callMiddleware("onSend_pre",t,s,Date.now(),performance.now()))return;const[i,a,r]=await(0,e.processPacket)(this.host.serverPackets,t,s,this.sendQueue,this.id);await this.callMiddleware("onSend_post",t,a,a.length)||this.send_processed(i,a,r)}sendVariant(e,t,...s){return this.send(this.host.serverPackets.getVariantTag(e,t),...s)}async request(t,...s){const i=s.at(-1),a=s.length>1&&i&&"object"==typeof i&&!Array.isArray(i)&&Object.keys(i).every(e=>"timeoutMs"===e)?s.pop():{},[n,o]=await(0,e.processPacket)(this.host.serverPackets,t,s,this.sendQueue,this.id);if(this.rater.trigger(`server${n}`))throw new Error(`Packet "${t}" exceeded its rate limit`);const h=this.nextRequestId++;return this.nextRequestId>2147483647&&(this.nextRequestId=1),new Promise((e,s)=>{const i=setTimeout(()=>{this.pendingRequests.delete(h),s(new Error(`RPC request "${t}" timed out`))},a.timeoutMs??5e3);this.pendingRequests.set(h,{resolve:e,reject:s,timer:i}),this.raw_send((0,r.encodeControlRequest)(h,n,o))})}respond(e,t){this.responders.set(this.host.clientPackets.resolveTag(e),t)}async sendSafe(e,...t){try{return await this.send(e,...t),!0}catch(t){return this.host.handleSendError(t,{packetTag:e,connection:this}),!1}}async sendVolatile(e,...t){return!!this.canSendVolatile()&&(await this.send(e,...t),!0)}sendReliable(e,...t){return this.send(e,...t)}broadcastFiltered(e,t,...s){this.host.broadcastFiltered(e,e=>e!==this&&t(e),...s)}broadcast(e,...t){this.broadcastFiltered(e,()=>!0,...t)}join(e){this.host.join(this,e)}leave(e){this.host.leave(this,e)}getRooms(){return this.host.rooms(this)}broadcastRoom(e,t,...s){return this.host.broadcastRoomExcept(this,e,t,...s)}togglePrint(){this.print=!this.print}raw_send(e){this.isClosed()?console.warn("Cannot send through a closed connection",this.id,(0,i.stringifyBuffer)(e)):this.rater.trigger("S")||(this.print&&console.log(`⬆ (${this.id},${e.byteLength})`,e.length>0&&this.host.serverPackets.getTag(e[0])||"<INVALID>",(0,i.stringifyBuffer)(e)),super.raw_send(e))}tag(e,t=!0){this.host.tag(this,e,t)}}exports.SonicWSConnection=n;
@@ -11,22 +11,28 @@ import { PacketType } from "../packets/PacketType";
11
11
  import { MiddlewareHolder, ServerMiddleware } from "../PacketProcessor";
12
12
  import type { SonicWSAdapter } from "./Adapter";
13
13
  export type SonicServerSettings = {
14
- /** If it should check for updates; defaults to true. */
14
+ /** Checks for protocol updates when enabled. Defaults to true. */
15
15
  readonly checkForUpdates?: boolean;
16
16
  /** If the rereference should use a 64 bit hash which is less prone to collision (1% after ~600 million) or a 32 bit hash. Defaults to true. */
17
17
  readonly bit64Hash?: boolean;
18
- /** Automatically serves the browser client at /SonicWS/bundle.js and /SonicWS/bundle.wasm. Defaults to true. */
18
+ /** Serves browser assets from `/SonicWS` when enabled. Defaults to true. */
19
19
  readonly serveBrowserClient?: boolean;
20
+ /** Limits a required application handshake. Defaults to 10 seconds. */
21
+ readonly handshakeTimeoutMs?: number;
22
+ /** Controls the WebSocket ping interval. Zero disables it. */
23
+ readonly heartbeatIntervalMs?: number;
24
+ /** Limits how long the server waits for pong before terminating. */
25
+ readonly heartbeatTimeoutMs?: number;
20
26
  };
21
27
  /**
22
- * Sonic WS Server Options
28
+ * Configures a SonicWS server.
23
29
  */
24
30
  export type SonicServerOptions = {
25
- /** An array of packets the client can send and server can listen for; using CreatePacket(), CreateObjPacket(), and CreateEnumPacket() */
31
+ /** Declares packets accepted from clients. */
26
32
  readonly clientPackets?: PacketTypings;
27
- /** An array of packets the server can send and client can listen for; using CreatePacket(), CreateObjPacket(), and CreateEnumPacket() */
33
+ /** Declares packets sent to clients. */
28
34
  readonly serverPackets?: PacketTypings;
29
- /** Default WS Options */
35
+ /** Forwards options to the underlying `ws` server. */
30
36
  readonly websocketOptions?: WS.ServerOptions;
31
37
  readonly sonicServerSettings?: SonicServerSettings;
32
38
  readonly onSendError?: (error: unknown, context: {
@@ -40,6 +46,7 @@ export type SonicServerOptions = {
40
46
  readonly recovery?: {
41
47
  maxDisconnectionMs?: number;
42
48
  maxPackets?: number;
49
+ authorize?: (previousState: Record<string, unknown>, currentState: Record<string, unknown>, connection: SonicWSConnection) => boolean | Promise<boolean>;
43
50
  };
44
51
  };
45
52
  export type PacketTypings = readonly Packet<PacketType | readonly PacketType[]>[];
@@ -65,6 +72,7 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
65
72
  private readonly sessions;
66
73
  private readonly recoveryMaxDisconnectionMs;
67
74
  private readonly recoveryMaxPackets;
75
+ private readonly recoveryAuthorize?;
68
76
  /**
69
77
  * Initializes and hosts a websocket with sonic protocol
70
78
  * Rate limits can be set with wss.setClientRateLimit(x) and wss.setServerRateLimit(x); it is defaulted at 500/second per both
@@ -138,7 +146,7 @@ export declare class SonicWSServer extends MiddlewareHolder<ServerMiddleware> {
138
146
  resumeSession(connection: SonicWSConnection, sessionId: string, lastSequence: number): Promise<void>;
139
147
  private broadcastInternal;
140
148
  broadcastTagged(tag: string, packetTag: string, ...values: any): Promise<void>;
141
- /** Sends a packet to every local and adapter-connected member of a room. */
149
+ /** Sends a packet to every local and adapter-connected room member. */
142
150
  broadcastRoom(room: string, packetTag: string, ...values: any[]): Promise<void>;
143
151
  /** Sends a room packet except to one connection. */
144
152
  broadcastRoomExcept(connection: SonicWSConnection, room: string, packetTag: string, ...values: any[]): Promise<void>;