@smoregg/sdk 2.2.0 β 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +361 -111
- package/dist/cjs/controller.cjs +0 -66
- package/dist/cjs/controller.cjs.map +1 -1
- package/dist/cjs/events.cjs +2 -8
- package/dist/cjs/events.cjs.map +1 -1
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/screen.cjs +31 -61
- package/dist/cjs/screen.cjs.map +1 -1
- package/dist/cjs/testing.cjs +5 -61
- package/dist/cjs/testing.cjs.map +1 -1
- package/dist/cjs/transport/protocol.cjs.map +1 -1
- package/dist/cjs/types.cjs +16 -0
- package/dist/cjs/types.cjs.map +1 -1
- package/dist/esm/controller.js +0 -66
- package/dist/esm/controller.js.map +1 -1
- package/dist/esm/events.js +2 -8
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/screen.js +31 -61
- package/dist/esm/screen.js.map +1 -1
- package/dist/esm/testing.js +5 -61
- package/dist/esm/testing.js.map +1 -1
- package/dist/esm/transport/protocol.js.map +1 -1
- package/dist/esm/types.js +16 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/types/controller.d.ts.map +1 -1
- package/dist/types/events.d.ts +0 -4
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/screen.d.ts.map +1 -1
- package/dist/types/testing.d.ts +5 -2
- package/dist/types/testing.d.ts.map +1 -1
- package/dist/types/transport/protocol.d.ts +0 -1
- package/dist/types/transport/protocol.d.ts.map +1 -1
- package/dist/types/types.d.ts +87 -39
- package/dist/types/types.d.ts.map +1 -1
- package/dist/umd/smore-sdk.umd.js +49 -135
- package/dist/umd/smore-sdk.umd.js.map +1 -1
- package/dist/umd/smore-sdk.umd.min.js +1 -1
- package/dist/umd/smore-sdk.umd.min.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).SmoreSDK={})}(this,function(e){"use strict";function t(e){return null!==e&&"object"==typeof e&&"type"in e&&"string"==typeof e.type&&e.type.startsWith("_bridge:")}function r(e){if(!e||"object"!=typeof e)throw new Error("[SDK] _bridge:init payload must be an object");const t=e;if("string"!=typeof t.side||!["host","player"].includes(t.side))throw new Error(`[SDK] _bridge:init payload.side must be "host" or "player", got: ${t.side}`);if("string"!=typeof t.roomCode||0===t.roomCode.length)throw new Error("[SDK] _bridge:init payload.roomCode must be a non-empty string");if(!Array.isArray(t.players))throw new Error("[SDK] _bridge:init payload.players must be an array");if(void 0!==t.myIndex&&"number"!=typeof t.myIndex)throw new Error("[SDK] _bridge:init payload.myIndex must be a number if provided");return!0}class s{static ACK_TIMEOUT=3e4;handlers=new Map;ackCallbacks=new Map;ackCounter=0;parentOrigin;boundMessageHandler;constructor(e="*"){this.parentOrigin=e,this.boundMessageHandler=this.handleMessage.bind(this),window.addEventListener("message",this.boundMessageHandler)}emit(e,...t){let r,n=t[0];if(1===t.length&&"function"==typeof t[0]){n=void 0;const e=t[0];r="ack_"+ ++this.ackCounter,this.ackCallbacks.set(r,e),setTimeout(()=>{this.ackCallbacks.has(r)&&this.ackCallbacks.delete(r)},s.ACK_TIMEOUT)}else if(t.length>=2&&"function"==typeof t[t.length-1]){n=t[0];const e=t[t.length-1];r="ack_"+ ++this.ackCounter,this.ackCallbacks.set(r,e),setTimeout(()=>{this.ackCallbacks.has(r)&&this.ackCallbacks.delete(r)},s.ACK_TIMEOUT)}window.parent.postMessage({type:"_bridge:emit",payload:{event:e,data:n,ackId:r}},this.parentOrigin)}on(e,t){let r=this.handlers.get(e);r||(r=new Set,this.handlers.set(e,r)),r.add(t)}off(e,t){t?this.handlers.get(e)?.delete(t):this.handlers.delete(e)}destroy(){window.removeEventListener("message",this.boundMessageHandler),this.handlers.clear(),this.ackCallbacks.clear()}handleMessage(e){if("*"!==this.parentOrigin&&e.origin!==this.parentOrigin)return;const r=e.data;if(t(r))if("_bridge:event"===r.type){const{event:e,data:t}=r.payload,s=this.handlers.get(e);s&&s.forEach(e=>e(t))}else if("_bridge:ack"===r.type){const{ackId:e,data:t}=r.payload,s=this.ackCallbacks.get(e);s&&(this.ackCallbacks.delete(e),s(t))}}}class n extends Error{code;cause;details;constructor(e,t,r){super(t,r?.cause?{cause:r.cause}:void 0),this.name="SmoreSDKError",this.code=e,this.cause=r?.cause,this.details=r?.details;const s=Error;"function"==typeof s.captureStackTrace&&s.captureStackTrace(this,n)}toSmoreError(){return{code:this.code,message:this.message,cause:this.cause,details:this.details}}}const i={GAME_OVER:"smore:game-over",PLAYER_JOINED:"smore:player-joined",PLAYER_LEFT:"smore:player-left",PLAYER_DISCONNECTED:"smore:player-disconnected",PLAYER_RECONNECTED:"smore:player-reconnected",PLAYER_CHARACTER_UPDATED:"smore:player-character-updated",RATE_LIMITED:"smore:rate-limited",GAME_READY:"smore:game-ready",ALL_READY:"smore:all-ready",SELF_DISCONNECTED:"smore:self-disconnected",SELF_RECONNECTED:"smore:self-reconnected",SEND_TO_PLAYER:"smore:send-to-player",STATE_SET:"smore:set-custom-state",STATE_CHANGED:"smore:custom-state-changed",STATE_GET_ALL:"smore:get-custom-states",STATE_ALL:"smore:custom-states"};new Set(Object.values(i));const o=/^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;function a(e){if(!e||"string"!=typeof e)throw new n("INVALID_EVENT","Event name must be a non-empty string");if(e.length>128)throw new n("INVALID_EVENT",`Event name exceeds maximum length of 128 characters (got ${e.length}).`,{details:{event:e.slice(0,50)+"..."}});if(!o.test(e))throw new n("INVALID_EVENT",`Invalid event name "${e}". Event names must start with a letter, contain only letters, numbers, hyphens, or underscores, and end with a letter or number.`,{details:{event:e}})}const l=new Set(["$all-ready","$controller-join","$controller-leave","$controller-disconnect","$controller-reconnect","$character-updated","$error","$connection-change"]),d=new Set([...l,"$game-over","$state-recovery"]);class c{enabled;level;prefix;logSend;logReceive;logLifecycle;customLogger;static levelOrder={debug:0,info:1,warn:2,error:3};constructor(e,t="[Smore]"){const r="boolean"==typeof e?{enabled:e}:e;this.enabled=r?.enabled??!1,this.level=r?.level??"debug",this.prefix=r?.prefix??t,this.logSend=r?.logSend??!0,this.logReceive=r?.logReceive??!0,this.logLifecycle=r?.logLifecycle??!0,this.customLogger=r?.logger}shouldLog(e){return this.enabled&&c.levelOrder[e]>=c.levelOrder[this.level]}log(e,t,r){if(!this.shouldLog(e))return;if(this.customLogger)return void this.customLogger(e,`${this.prefix} ${t}`,r);const s="error"===e?"error":"warn"===e?"warn":"debug"===e?"debug":"info";void 0!==r?console[s](`${this.prefix} ${t}`,r):console[s](`${this.prefix} ${t}`)}debug(e,t){this.log("debug",e,t)}info(e,t){this.log("info",e,t)}warn(e,t){this.log("warn",e,t)}error(e,t){this.log("error",e,t)}send(e,t){this.enabled&&this.logSend&&this.debug(`-> SEND: ${e}`,t)}receive(e,t){this.enabled&&this.logReceive&&this.debug(`<- RECV: ${e}`,t)}lifecycle(e,t){this.logLifecycle&&this.info(`[Lifecycle] ${e}`,t)}}const h=65536;function g(e,t){return{playerIndex:e.playerIndex??t,nickname:e.nickname||e.name||`Player ${(e.playerIndex??t)+1}`,connected:!1!==e.connected,appearance:e.appearance??e.character}}function y(e){if(null!=e)try{const t=JSON.stringify(e),r=(new TextEncoder).encode(t).byteLength;if(r>h)throw new n("PAYLOAD_TOO_LARGE",`Event payload exceeds maximum size of 65536 bytes (got ${r} bytes). The server will silently drop this event.`,{details:{size:r,limit:h}})}catch(e){if(e instanceof n)throw e}}class p{transport=null;config;logger;_controllers=[];_roomCode="";_isReady=!1;_isDestroyed=!1;_initTimeoutId=null;eventHandlers=new Map;registeredTransportHandlers=[];boundMessageHandler=null;handlerToTransport=new Map;_pendingHandlers=[];_lifecycleListeners=new Map;_outboundBuffer=[];_allReadyFired=!1;_isConnected=!1;_customStates=new Map;_stateChangeListeners=new Set;_protocolVersion=1;_readyResolve;_readyReject;ready;constructor(e={}){this.config=e,this.logger=new c(e.debug,"[SmoreScreen]"),this.ready=new Promise((e,t)=>{this._readyResolve=e,this._readyReject=t}),this.startInitialization()}startInitialization(){this.logger.lifecycle("Initializing screen...");const e=this.config.parentOrigin??"*",i=this.config.timeout??1e4;this._initTimeoutId=setTimeout(()=>{this.cleanup();const e=new n("TIMEOUT",`Screen initialization timed out after ${i}ms. Make sure the parent frame sends _bridge:init. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Screen instance to retry (this instance has been cleaned up).`,{details:{timeout:i}});this.handleError(e),this._readyReject(e)},i),this.boundMessageHandler=i=>{if("*"!==e&&i.origin!==e)return;const o=i.data;if(t(o))if("_bridge:init"===o.type){clearTimeout(this._initTimeoutId);const t=o.payload;try{r(t)}catch(e){const r=new n("INIT_FAILED",`Invalid _bridge:init payload: ${e instanceof Error?e.message:String(e)}`,{details:{payload:t}});return this.logger.warn("_bridge:init validation failed",r),this.handleError(r),void this._readyReject(r)}const i=t;if("host"!==i.side){const e=new n("INIT_FAILED",`Received init for wrong side: ${i.side}. Expected "host".`,{details:{side:i.side}});return this.handleError(e),void this._readyReject(e)}this.transport=this.config.transport??new s(e),this._roomCode=i.roomCode;const a=i.protocolVersion;void 0!==a&&(this._protocolVersion=a,1!==a&&this.logger.warn(`Protocol version mismatch: SDK v1, server v${a}. Some features may not work correctly.`)),this._controllers=this.mapControllersFromInit(i.players),0===this._controllers.length&&this.logger.warn("Screen initialized with zero controllers"),this.setupEventHandlers();for(const{event:e,handler:t}of this._pendingHandlers)this.setupUserEventHandler(e,t);this._pendingHandlers=[],this._isConnected=!0,this._isReady=!0;for(const e of this._outboundBuffer)try{switch(e.method){case"broadcast":this.broadcast(e.args[0],e.args[1]);break;case"sendToController":this.sendToController(e.args[0],e.args[1],e.args[2])}}catch(e){this.handleError(e instanceof n?e:new n("UNKNOWN","Failed to flush buffered message"))}this._outboundBuffer=[],this.logger.lifecycle("Screen ready",{roomCode:this._roomCode,controllers:this._controllers.length}),!1!==this.config.autoReady&&(this.logger.lifecycle("Auto-signaling ready (autoReady enabled)"),this.signalReady()),this._readyResolve()}else if("_bridge:update"===o.type){if(!this._isReady)return void this.logger.debug("Ignoring _bridge:update before init completes");const e=o.payload;if(e.players&&Array.isArray(e.players)){const t=this._controllers,r=this.mapControllersFromInit(e.players);this._controllers=r;for(const e of r)t.some(t=>t.playerIndex===e.playerIndex)||(this.logger.lifecycle("Controller joined (via update)",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-join",e.playerIndex,e));for(const e of t)r.some(t=>t.playerIndex===e.playerIndex)||(this.logger.lifecycle("Controller left (via update)",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-leave",e.playerIndex))}this.logger.lifecycle("Room updated",{controllers:this._controllers.length})}},window.addEventListener("message",this.boundMessageHandler),window.parent.postMessage({type:"_bridge:ready",protocolVersion:1},e),this.logger.lifecycle("Sent _bridge:ready to parent")}mapControllersFromInit(e){return e.map((e,t)=>g(e,t))}setupEventHandlers(){this.transport&&(this.registerTransportHandler(i.PLAYER_JOINED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=g(r,r.playerIndex);if(this._controllers.some(t=>t.playerIndex===e.playerIndex))return;this._controllers=[...this._controllers,e],this.logger.lifecycle("Controller joined",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-join",e.playerIndex,e)}}),this.registerTransportHandler(i.PLAYER_LEFT,e=>{const t=e,r=t?.player?.playerIndex??t?.playerIndex;if("number"==typeof r){if(!this._controllers.some(e=>e.playerIndex===r))return;this._controllers=this._controllers.filter(e=>e.playerIndex!==r),this.logger.lifecycle("Controller left",{playerIndex:r}),this._emitLifecycle("$controller-leave",r)}}),this.registerTransportHandler(i.PLAYER_DISCONNECTED,e=>{const t=e,r=t?.player?.playerIndex??t?.playerIndex;"number"==typeof r&&(this._controllers=this._controllers.map(e=>e.playerIndex===r?{...e,connected:!1}:e),this.logger.lifecycle("Controller disconnected",{playerIndex:r}),this._emitLifecycle("$controller-disconnect",r))}),this.registerTransportHandler(i.PLAYER_RECONNECTED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=g(r,r.playerIndex);this._controllers=this._controllers.map(t=>t.playerIndex===e.playerIndex?e:t),this.logger.lifecycle("Controller reconnected",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-reconnect",e.playerIndex,e)}}),this.registerTransportHandler(i.PLAYER_CHARACTER_UPDATED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=r.playerIndex,t=r.character??null;this._controllers=this._controllers.map(r=>r.playerIndex===e?{...r,appearance:t}:r),this.logger.lifecycle("Player character updated",{playerIndex:e}),this._emitLifecycle("$character-updated",e,t??null)}}),this.registerTransportHandler(i.RATE_LIMITED,e=>{const t=e,r=t?.event??"unknown";this.handleError(new n("RATE_LIMITED",`Server rate-limited event: ${r}`,{details:{event:r}}))}),this.registerTransportHandler(i.ALL_READY,()=>{this.logger.lifecycle("All participants ready"),this._allReadyFired=!0,this._emitLifecycle("$all-ready")}),this.registerTransportHandler(i.SELF_DISCONNECTED,()=>{this._isConnected=!1,this.logger.lifecycle("Connection lost"),this._emitLifecycle("$connection-change",!1)}),this.registerTransportHandler(i.SELF_RECONNECTED,()=>{this._isConnected=!0,this.logger.lifecycle("Connection restored"),this._emitLifecycle("$connection-change",!0)}),this.registerTransportHandler(i.STATE_CHANGED,e=>{const t=e;"number"==typeof t?.playerIndex&&t.state&&(this._customStates.set(t.playerIndex,t.state),this._stateChangeListeners.forEach(e=>{try{e(t.playerIndex,t.state)}catch(e){this.handleError(new n("UNKNOWN","Error in custom state change listener",{cause:e instanceof Error?e:void 0}))}}))}),this.registerTransportHandler(i.STATE_ALL,e=>{const t=e;if(t?.states)for(const[e,r]of Object.entries(t.states)){const t=Number(e);this._customStates.set(t,r),this._stateChangeListeners.forEach(e=>{try{e(t,r)}catch(e){this.handleError(new n("UNKNOWN","Error in custom state change listener",{cause:e instanceof Error?e:void 0}))}})}}))}setupUserEventHandler(e,t){const r=r=>{this.logger.receive(e,r);const s=r,{playerIndex:i,...o}=s;if("number"==typeof i)try{t(i,o)}catch(t){this.handleError(new n("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e,playerIndex:i}}))}else this.logger.debug(`Dropping event "${e}" without playerIndex`,r)};this.registerTransportHandler(e,r);let s=this.eventHandlers.get(e);s||(s=new Set,this.eventHandlers.set(e,s)),s.add(t),this.handlerToTransport.set(t,{event:e,transportHandler:r})}registerTransportHandler(e,t){this.transport&&(this.transport.on(e,t),this.registeredTransportHandlers.push({event:e,handler:t}))}get controllers(){return[...this._controllers]}get roomCode(){return this._roomCode}get isReady(){return this._isReady}get isDestroyed(){return this._isDestroyed}get isConnected(){return this._isConnected}get protocolVersion(){return this._protocolVersion}_addLifecycleListener(e,t){let r=this._lifecycleListeners.get(e);return r||(r=new Set,this._lifecycleListeners.set(e,r)),r.add(t),()=>{r.delete(t),0===r.size&&this._lifecycleListeners.delete(e)}}_emitLifecycle(e,...t){this._lifecycleListeners.get(e)?.forEach(r=>{try{r(...t)}catch(t){this.handleError(new n("UNKNOWN",`Error in lifecycle handler for "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}})}_hasLifecycleListeners(e){const t=this._lifecycleListeners.get(e);return void 0!==t&&t.size>0}onAllReady(e){return this._allReadyFired&&e(),this._addLifecycleListener("$all-ready",e)}onControllerJoin(e){return this._addLifecycleListener("$controller-join",e)}onControllerLeave(e){return this._addLifecycleListener("$controller-leave",e)}onControllerDisconnect(e){return this._addLifecycleListener("$controller-disconnect",e)}onControllerReconnect(e){return this._addLifecycleListener("$controller-reconnect",e)}onCharacterUpdated(e){return this._addLifecycleListener("$character-updated",e)}onError(e){return this._addLifecycleListener("$error",e)}onConnectionChange(e){return this._addLifecycleListener("$connection-change",e)}getControllerState(e){return this._customStates.get(e)}getAllControllerStates(){const e={};for(const[t,r]of this._customStates)e[t]=r;return e}onCustomStateChange(e){return this._stateChangeListeners.add(e),()=>{this._stateChangeListeners.delete(e)}}broadcast(e,t){if(this._isDestroyed)throw new n("DESTROYED","Cannot call broadcast() after destroy()");if(!this._isReady||!this.transport)return this._outboundBuffer.push({method:"broadcast",args:[e,t]}),void this.logger.debug(`Buffered broadcast "${e}" (screen not ready yet)`);a(e),y(t),this.logger.send(e,t),this.transport.emit(e,t)}sendToController(e,t,r){if(this._isDestroyed)throw new n("DESTROYED","Cannot call sendToController() after destroy()");if(!this._isReady||!this.transport)return this._outboundBuffer.push({method:"sendToController",args:[e,t,r]}),void this.logger.debug(`Buffered sendToController "${t}" -> Player ${e} (screen not ready yet)`);a(t),function(e,t){if("number"!=typeof e||!Number.isInteger(e))throw new n("INVALID_PLAYER","Player index must be an integer");if(!t.some(t=>t.playerIndex===e))throw new n("INVALID_PLAYER",`No controller found with player index ${e}`,{details:{playerIndex:e}})}(e,this._controllers),y(r),r&&"object"==typeof r&&"targetPlayerIndex"in r&&this.logger.warn(`Event "${t}" data contains reserved field "targetPlayerIndex" which will be overwritten for routing.`),this.logger.send(`${t} -> Player ${e}`,r),this.transport.emit(t,{targetPlayerIndex:e,...r&&"object"==typeof r?r:{data:r}})}gameOver(e){this.ensureReady("gameOver"),this.logger.lifecycle("Game over",e),this.transport.emit(i.GAME_OVER,{results:e})}signalReady(){this.ensureReady("signalReady"),this.logger.lifecycle("Signaling ready"),this.transport.emit(i.GAME_READY,{})}on(e,t){if("string"==typeof e&&e.startsWith("$")){const r=l;if(!r.has(e))throw new n("INVALID_EVENT",`Unknown lifecycle event: "${e}". Valid lifecycle events: ${Array.from(r).join(", ")}`);return"$all-ready"===e&&this._allReadyFired&&t(),this._addLifecycleListener(e,t)}a(e);let r=this.eventHandlers.get(e);if(r||(r=new Set,this.eventHandlers.set(e,r)),r.add(t),this.transport){const r=r=>{this.logger.receive(e,r);const s=r,{playerIndex:i,...o}=s;if("number"==typeof i)try{t(i,o)}catch(t){this.handleError(new n("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0}))}else this.logger.debug(`Dropping event "${e}" without playerIndex`,r)};this.registerTransportHandler(e,r),this.handlerToTransport.set(t,{event:e,transportHandler:r})}else this._pendingHandlers.push({event:e,handler:t});return()=>{r?.delete(t),0===r?.size&&this.eventHandlers.delete(e),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t));const s=this.handlerToTransport.get(t);s&&(this.transport?.off(e,s.transportHandler),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(e=>e.handler!==s.transportHandler),this.handlerToTransport.delete(t))}}once(e,t){if("string"==typeof e&&e.startsWith("$")){if(!l.has(e))throw new n("INVALID_EVENT",`Unknown lifecycle event: "${e}"`);if("$all-ready"===e&&this._allReadyFired)return t(),()=>{};const r=(...e)=>{s(),t(...e)},s=this._addLifecycleListener(e,r);return s}const r=this.on(e,(e,s)=>{r(),t(e,s)});return r}off(e,t){if("string"==typeof e&&e.startsWith("$"))t?this._lifecycleListeners.get(e)?.delete(t):this._lifecycleListeners.delete(e);else if(t){const r=this.eventHandlers.get(e);r?.delete(t),0===r?.size&&this.eventHandlers.delete(e);const s=this.handlerToTransport.get(t);s&&(this.transport?.off(e,s.transportHandler),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(e=>e.handler!==s.transportHandler),this.handlerToTransport.delete(t)),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t))}else{this.eventHandlers.delete(e),this.transport?.off(e),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}}removeAllListeners(e){if(e){this.eventHandlers.delete(e),this.transport?.off(e),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}else for(const e of[...this.eventHandlers.keys()])this.removeAllListeners(e)}getController(e){return this._controllers.find(t=>t.playerIndex===e)}getControllerCount(){return this._controllers.filter(e=>e.connected).length}destroy(){this._isDestroyed||(this.logger.lifecycle("Destroying screen..."),this._isDestroyed=!0,this._isReady=!1,this.cleanup(),this.logger.lifecycle("Screen destroyed"))}cleanup(){this._initTimeoutId&&(clearTimeout(this._initTimeoutId),this._initTimeoutId=null);for(const{event:e,handler:t}of this.registeredTransportHandlers)this.transport?.off(e,t);this.registeredTransportHandlers=[],this.eventHandlers.clear(),this.handlerToTransport.clear(),this._pendingHandlers=[],this._lifecycleListeners.clear(),this._customStates.clear(),this._stateChangeListeners.clear(),this._isConnected=!1,this._outboundBuffer=[],this.transport instanceof s&&this.transport.destroy(),this.transport=null,this.boundMessageHandler&&(window.removeEventListener("message",this.boundMessageHandler),this.boundMessageHandler=null)}handleError(e){this.logger.warn(`Error in handler: ${e.message}`);const t=e.toSmoreError();this._hasLifecycleListeners("$error")?this._emitLifecycle("$error",t):this.logger.error(e.message,e.details)}ensureReady(e){if(this._isDestroyed)throw new n("DESTROYED",`Cannot call ${e}() after destroy()`,{details:{method:e}});if(!this._isReady||!this.transport)throw new n("NOT_READY",`Cannot call ${e}() before screen is ready. Use await screen.ready.`,{details:{method:e}})}}class f{transport=null;config;logger;_roomCode="";_myPlayerIndex=-1;_isReady=!1;_isDestroyed=!1;_initTimeoutId=null;boundMessageHandler=null;registeredHandlers=[];eventListeners=new Map;handlerToTransport=new Map;_controllers=[];_customStates=new Map;_stateChangeListeners=new Set;_pendingHandlers=[];_lifecycleListeners=new Map;_outboundBuffer=[];_allReadyFired=!1;_isConnected=!1;_protocolVersion=1;_readyResolve;_readyReject;ready;constructor(e={}){this.config=e,this.logger=new c(e.debug,"[SmoreController]"),this.ready=new Promise((e,t)=>{this._readyResolve=e,this._readyReject=t}),this.startInitialization()}get myPlayerIndex(){return this._myPlayerIndex}get roomCode(){return this._roomCode}get isReady(){return this._isReady}get isDestroyed(){return this._isDestroyed}get isConnected(){return this._isConnected}get protocolVersion(){return this._protocolVersion}get controllers(){return[...this._controllers]}get me(){return this._controllers.find(e=>e.playerIndex===this._myPlayerIndex)}getControllerCount(){return this._controllers.filter(e=>e.connected).length}getController(e){return this._controllers.find(t=>t.playerIndex===e)}startInitialization(){const e=this.config.parentOrigin??"*",r=this.config.timeout??1e4;this.logger.lifecycle("Initializing controller...",{parentOrigin:e,timeout:r}),this._initTimeoutId=setTimeout(()=>{this.cleanup();const e=new n("TIMEOUT",`Controller initialization timed out after ${r}ms. Make sure the parent window sends _bridge:init message. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Controller instance to retry (this instance has been cleaned up).`,{details:{timeout:r}});this.handleError(e),this._readyReject(e)},r),this.boundMessageHandler=r=>{if("*"!==e&&r.origin!==e)return;const s=r.data;t(s)&&("_bridge:init"===s.type?(clearTimeout(this._initTimeoutId),this.handleInit(s,e)):"_bridge:update"===s.type&&this.handleUpdate(s))},window.addEventListener("message",this.boundMessageHandler),this.logger.lifecycle("Sending _bridge:ready to parent"),window.parent.postMessage({type:"_bridge:ready",protocolVersion:1},e)}handleInit(e,t){const o=e.payload;this.logger.debug("Received _bridge:init",o);try{r(o)}catch(e){const t=new n("INIT_FAILED",`Invalid _bridge:init payload: ${e instanceof Error?e.message:String(e)}`,{details:{payload:o}});return this.logger.warn("_bridge:init validation failed",t),this.handleError(t),void this._readyReject(t)}const a=o;if("player"!==a.side){const e=new n("INIT_FAILED",`Controller received init for wrong side: ${a.side}`,{details:{side:a.side}});return this.handleError(e),void this._readyReject(e)}if(void 0===a.myIndex){const e=new n("INIT_FAILED","Missing myIndex in init payload",{details:a});return this.handleError(e),void this._readyReject(e)}this.transport=this.config.transport??new s(t),this._roomCode=a.roomCode;const l=a.protocolVersion;void 0!==l&&(this._protocolVersion=l,1!==l&&this.logger.warn(`Protocol version mismatch: SDK v1, server v${l}. Some features may not work correctly.`)),this._myPlayerIndex=a.myIndex;const d=a.players;this._controllers=d.filter(e=>"number"==typeof e.playerIndex).map((e,t)=>g(e,t)),this.setupEventHandlers();for(const{event:e,handler:t}of this._pendingHandlers)this.setupUserEventHandler(e,t);this._pendingHandlers=[],this._isConnected=!0,this._isReady=!0,a.gameInProgress&&this.transport&&(this.logger.lifecycle("Game in progress detected, requesting state recovery"),this.transport.emit(i.STATE_GET_ALL,{}));for(const e of this._outboundBuffer)try{if("send"===e.method)this.send(e.args[0],e.args[1])}catch(e){this.handleError(e instanceof n?e:new n("UNKNOWN","Failed to flush buffered message"))}this._outboundBuffer=[],this.logger.lifecycle("Controller ready",{roomCode:this._roomCode,myIndex:this._myPlayerIndex}),!1!==this.config.autoReady&&(this.logger.lifecycle("Auto-signaling ready (autoReady enabled)"),this.signalReady()),this._readyResolve()}handleUpdate(e){if(!this._isReady)return void this.logger.debug("Ignoring _bridge:update before init completes");const t=e.payload;if(this.logger.debug("Received _bridge:update",t),t.players&&Array.isArray(t.players)){const e=t.players.filter(e=>"number"==typeof e.playerIndex).map((e,t)=>g(e,t)),r=this._controllers;for(const t of e)r.some(e=>e.playerIndex===t.playerIndex)||this._emitLifecycle("$controller-join",t.playerIndex,t);for(const t of r)e.some(e=>e.playerIndex===t.playerIndex)||this._emitLifecycle("$controller-leave",t.playerIndex);for(const t of e){const e=r.find(e=>e.playerIndex===t.playerIndex);e&&(e.connected&&!t.connected&&(this.logger.debug("Player disconnected (via update)",{playerIndex:t.playerIndex}),this._emitLifecycle("$controller-disconnect",t.playerIndex)),!e.connected&&t.connected&&(this.logger.debug("Player reconnected (via update)",{playerIndex:t.playerIndex}),this._emitLifecycle("$controller-reconnect",t.playerIndex,t)))}this._controllers=e}}setupEventHandlers(){this.transport&&(this.registerHandler(i.PLAYER_JOINED,e=>{const t=e,r=t.player,s=r?.playerIndex??t.playerIndex;if(void 0!==s){if(this._controllers.some(e=>e.playerIndex===s))return;const e=g(r||{playerIndex:s,connected:!0},s);this._controllers=[...this._controllers,e],this.logger.debug("Player joined",{playerIndex:s}),this._emitLifecycle("$controller-join",s,e)}}),this.registerHandler(i.PLAYER_LEFT,e=>{const t=e,r=t.player?.playerIndex??t.playerIndex;if(void 0!==r){if(!this._controllers.some(e=>e.playerIndex===r))return;this._controllers=this._controllers.filter(e=>e.playerIndex!==r),this.logger.debug("Player left",{playerIndex:r}),this._emitLifecycle("$controller-leave",r)}}),this.registerHandler(i.PLAYER_DISCONNECTED,e=>{const t=e,r=t.player,s=r?.playerIndex??t.playerIndex;void 0!==s&&(this._controllers=this._controllers.map(e=>e.playerIndex===s?{...e,connected:!1}:e),this.logger.debug("Player disconnected",{playerIndex:s}),this._emitLifecycle("$controller-disconnect",s))}),this.registerHandler(i.PLAYER_RECONNECTED,e=>{const t=e,r=t.player,s=r?.playerIndex??t.playerIndex;if(void 0!==s){const e=g(r||{playerIndex:s,connected:!0},s);this._controllers=this._controllers.map(t=>t.playerIndex===s?e:t),this.logger.debug("Player reconnected",{playerIndex:s}),this._emitLifecycle("$controller-reconnect",s,e)}}),this.registerHandler(i.PLAYER_CHARACTER_UPDATED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=r.playerIndex,t=r.character??null;this._controllers=this._controllers.map(r=>r.playerIndex===e?{...r,appearance:t}:r),this.logger.debug("Player character updated",{playerIndex:e}),this._emitLifecycle("$character-updated",e,t??null)}}),this.registerHandler(i.RATE_LIMITED,e=>{const t=e,r=t?.event??"unknown";this.handleError(new n("RATE_LIMITED",`Server rate-limited event: ${r}`,{details:{event:r}}))}),this.registerHandler(i.GAME_OVER,e=>{const t=e;this.logger.lifecycle("Game over",t?.results),this._emitLifecycle("$game-over",t?.results)}),this.registerHandler(i.ALL_READY,()=>{this.logger.lifecycle("All participants ready"),this._allReadyFired=!0,this._emitLifecycle("$all-ready")}),this.registerHandler(i.SELF_DISCONNECTED,()=>{this._isConnected=!1,this.logger.lifecycle("Connection lost"),this._emitLifecycle("$connection-change",!1)}),this.registerHandler(i.SELF_RECONNECTED,()=>{this._isConnected=!0,this.logger.lifecycle("Connection restored"),this._emitLifecycle("$connection-change",!0)}),this.registerHandler(i.STATE_CHANGED,e=>{const t=e;"number"==typeof t?.playerIndex&&t.state&&(this._customStates.set(t.playerIndex,t.state),this._stateChangeListeners.forEach(e=>{try{e(t.playerIndex,t.state)}catch(e){this.handleError(new n("UNKNOWN","Error in custom state change listener",{cause:e instanceof Error?e:void 0}))}}))}),this.registerHandler(i.STATE_ALL,e=>{const t=e;if(t?.states){for(const[e,r]of Object.entries(t.states)){const t=Number(e);this._customStates.set(t,r),this._stateChangeListeners.forEach(e=>{try{e(t,r)}catch(e){this.handleError(new n("UNKNOWN","Error in custom state change listener",{cause:e instanceof Error?e:void 0}))}})}this._emitLifecycle("$state-recovery",t.states)}}))}setupUserEventHandler(e,t){const r=r=>{this.logReceive(e,r);try{t(r)}catch(t){this.handleError(new n("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}};this.transport&&(this.transport.on(e,r),this.registeredHandlers.push({event:e,handler:r}),this.handlerToTransport.set(t,{event:e,transportHandler:r}));let s=this.eventListeners.get(e);s||(s=new Set,this.eventListeners.set(e,s)),s.add(t)}registerHandler(e,t){this.transport&&(this.transport.on(e,t),this.registeredHandlers.push({event:e,handler:t}))}_addLifecycleListener(e,t){let r=this._lifecycleListeners.get(e);return r||(r=new Set,this._lifecycleListeners.set(e,r)),r.add(t),()=>{r.delete(t),0===r.size&&this._lifecycleListeners.delete(e)}}_emitLifecycle(e,...t){this._lifecycleListeners.get(e)?.forEach(r=>{try{r(...t)}catch(t){this.handleError(new n("UNKNOWN",`Error in lifecycle handler for "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}})}_hasLifecycleListeners(e){const t=this._lifecycleListeners.get(e);return void 0!==t&&t.size>0}onAllReady(e){return this._allReadyFired&&e(),this._addLifecycleListener("$all-ready",e)}onControllerJoin(e){return this._addLifecycleListener("$controller-join",e)}onControllerLeave(e){return this._addLifecycleListener("$controller-leave",e)}onControllerDisconnect(e){return this._addLifecycleListener("$controller-disconnect",e)}onControllerReconnect(e){return this._addLifecycleListener("$controller-reconnect",e)}onCharacterUpdated(e){return this._addLifecycleListener("$character-updated",e)}onError(e){return this._addLifecycleListener("$error",e)}onConnectionChange(e){return this._addLifecycleListener("$connection-change",e)}onGameOver(e){return this._addLifecycleListener("$game-over",e)}setState(e){const t={...this._customStates.get(this._myPlayerIndex)??{},...e};this._customStates.set(this._myPlayerIndex,t),this.transport&&this.transport.emit(i.STATE_SET,{state:e})}getMyState(){return this._customStates.get(this._myPlayerIndex)}onCustomStateChange(e){return this._stateChangeListeners.add(e),()=>{this._stateChangeListeners.delete(e)}}send(e,t){if(this._isDestroyed)throw new n("DESTROYED","Cannot call send() after destroy()");if(!this._isReady||!this.transport)return this._outboundBuffer.push({method:"send",args:[e,t]}),void this.logger.debug(`Buffered send "${e}" (controller not ready yet)`);a(e),y(t),"object"==typeof t&&null!==t||this.logger.warn('Event data should be an object. Primitive values will be wrapped as { data: value } by the relay server. To avoid confusion, wrap explicitly: send("event", { value: 42 }) instead of send("event", 42).'),this.logSend(e,t),this.transport.emit(e,t)}signalReady(){this.ensureReady("signalReady"),this.logSend(i.GAME_READY,{}),this.transport.emit(i.GAME_READY,{})}on(e,t){if("string"==typeof e&&e.startsWith("$")){const r=d;if(!r.has(e))throw new n("INVALID_EVENT",`Unknown lifecycle event: "${e}". Valid lifecycle events: ${Array.from(r).join(", ")}`);return"$all-ready"===e&&this._allReadyFired&&t(),this._addLifecycleListener(e,t)}a(e);let r=this.eventListeners.get(e);if(r||(r=new Set,this.eventListeners.set(e,r)),r.add(t),this.transport){const r=r=>{this.logReceive(e,r);try{t(r)}catch(t){this.handleError(new n("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}};this.transport.on(e,r),this.registeredHandlers.push({event:e,handler:r}),this.handlerToTransport.set(t,{event:e,transportHandler:r})}else this._pendingHandlers.push({event:e,handler:t});return()=>{r?.delete(t),0===r?.size&&this.eventListeners.delete(e),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t));const s=this.handlerToTransport.get(t);s&&(this.transport?.off(e,s.transportHandler),this.registeredHandlers=this.registeredHandlers.filter(e=>e.handler!==s.transportHandler),this.handlerToTransport.delete(t))}}once(e,t){if("string"==typeof e&&e.startsWith("$")){if(!d.has(e))throw new n("INVALID_EVENT",`Unknown lifecycle event: "${e}"`);if("$all-ready"===e&&this._allReadyFired)return t(),()=>{};const r=(...e)=>{s(),t(...e)},s=this._addLifecycleListener(e,r);return s}const r=this.on(e,e=>{r(),t(e)});return r}off(e,t){if("string"==typeof e&&e.startsWith("$"))t?this._lifecycleListeners.get(e)?.delete(t):this._lifecycleListeners.delete(e);else if(t){const r=this.eventListeners.get(e);r?.delete(t),0===r?.size&&this.eventListeners.delete(e);const s=this.handlerToTransport.get(t);s&&(this.transport?.off(e,s.transportHandler),this.registeredHandlers=this.registeredHandlers.filter(e=>e.handler!==s.transportHandler),this.handlerToTransport.delete(t)),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t))}else{this.eventListeners.delete(e),this.transport?.off(e),this.registeredHandlers=this.registeredHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}}removeAllListeners(e){if(e){this.eventListeners.delete(e),this.transport?.off(e),this.registeredHandlers=this.registeredHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}else for(const e of[...this.eventListeners.keys()])this.removeAllListeners(e)}destroy(){this._isDestroyed||(this.logger.lifecycle("Destroying controller"),this._isDestroyed=!0,this._isReady=!1,this.cleanup())}cleanup(){this._initTimeoutId&&(clearTimeout(this._initTimeoutId),this._initTimeoutId=null),this._isReady=!1;for(const{event:e,handler:t}of this.registeredHandlers)this.transport?.off(e,t);this.registeredHandlers=[],this.eventListeners.clear(),this.handlerToTransport.clear(),this._pendingHandlers=[],this._lifecycleListeners.clear(),this._customStates.clear(),this._stateChangeListeners.clear(),this._isConnected=!1,this._outboundBuffer=[],this.transport&&(this.transport.destroy(),this.transport=null),this.boundMessageHandler&&(window.removeEventListener("message",this.boundMessageHandler),this.boundMessageHandler=null)}ensureReady(e){if(this._isDestroyed)throw new n("DESTROYED",`Cannot call ${e}() after destroy()`,{details:{method:e}});if(!this._isReady||!this.transport)throw new n("NOT_READY",`Cannot call ${e}() before controller is ready. Use await controller.ready.`,{details:{method:e,isReady:this._isReady}})}handleError(e){this.logger.warn(`Error in handler: ${e.message}`);const t=e.toSmoreError();this._hasLifecycleListeners("$error")?this._emitLifecycle("$error",t):this.logger.error(e.message,e.details)}logSend(e,t){this.logger.send(e,t)}logReceive(e,t){this.logger.receive(e,t)}}e.LifecycleEvent={ALL_READY:"$all-ready",CONTROLLER_JOIN:"$controller-join",CONTROLLER_LEAVE:"$controller-leave",CONTROLLER_DISCONNECT:"$controller-disconnect",CONTROLLER_RECONNECT:"$controller-reconnect",CHARACTER_UPDATED:"$character-updated",ERROR:"$error",GAME_OVER:"$game-over",CONNECTION_CHANGE:"$connection-change"},e.SmoreSDKError=n,e.createController=function(e){return new f(e??{})},e.createScreen=function(e){return new p(e)}});
|
|
1
|
+
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).SmoreSDK={})}(this,function(e){"use strict";function t(e){return null!==e&&"object"==typeof e&&"type"in e&&"string"==typeof e.type&&e.type.startsWith("_bridge:")}function r(e){if(!e||"object"!=typeof e)throw new Error("[SDK] _bridge:init payload must be an object");const t=e;if("string"!=typeof t.side||!["host","player"].includes(t.side))throw new Error(`[SDK] _bridge:init payload.side must be "host" or "player", got: ${t.side}`);if("string"!=typeof t.roomCode||0===t.roomCode.length)throw new Error("[SDK] _bridge:init payload.roomCode must be a non-empty string");if(!Array.isArray(t.players))throw new Error("[SDK] _bridge:init payload.players must be an array");if(void 0!==t.myIndex&&"number"!=typeof t.myIndex)throw new Error("[SDK] _bridge:init payload.myIndex must be a number if provided");return!0}class n{static ACK_TIMEOUT=3e4;handlers=new Map;ackCallbacks=new Map;ackCounter=0;parentOrigin;boundMessageHandler;constructor(e="*"){this.parentOrigin=e,this.boundMessageHandler=this.handleMessage.bind(this),window.addEventListener("message",this.boundMessageHandler)}emit(e,...t){let r,s=t[0];if(1===t.length&&"function"==typeof t[0]){s=void 0;const e=t[0];r="ack_"+ ++this.ackCounter,this.ackCallbacks.set(r,e),setTimeout(()=>{this.ackCallbacks.has(r)&&this.ackCallbacks.delete(r)},n.ACK_TIMEOUT)}else if(t.length>=2&&"function"==typeof t[t.length-1]){s=t[0];const e=t[t.length-1];r="ack_"+ ++this.ackCounter,this.ackCallbacks.set(r,e),setTimeout(()=>{this.ackCallbacks.has(r)&&this.ackCallbacks.delete(r)},n.ACK_TIMEOUT)}window.parent.postMessage({type:"_bridge:emit",payload:{event:e,data:s,ackId:r}},this.parentOrigin)}on(e,t){let r=this.handlers.get(e);r||(r=new Set,this.handlers.set(e,r)),r.add(t)}off(e,t){t?this.handlers.get(e)?.delete(t):this.handlers.delete(e)}destroy(){window.removeEventListener("message",this.boundMessageHandler),this.handlers.clear(),this.ackCallbacks.clear()}handleMessage(e){if("*"!==this.parentOrigin&&e.origin!==this.parentOrigin)return;const r=e.data;if(t(r))if("_bridge:event"===r.type){const{event:e,data:t}=r.payload,n=this.handlers.get(e);n&&n.forEach(e=>e(t))}else if("_bridge:ack"===r.type){const{ackId:e,data:t}=r.payload,n=this.ackCallbacks.get(e);n&&(this.ackCallbacks.delete(e),n(t))}}}class s extends Error{code;cause;details;constructor(e,t,r){super(t,r?.cause?{cause:r.cause}:void 0),this.name="SmoreSDKError",this.code=e,this.cause=r?.cause,this.details=r?.details;const n=Error;"function"==typeof n.captureStackTrace&&n.captureStackTrace(this,s)}toSmoreError(){return{code:this.code,message:this.message,cause:this.cause,details:this.details}}}const i={GAME_OVER:"smore:game-over",PLAYER_JOINED:"smore:player-joined",PLAYER_LEFT:"smore:player-left",PLAYER_DISCONNECTED:"smore:player-disconnected",PLAYER_RECONNECTED:"smore:player-reconnected",PLAYER_CHARACTER_UPDATED:"smore:player-character-updated",RATE_LIMITED:"smore:rate-limited",GAME_READY:"smore:game-ready",ALL_READY:"smore:all-ready",SELF_DISCONNECTED:"smore:self-disconnected",SELF_RECONNECTED:"smore:self-reconnected",SEND_TO_PLAYER:"smore:send-to-player"};new Set(Object.values(i));const o=/^[a-zA-Z]([a-zA-Z0-9_-]*[a-zA-Z0-9])?$/;function l(e){if(!e||"string"!=typeof e)throw new s("INVALID_EVENT","Event name must be a non-empty string");if(e.length>128)throw new s("INVALID_EVENT",`Event name exceeds maximum length of 128 characters (got ${e.length}).`,{details:{event:e.slice(0,50)+"..."}});if(!o.test(e))throw new s("INVALID_EVENT",`Invalid event name "${e}". Event names must start with a letter, contain only letters, numbers, hyphens, or underscores, and end with a letter or number.`,{details:{event:e}})}const a=new Set(["$all-ready","$controller-join","$controller-leave","$controller-disconnect","$controller-reconnect","$character-updated","$error","$connection-change"]),d=new Set([...a,"$game-over"]);class c{enabled;level;prefix;logSend;logReceive;logLifecycle;customLogger;static levelOrder={debug:0,info:1,warn:2,error:3};constructor(e,t="[Smore]"){const r="boolean"==typeof e?{enabled:e}:e;this.enabled=r?.enabled??!1,this.level=r?.level??"debug",this.prefix=r?.prefix??t,this.logSend=r?.logSend??!0,this.logReceive=r?.logReceive??!0,this.logLifecycle=r?.logLifecycle??!0,this.customLogger=r?.logger}shouldLog(e){return this.enabled&&c.levelOrder[e]>=c.levelOrder[this.level]}log(e,t,r){if(!this.shouldLog(e))return;if(this.customLogger)return void this.customLogger(e,`${this.prefix} ${t}`,r);const n="error"===e?"error":"warn"===e?"warn":"debug"===e?"debug":"info";void 0!==r?console[n](`${this.prefix} ${t}`,r):console[n](`${this.prefix} ${t}`)}debug(e,t){this.log("debug",e,t)}info(e,t){this.log("info",e,t)}warn(e,t){this.log("warn",e,t)}error(e,t){this.log("error",e,t)}send(e,t){this.enabled&&this.logSend&&this.debug(`-> SEND: ${e}`,t)}receive(e,t){this.enabled&&this.logReceive&&this.debug(`<- RECV: ${e}`,t)}lifecycle(e,t){this.logLifecycle&&this.info(`[Lifecycle] ${e}`,t)}}const h=65536;function y(e,t){return{playerIndex:e.playerIndex??t,nickname:e.nickname||e.name||`Player ${(e.playerIndex??t)+1}`,connected:!1!==e.connected,appearance:e.appearance??e.character}}function g(e){if(null!=e)try{const t=JSON.stringify(e),r=(new TextEncoder).encode(t).byteLength;if(r>h)throw new s("PAYLOAD_TOO_LARGE",`Event payload exceeds maximum size of 65536 bytes (got ${r} bytes). The server will silently drop this event.`,{details:{size:r,limit:h}})}catch(e){if(e instanceof s)throw e}}class p{transport=null;config;logger;_controllers=[];_roomCode="";_isReady=!1;_isDestroyed=!1;_initTimeoutId=null;eventHandlers=new Map;registeredTransportHandlers=[];boundMessageHandler=null;handlerToTransport=new Map;_pendingHandlers=[];_lifecycleListeners=new Map;_outboundBuffer=[];_allReadyFired=!1;_isConnected=!1;_protocolVersion=1;_readyResolve;_readyReject;ready;constructor(e={}){this.config=e,this.logger=new c(e.debug,"[SmoreScreen]"),this.ready=new Promise((e,t)=>{this._readyResolve=e,this._readyReject=t}),this.startInitialization()}startInitialization(){this.logger.lifecycle("Initializing screen...");const e=this.config.parentOrigin??"*",i=this.config.timeout??1e4;this._initTimeoutId=setTimeout(()=>{this.cleanup();const e=new s("TIMEOUT",`Screen initialization timed out after ${i}ms. Make sure the parent frame sends _bridge:init. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Screen instance to retry (this instance has been cleaned up).`,{details:{timeout:i}});this.handleError(e),this._readyReject(e)},i),this.boundMessageHandler=i=>{if("*"!==e&&i.origin!==e)return;const o=i.data;if(t(o))if("_bridge:init"===o.type){clearTimeout(this._initTimeoutId);const t=o.payload;try{r(t)}catch(e){const r=new s("INIT_FAILED",`Invalid _bridge:init payload: ${e instanceof Error?e.message:String(e)}`,{details:{payload:t}});return this.logger.warn("_bridge:init validation failed",r),this.handleError(r),void this._readyReject(r)}const i=t;if("host"!==i.side){const e=new s("INIT_FAILED",`Received init for wrong side: ${i.side}. Expected "host".`,{details:{side:i.side}});return this.handleError(e),void this._readyReject(e)}this.transport=this.config.transport??new n(e),this._roomCode=i.roomCode;const l=i.protocolVersion;void 0!==l&&(this._protocolVersion=l,1!==l&&this.logger.warn(`Protocol version mismatch: SDK v1, server v${l}. Some features may not work correctly.`)),this._controllers=this.mapControllersFromInit(i.players),0===this._controllers.length&&this.logger.warn("Screen initialized with zero controllers"),this.setupEventHandlers();for(const{event:e,handler:t}of this._pendingHandlers)this.setupUserEventHandler(e,t);this._pendingHandlers=[],this._isConnected=!0,this._isReady=!0;for(const e of this._outboundBuffer)try{switch(e.method){case"broadcast":this.broadcast(e.args[0],e.args[1]);break;case"sendToController":this.sendToController(e.args[0],e.args[1],e.args[2])}}catch(e){this.handleError(e instanceof s?e:new s("UNKNOWN","Failed to flush buffered message"))}this._outboundBuffer=[],this.logger.lifecycle("Screen ready",{roomCode:this._roomCode,controllers:this._controllers.length}),!1!==this.config.autoReady&&(this.logger.lifecycle("Auto-signaling ready (autoReady enabled)"),this.signalReady()),this._readyResolve()}else if("_bridge:update"===o.type){if(!this._isReady)return void this.logger.debug("Ignoring _bridge:update before init completes");const e=o.payload;if(e.players&&Array.isArray(e.players)){const t=this._controllers,r=this.mapControllersFromInit(e.players);this._controllers=r;for(const e of r)t.some(t=>t.playerIndex===e.playerIndex)||(this.logger.lifecycle("Controller joined (via update)",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-join",e.playerIndex,e));for(const e of t)r.some(t=>t.playerIndex===e.playerIndex)||(this.logger.lifecycle("Controller left (via update)",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-leave",e.playerIndex))}this.logger.lifecycle("Room updated",{controllers:this._controllers.length})}},window.addEventListener("message",this.boundMessageHandler),window.parent.postMessage({type:"_bridge:ready",protocolVersion:1},e),this.logger.lifecycle("Sent _bridge:ready to parent")}mapControllersFromInit(e){return e.map((e,t)=>y(e,t))}setupEventHandlers(){this.transport&&(this.registerTransportHandler(i.PLAYER_JOINED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=y(r,r.playerIndex);if(this._controllers.some(t=>t.playerIndex===e.playerIndex))return;this._controllers=[...this._controllers,e],this.logger.lifecycle("Controller joined",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-join",e.playerIndex,e)}}),this.registerTransportHandler(i.PLAYER_LEFT,e=>{const t=e,r=t?.player?.playerIndex??t?.playerIndex;if("number"==typeof r){if(!this._controllers.some(e=>e.playerIndex===r))return;this._controllers=this._controllers.filter(e=>e.playerIndex!==r),this.logger.lifecycle("Controller left",{playerIndex:r}),this._emitLifecycle("$controller-leave",r)}}),this.registerTransportHandler(i.PLAYER_DISCONNECTED,e=>{const t=e,r=t?.player?.playerIndex??t?.playerIndex;"number"==typeof r&&(this._controllers=this._controllers.map(e=>e.playerIndex===r?{...e,connected:!1}:e),this.logger.lifecycle("Controller disconnected",{playerIndex:r}),this._emitLifecycle("$controller-disconnect",r))}),this.registerTransportHandler(i.PLAYER_RECONNECTED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=y(r,r.playerIndex);this._controllers=this._controllers.map(t=>t.playerIndex===e.playerIndex?e:t),this.logger.lifecycle("Controller reconnected",{playerIndex:e.playerIndex}),this._emitLifecycle("$controller-reconnect",e.playerIndex,e)}}),this.registerTransportHandler(i.PLAYER_CHARACTER_UPDATED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=r.playerIndex,t=r.character??null;this._controllers=this._controllers.map(r=>r.playerIndex===e?{...r,appearance:t}:r),this.logger.lifecycle("Player character updated",{playerIndex:e}),this._emitLifecycle("$character-updated",e,t??null)}}),this.registerTransportHandler(i.RATE_LIMITED,e=>{const t=e,r=t?.event??"unknown";this.handleError(new s("RATE_LIMITED",`Server rate-limited event: ${r}`,{details:{event:r}}))}),this.registerTransportHandler(i.ALL_READY,()=>{this.logger.lifecycle("All participants ready"),this._allReadyFired=!0,this._emitLifecycle("$all-ready")}),this.registerTransportHandler(i.SELF_DISCONNECTED,()=>{this._isConnected=!1,this.logger.lifecycle("Connection lost"),this._emitLifecycle("$connection-change",!1)}),this.registerTransportHandler(i.SELF_RECONNECTED,()=>{this._isConnected=!0,this.logger.lifecycle("Connection restored"),this._emitLifecycle("$connection-change",!0)}))}setupUserEventHandler(e,t){const r=r=>{this.logger.receive(e,r);const n=r,{playerIndex:i,...o}=n;if("number"==typeof i)try{t(i,o)}catch(t){this.handleError(new s("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e,playerIndex:i}}))}else this.logger.debug(`Dropping event "${e}" without playerIndex`,r)};this.registerTransportHandler(e,r);let n=this.eventHandlers.get(e);n||(n=new Set,this.eventHandlers.set(e,n)),n.add(t),this.handlerToTransport.set(t,{event:e,transportHandler:r})}registerTransportHandler(e,t){this.transport&&(this.transport.on(e,t),this.registeredTransportHandlers.push({event:e,handler:t}))}get controllers(){return[...this._controllers]}get roomCode(){return this._roomCode}get isReady(){return this._isReady}get isDestroyed(){return this._isDestroyed}get isConnected(){return this._isConnected}get protocolVersion(){return this._protocolVersion}_addLifecycleListener(e,t){let r=this._lifecycleListeners.get(e);return r||(r=new Set,this._lifecycleListeners.set(e,r)),r.add(t),()=>{r.delete(t),0===r.size&&this._lifecycleListeners.delete(e)}}_emitLifecycle(e,...t){this._lifecycleListeners.get(e)?.forEach(r=>{try{r(...t)}catch(t){this.handleError(new s("UNKNOWN",`Error in lifecycle handler for "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}})}_hasLifecycleListeners(e){const t=this._lifecycleListeners.get(e);return void 0!==t&&t.size>0}onAllReady(e){return this._allReadyFired&&e(),this._addLifecycleListener("$all-ready",e)}onControllerJoin(e){return this._addLifecycleListener("$controller-join",e)}onControllerLeave(e){return this._addLifecycleListener("$controller-leave",e)}onControllerDisconnect(e){return this._addLifecycleListener("$controller-disconnect",e)}onControllerReconnect(e){return this._addLifecycleListener("$controller-reconnect",e)}onCharacterUpdated(e){return this._addLifecycleListener("$character-updated",e)}onError(e){return this._addLifecycleListener("$error",e)}onConnectionChange(e){return this._addLifecycleListener("$connection-change",e)}broadcast(e,t){if(this._isDestroyed)throw new s("DESTROYED","Cannot call broadcast() after destroy()");if(!this._isReady||!this.transport)return this._outboundBuffer.push({method:"broadcast",args:[e,t]}),void this.logger.debug(`Buffered broadcast "${e}" (screen not ready yet)`);l(e),g(t),this.logger.send(e,t),this.transport.emit(e,t)}sendToController(e,t,r){if(this._isDestroyed)throw new s("DESTROYED","Cannot call sendToController() after destroy()");if(!this._isReady||!this.transport)return this._outboundBuffer.push({method:"sendToController",args:[e,t,r]}),void this.logger.debug(`Buffered sendToController "${t}" -> Player ${e} (screen not ready yet)`);l(t),function(e,t){if("number"!=typeof e||!Number.isInteger(e))throw new s("INVALID_PLAYER","Player index must be an integer");if(!t.some(t=>t.playerIndex===e))throw new s("INVALID_PLAYER",`No controller found with player index ${e}`,{details:{playerIndex:e}})}(e,this._controllers),g(r),r&&"object"==typeof r&&"targetPlayerIndex"in r&&this.logger.warn(`Event "${t}" data contains reserved field "targetPlayerIndex" which will be overwritten for routing.`),this.logger.send(`${t} -> Player ${e}`,r),this.transport.emit(t,{targetPlayerIndex:e,...r&&"object"==typeof r?r:{data:r}})}gameOver(e){this.ensureReady("gameOver"),this.logger.lifecycle("Game over",e),this.transport.emit(i.GAME_OVER,{results:e})}signalReady(){this.ensureReady("signalReady"),this.logger.lifecycle("Signaling ready"),this.transport.emit(i.GAME_READY,{})}on(e,t){if("string"==typeof e&&e.startsWith("$")){const r=a;if(!r.has(e))throw new s("INVALID_EVENT",`Unknown lifecycle event: "${e}". Valid lifecycle events: ${Array.from(r).join(", ")}`);return"$all-ready"===e&&this._allReadyFired&&t(),this._addLifecycleListener(e,t)}l(e);let r=this.eventHandlers.get(e);if(r||(r=new Set,this.eventHandlers.set(e,r)),r.add(t),this.transport){const r=r=>{this.logger.receive(e,r);const n=r,{playerIndex:i,...o}=n;if("number"==typeof i)try{t(i,o)}catch(t){this.handleError(new s("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0}))}else this.logger.debug(`Dropping event "${e}" without playerIndex`,r)};this.registerTransportHandler(e,r),this.handlerToTransport.set(t,{event:e,transportHandler:r})}else this._pendingHandlers.push({event:e,handler:t});return()=>{r?.delete(t),0===r?.size&&this.eventHandlers.delete(e),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t));const n=this.handlerToTransport.get(t);n&&(this.transport?.off(e,n.transportHandler),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(e=>e.handler!==n.transportHandler),this.handlerToTransport.delete(t))}}once(e,t){if("string"==typeof e&&e.startsWith("$")){if(!a.has(e))throw new s("INVALID_EVENT",`Unknown lifecycle event: "${e}"`);if("$all-ready"===e&&this._allReadyFired)return t(),()=>{};const r=(...e)=>{n(),t(...e)},n=this._addLifecycleListener(e,r);return n}const r=this.on(e,(e,n)=>{r(),t(e,n)});return r}off(e,t){if("string"==typeof e&&e.startsWith("$"))t?this._lifecycleListeners.get(e)?.delete(t):this._lifecycleListeners.delete(e);else if(t){const r=this.eventHandlers.get(e);r?.delete(t),0===r?.size&&this.eventHandlers.delete(e);const n=this.handlerToTransport.get(t);n&&(this.transport?.off(e,n.transportHandler),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(e=>e.handler!==n.transportHandler),this.handlerToTransport.delete(t)),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t))}else{this.eventHandlers.delete(e),this.transport?.off(e),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}}removeAllListeners(e){if(e){this.eventHandlers.delete(e),this.transport?.off(e),this.registeredTransportHandlers=this.registeredTransportHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}else for(const e of[...this.eventHandlers.keys()])this.removeAllListeners(e)}getController(e){return this._controllers.find(t=>t.playerIndex===e)}getControllerCount(){return this._controllers.filter(e=>e.connected).length}destroy(){this._isDestroyed||(this.logger.lifecycle("Destroying screen..."),this._isDestroyed=!0,this._isReady=!1,this.cleanup(),this.logger.lifecycle("Screen destroyed"))}cleanup(){this._initTimeoutId&&(clearTimeout(this._initTimeoutId),this._initTimeoutId=null);for(const{event:e,handler:t}of this.registeredTransportHandlers)this.transport?.off(e,t);this.registeredTransportHandlers=[],this.eventHandlers.clear(),this.handlerToTransport.clear(),this._pendingHandlers=[],this._lifecycleListeners.clear(),this._isConnected=!1,this._outboundBuffer=[],this.transport instanceof n&&this.transport.destroy(),this.transport=null,this.boundMessageHandler&&(window.removeEventListener("message",this.boundMessageHandler),this.boundMessageHandler=null)}handleError(e){this.logger.warn(`Error in handler: ${e.message}`);const t=e.toSmoreError();this._hasLifecycleListeners("$error")?this._emitLifecycle("$error",t):this.logger.error(e.message,e.details)}ensureReady(e){if(this._isDestroyed)throw new s("DESTROYED",`Cannot call ${e}() after destroy()`,{details:{method:e}});if(!this._isReady||!this.transport)throw new s("NOT_READY",`Cannot call ${e}() before screen is ready. Use await screen.ready.`,{details:{method:e}})}}class f{transport=null;config;logger;_roomCode="";_myPlayerIndex=-1;_isReady=!1;_isDestroyed=!1;_initTimeoutId=null;boundMessageHandler=null;registeredHandlers=[];eventListeners=new Map;handlerToTransport=new Map;_controllers=[];_pendingHandlers=[];_lifecycleListeners=new Map;_outboundBuffer=[];_allReadyFired=!1;_isConnected=!1;_protocolVersion=1;_readyResolve;_readyReject;ready;constructor(e={}){this.config=e,this.logger=new c(e.debug,"[SmoreController]"),this.ready=new Promise((e,t)=>{this._readyResolve=e,this._readyReject=t}),this.startInitialization()}get myPlayerIndex(){return this._myPlayerIndex}get roomCode(){return this._roomCode}get isReady(){return this._isReady}get isDestroyed(){return this._isDestroyed}get isConnected(){return this._isConnected}get protocolVersion(){return this._protocolVersion}get controllers(){return[...this._controllers]}get me(){return this._controllers.find(e=>e.playerIndex===this._myPlayerIndex)}getControllerCount(){return this._controllers.filter(e=>e.connected).length}getController(e){return this._controllers.find(t=>t.playerIndex===e)}startInitialization(){const e=this.config.parentOrigin??"*",r=this.config.timeout??1e4;this.logger.lifecycle("Initializing controller...",{parentOrigin:e,timeout:r}),this._initTimeoutId=setTimeout(()=>{this.cleanup();const e=new s("TIMEOUT",`Controller initialization timed out after ${r}ms. Make sure the parent window sends _bridge:init message. Check that the iframe has correct sandbox attributes (allow-scripts required) and same-origin/cross-origin settings. Create a new Controller instance to retry (this instance has been cleaned up).`,{details:{timeout:r}});this.handleError(e),this._readyReject(e)},r),this.boundMessageHandler=r=>{if("*"!==e&&r.origin!==e)return;const n=r.data;t(n)&&("_bridge:init"===n.type?(clearTimeout(this._initTimeoutId),this.handleInit(n,e)):"_bridge:update"===n.type&&this.handleUpdate(n))},window.addEventListener("message",this.boundMessageHandler),this.logger.lifecycle("Sending _bridge:ready to parent"),window.parent.postMessage({type:"_bridge:ready",protocolVersion:1},e)}handleInit(e,t){const i=e.payload;this.logger.debug("Received _bridge:init",i);try{r(i)}catch(e){const t=new s("INIT_FAILED",`Invalid _bridge:init payload: ${e instanceof Error?e.message:String(e)}`,{details:{payload:i}});return this.logger.warn("_bridge:init validation failed",t),this.handleError(t),void this._readyReject(t)}const o=i;if("player"!==o.side){const e=new s("INIT_FAILED",`Controller received init for wrong side: ${o.side}`,{details:{side:o.side}});return this.handleError(e),void this._readyReject(e)}if(void 0===o.myIndex){const e=new s("INIT_FAILED","Missing myIndex in init payload",{details:o});return this.handleError(e),void this._readyReject(e)}this.transport=this.config.transport??new n(t),this._roomCode=o.roomCode;const l=o.protocolVersion;void 0!==l&&(this._protocolVersion=l,1!==l&&this.logger.warn(`Protocol version mismatch: SDK v1, server v${l}. Some features may not work correctly.`)),this._myPlayerIndex=o.myIndex;const a=o.players;this._controllers=a.filter(e=>"number"==typeof e.playerIndex).map((e,t)=>y(e,t)),this.setupEventHandlers();for(const{event:e,handler:t}of this._pendingHandlers)this.setupUserEventHandler(e,t);this._pendingHandlers=[],this._isConnected=!0,this._isReady=!0;for(const e of this._outboundBuffer)try{if("send"===e.method)this.send(e.args[0],e.args[1])}catch(e){this.handleError(e instanceof s?e:new s("UNKNOWN","Failed to flush buffered message"))}this._outboundBuffer=[],this.logger.lifecycle("Controller ready",{roomCode:this._roomCode,myIndex:this._myPlayerIndex}),!1!==this.config.autoReady&&(this.logger.lifecycle("Auto-signaling ready (autoReady enabled)"),this.signalReady()),this._readyResolve()}handleUpdate(e){if(!this._isReady)return void this.logger.debug("Ignoring _bridge:update before init completes");const t=e.payload;if(this.logger.debug("Received _bridge:update",t),t.players&&Array.isArray(t.players)){const e=t.players.filter(e=>"number"==typeof e.playerIndex).map((e,t)=>y(e,t)),r=this._controllers;for(const t of e)r.some(e=>e.playerIndex===t.playerIndex)||this._emitLifecycle("$controller-join",t.playerIndex,t);for(const t of r)e.some(e=>e.playerIndex===t.playerIndex)||this._emitLifecycle("$controller-leave",t.playerIndex);for(const t of e){const e=r.find(e=>e.playerIndex===t.playerIndex);e&&(e.connected&&!t.connected&&(this.logger.debug("Player disconnected (via update)",{playerIndex:t.playerIndex}),this._emitLifecycle("$controller-disconnect",t.playerIndex)),!e.connected&&t.connected&&(this.logger.debug("Player reconnected (via update)",{playerIndex:t.playerIndex}),this._emitLifecycle("$controller-reconnect",t.playerIndex,t)))}this._controllers=e}}setupEventHandlers(){this.transport&&(this.registerHandler(i.PLAYER_JOINED,e=>{const t=e,r=t.player,n=r?.playerIndex??t.playerIndex;if(void 0!==n){if(this._controllers.some(e=>e.playerIndex===n))return;const e=y(r||{playerIndex:n,connected:!0},n);this._controllers=[...this._controllers,e],this.logger.debug("Player joined",{playerIndex:n}),this._emitLifecycle("$controller-join",n,e)}}),this.registerHandler(i.PLAYER_LEFT,e=>{const t=e,r=t.player?.playerIndex??t.playerIndex;if(void 0!==r){if(!this._controllers.some(e=>e.playerIndex===r))return;this._controllers=this._controllers.filter(e=>e.playerIndex!==r),this.logger.debug("Player left",{playerIndex:r}),this._emitLifecycle("$controller-leave",r)}}),this.registerHandler(i.PLAYER_DISCONNECTED,e=>{const t=e,r=t.player,n=r?.playerIndex??t.playerIndex;void 0!==n&&(this._controllers=this._controllers.map(e=>e.playerIndex===n?{...e,connected:!1}:e),this.logger.debug("Player disconnected",{playerIndex:n}),this._emitLifecycle("$controller-disconnect",n))}),this.registerHandler(i.PLAYER_RECONNECTED,e=>{const t=e,r=t.player,n=r?.playerIndex??t.playerIndex;if(void 0!==n){const e=y(r||{playerIndex:n,connected:!0},n);this._controllers=this._controllers.map(t=>t.playerIndex===n?e:t),this.logger.debug("Player reconnected",{playerIndex:n}),this._emitLifecycle("$controller-reconnect",n,e)}}),this.registerHandler(i.PLAYER_CHARACTER_UPDATED,e=>{const t=e,r=t?.player;if(r&&"number"==typeof r.playerIndex){const e=r.playerIndex,t=r.character??null;this._controllers=this._controllers.map(r=>r.playerIndex===e?{...r,appearance:t}:r),this.logger.debug("Player character updated",{playerIndex:e}),this._emitLifecycle("$character-updated",e,t??null)}}),this.registerHandler(i.RATE_LIMITED,e=>{const t=e,r=t?.event??"unknown";this.handleError(new s("RATE_LIMITED",`Server rate-limited event: ${r}`,{details:{event:r}}))}),this.registerHandler(i.GAME_OVER,e=>{const t=e;this.logger.lifecycle("Game over",t?.results),this._emitLifecycle("$game-over",t?.results)}),this.registerHandler(i.ALL_READY,()=>{this.logger.lifecycle("All participants ready"),this._allReadyFired=!0,this._emitLifecycle("$all-ready")}),this.registerHandler(i.SELF_DISCONNECTED,()=>{this._isConnected=!1,this.logger.lifecycle("Connection lost"),this._emitLifecycle("$connection-change",!1)}),this.registerHandler(i.SELF_RECONNECTED,()=>{this._isConnected=!0,this.logger.lifecycle("Connection restored"),this._emitLifecycle("$connection-change",!0)}))}setupUserEventHandler(e,t){const r=r=>{this.logReceive(e,r);try{t(r)}catch(t){this.handleError(new s("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}};this.transport&&(this.transport.on(e,r),this.registeredHandlers.push({event:e,handler:r}),this.handlerToTransport.set(t,{event:e,transportHandler:r}));let n=this.eventListeners.get(e);n||(n=new Set,this.eventListeners.set(e,n)),n.add(t)}registerHandler(e,t){this.transport&&(this.transport.on(e,t),this.registeredHandlers.push({event:e,handler:t}))}_addLifecycleListener(e,t){let r=this._lifecycleListeners.get(e);return r||(r=new Set,this._lifecycleListeners.set(e,r)),r.add(t),()=>{r.delete(t),0===r.size&&this._lifecycleListeners.delete(e)}}_emitLifecycle(e,...t){this._lifecycleListeners.get(e)?.forEach(r=>{try{r(...t)}catch(t){this.handleError(new s("UNKNOWN",`Error in lifecycle handler for "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}})}_hasLifecycleListeners(e){const t=this._lifecycleListeners.get(e);return void 0!==t&&t.size>0}onAllReady(e){return this._allReadyFired&&e(),this._addLifecycleListener("$all-ready",e)}onControllerJoin(e){return this._addLifecycleListener("$controller-join",e)}onControllerLeave(e){return this._addLifecycleListener("$controller-leave",e)}onControllerDisconnect(e){return this._addLifecycleListener("$controller-disconnect",e)}onControllerReconnect(e){return this._addLifecycleListener("$controller-reconnect",e)}onCharacterUpdated(e){return this._addLifecycleListener("$character-updated",e)}onError(e){return this._addLifecycleListener("$error",e)}onConnectionChange(e){return this._addLifecycleListener("$connection-change",e)}onGameOver(e){return this._addLifecycleListener("$game-over",e)}send(e,t){if(this._isDestroyed)throw new s("DESTROYED","Cannot call send() after destroy()");if(!this._isReady||!this.transport)return this._outboundBuffer.push({method:"send",args:[e,t]}),void this.logger.debug(`Buffered send "${e}" (controller not ready yet)`);l(e),g(t),"object"==typeof t&&null!==t||this.logger.warn('Event data should be an object. Primitive values will be wrapped as { data: value } by the relay server. To avoid confusion, wrap explicitly: send("event", { value: 42 }) instead of send("event", 42).'),this.logSend(e,t),this.transport.emit(e,t)}signalReady(){this.ensureReady("signalReady"),this.logSend(i.GAME_READY,{}),this.transport.emit(i.GAME_READY,{})}on(e,t){if("string"==typeof e&&e.startsWith("$")){const r=d;if(!r.has(e))throw new s("INVALID_EVENT",`Unknown lifecycle event: "${e}". Valid lifecycle events: ${Array.from(r).join(", ")}`);return"$all-ready"===e&&this._allReadyFired&&t(),this._addLifecycleListener(e,t)}l(e);let r=this.eventListeners.get(e);if(r||(r=new Set,this.eventListeners.set(e,r)),r.add(t),this.transport){const r=r=>{this.logReceive(e,r);try{t(r)}catch(t){this.handleError(new s("UNKNOWN",`Error in handler for event "${e}"`,{cause:t instanceof Error?t:void 0,details:{event:e}}))}};this.transport.on(e,r),this.registeredHandlers.push({event:e,handler:r}),this.handlerToTransport.set(t,{event:e,transportHandler:r})}else this._pendingHandlers.push({event:e,handler:t});return()=>{r?.delete(t),0===r?.size&&this.eventListeners.delete(e),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t));const n=this.handlerToTransport.get(t);n&&(this.transport?.off(e,n.transportHandler),this.registeredHandlers=this.registeredHandlers.filter(e=>e.handler!==n.transportHandler),this.handlerToTransport.delete(t))}}once(e,t){if("string"==typeof e&&e.startsWith("$")){if(!d.has(e))throw new s("INVALID_EVENT",`Unknown lifecycle event: "${e}"`);if("$all-ready"===e&&this._allReadyFired)return t(),()=>{};const r=(...e)=>{n(),t(...e)},n=this._addLifecycleListener(e,r);return n}const r=this.on(e,e=>{r(),t(e)});return r}off(e,t){if("string"==typeof e&&e.startsWith("$"))t?this._lifecycleListeners.get(e)?.delete(t):this._lifecycleListeners.delete(e);else if(t){const r=this.eventListeners.get(e);r?.delete(t),0===r?.size&&this.eventListeners.delete(e);const n=this.handlerToTransport.get(t);n&&(this.transport?.off(e,n.transportHandler),this.registeredHandlers=this.registeredHandlers.filter(e=>e.handler!==n.transportHandler),this.handlerToTransport.delete(t)),this._pendingHandlers=this._pendingHandlers.filter(r=>!(r.event===e&&r.handler===t))}else{this.eventListeners.delete(e),this.transport?.off(e),this.registeredHandlers=this.registeredHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}}removeAllListeners(e){if(e){this.eventListeners.delete(e),this.transport?.off(e),this.registeredHandlers=this.registeredHandlers.filter(t=>t.event!==e);for(const[t,r]of this.handlerToTransport)r.event===e&&this.handlerToTransport.delete(t);this._pendingHandlers=this._pendingHandlers.filter(t=>t.event!==e)}else for(const e of[...this.eventListeners.keys()])this.removeAllListeners(e)}destroy(){this._isDestroyed||(this.logger.lifecycle("Destroying controller"),this._isDestroyed=!0,this._isReady=!1,this.cleanup())}cleanup(){this._initTimeoutId&&(clearTimeout(this._initTimeoutId),this._initTimeoutId=null),this._isReady=!1;for(const{event:e,handler:t}of this.registeredHandlers)this.transport?.off(e,t);this.registeredHandlers=[],this.eventListeners.clear(),this.handlerToTransport.clear(),this._pendingHandlers=[],this._lifecycleListeners.clear(),this._isConnected=!1,this._outboundBuffer=[],this.transport&&(this.transport.destroy(),this.transport=null),this.boundMessageHandler&&(window.removeEventListener("message",this.boundMessageHandler),this.boundMessageHandler=null)}ensureReady(e){if(this._isDestroyed)throw new s("DESTROYED",`Cannot call ${e}() after destroy()`,{details:{method:e}});if(!this._isReady||!this.transport)throw new s("NOT_READY",`Cannot call ${e}() before controller is ready. Use await controller.ready.`,{details:{method:e,isReady:this._isReady}})}handleError(e){this.logger.warn(`Error in handler: ${e.message}`);const t=e.toSmoreError();this._hasLifecycleListeners("$error")?this._emitLifecycle("$error",t):this.logger.error(e.message,e.details)}logSend(e,t){this.logger.send(e,t)}logReceive(e,t){this.logger.receive(e,t)}}e.GAME_CATEGORIES={bet:{emoji:"π°",label:"Betting",labelKo:"λ΄κΈ°"},coop:{emoji:"π€",label:"Co-op",labelKo:"νλ"},versus:{emoji:"βοΈ",label:"Versus",labelKo:"λκ²°"},"time-attack":{emoji:"β±οΈ",label:"Time Attack",labelKo:"νμμ΄ν"},survival:{emoji:"π",label:"Survival",labelKo:"μμ‘΄"},reflex:{emoji:"π―",label:"Reflex",labelKo:"λ°μμλ"},deception:{emoji:"π΅οΈ",label:"Deception",labelKo:"μΆλ¦¬/μμμ"},creative:{emoji:"π¨",label:"Creative",labelKo:"μ°½μ"},party:{emoji:"π",label:"Party",labelKo:"νν°"},puzzle:{emoji:"π§©",label:"Puzzle",labelKo:"νΌμ¦"},board:{emoji:"π²",label:"Board Game",labelKo:"보λκ²μ"},strategy:{emoji:"π§ ",label:"Strategy",labelKo:"μ λ΅"},timing:{emoji:"π΅",label:"Timing",labelKo:"νμ΄λ°"}},e.LifecycleEvent={ALL_READY:"$all-ready",CONTROLLER_JOIN:"$controller-join",CONTROLLER_LEAVE:"$controller-leave",CONTROLLER_DISCONNECT:"$controller-disconnect",CONTROLLER_RECONNECT:"$controller-reconnect",CHARACTER_UPDATED:"$character-updated",ERROR:"$error",GAME_OVER:"$game-over",CONNECTION_CHANGE:"$connection-change"},e.SmoreSDKError=s,e.createController=function(e){return new f(e??{})},e.createScreen=function(e){return new p(e)}});
|
|
2
2
|
//# sourceMappingURL=smore-sdk.umd.min.js.map
|