live-quiz 0.1.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.
@@ -0,0 +1 @@
1
+ (function(S,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(S=typeof globalThis<"u"?globalThis:S||self,f(S.LiveQuizParticipant={}))})(this,(function(S){"use strict";var je=Object.defineProperty;var Oe=(S,f,E)=>f in S?je(S,f,{enumerable:!0,configurable:!0,writable:!0,value:E}):S[f]=E;var F=(S,f,E)=>Oe(S,typeof f!="symbol"?f+"":f,E);let f=()=>({emit(n,...e){for(let t=this.events[n]||[],s=0,i=t.length;s<i;s++)t[s](...e)},events:{},on(n,e){var t;return((t=this.events)[n]||(t[n]=[])).push(e),()=>{var s;this.events[n]=(s=this.events[n])==null?void 0:s.filter(i=>e!==i)}}});class E extends Error{constructor(e,t){e instanceof Error?(super(e.message),this.cause=e):super(e),this.reason=t,this.name="ReasonError"}}class Q extends E{constructor(e){super("Rejected",e),this.name="SubscriptionRejectedError"}}class ee extends E{constructor(e){super(e||"Timed out to receive subscription ack"),this.name="SubscriptionTimeoutError"}}class _ extends E{constructor(e,t){t?super(e,t):super("Disconnected",e),this.name="DisconnectedError"}}class te extends _{constructor(e){super(e,"stale_connection"),this.name="StaleConnectionError"}}function D(n){return n?`{${Object.keys(n).sort().filter(t=>n[t]!==void 0).map(t=>{let s=JSON.stringify(n[t]);return`${JSON.stringify(t)}:${s}`}).join(",")}}`:""}class se{constructor(e){this.channel=e,this.listeners=[]}watch(){this.listeners.push(this.channel.on("presence",e=>{if(e.type==="info"){this._state||(this._state=this.stateFromInfo(e));return}this._state&&(e.type==="join"?this._state[e.id]=e.info:e.type==="leave"&&delete this._state[e.id])}))}reset(){delete this._state}dispose(){delete this._info,delete this._state,this.listeners.forEach(e=>e()),this.listeners.length=0}async join(e,t){if(!this._info)return this._info={id:String(e),info:t},this.channel.perform("$presence:join",this._info)}async leave(){if(!this._info)return;let e=await this.channel.perform("$presence:leave");return delete this._info,e}async info(){return this._state?this._state:(this._promise||(this._promise=this._sync()),await this._promise,this._state)}async _sync(){this.watch();try{let e=await this.channel.perform("$presence:info",{});return this._state=this.stateFromInfo(e),this._state}finally{delete this._promise}}stateFromInfo(e){return e.records?e.records.reduce((t,{id:s,info:i})=>(t[s]=i,t),{}):{}}}const C=Symbol("state");class G{constructor(e={}){this.emitter=f(),this.params=Object.freeze(e),this.presence=new se(this),this.initialConnect=!0,this[C]="idle"}get identifier(){return this._identifier?this._identifier:(this._identifier=D({channel:this.channelId,...this.params}),this._identifier)}get channelId(){return this.constructor.identifier}get state(){return this[C]}attached(e){if(this.receiver){if(this.receiver!==e)throw Error("Already connected to a different receiver");return!1}return this.receiver=e,!0}connecting(){this[C]="connecting"}connected(){if(this.state==="connected"||this.state==="closed")return;this[C]="connected";let e=!1;this.initialConnect?(this.initialConnect=!1,this.emit("connect",{reconnect:!1,restored:e})):this.emit("connect",{reconnect:!0,restored:e})}restored(){if(this.state==="connected")throw Error("Already connected");this[C]="connected";let e=!0,t=!0;this.initialConnect=!1,this.emit("connect",{reconnect:t,restored:e})}disconnected(e){this.state==="disconnected"||this.state==="closed"||(this[C]="disconnected",this.presence.reset(),this.emit("disconnect",e))}closed(e){this.state!=="closed"&&(this[C]="closed",delete this.receiver,this.initialConnect=!0,this.presence.dispose(),this.emit("close",e))}disconnect(){this.state==="idle"||this.state==="closed"||this.receiver.unsubscribe(this)}async perform(e,t){if(this.state==="idle"||this.state==="closed")throw Error("Channel is not subscribed");return this.receiver.perform(this.identifier,e,t)}async send(e){return this.perform(void 0,e)}async whisper(e){try{await this.perform("$whisper",e)}catch(t){let s=this.receiver?this.receiver.logger:null;s&&s.warn("whisper failed: ",t)}}receive(e,t){this.emit("message",e,t)}on(e,t){return this.emitter.on(e,t)}once(e,t){let s=this.emitter.on(e,(...i)=>{s(),t(...i)});return s}emit(e,...t){return this.emitter.emit(e,...t)}ensureSubscribed(){return this.state==="connected"?Promise.resolve():this.state==="closed"?Promise.reject(Error("Channel is unsubscribed")):this.pendingSubscribe()}pendingSubscribe(){return this._pendingSubscribe?this._pendingSubscribe:(this._pendingSubscribe=new Promise((e,t)=>{let s=[()=>delete this._pendingSubscribe];s.push(this.on("connect",()=>{s.forEach(i=>i()),e()})),s.push(this.on("close",i=>{s.forEach(r=>r()),t(i||new E("Channel was disconnected before subscribing","canceled"))}))}),this._pendingSubscribe)}}class ie{constructor(e){this.id=e,this.intent="unsubscribed",this.state="idle",this.channels=[],this.disposed=!1,this._pendings=[]}add(e){this.channels.includes(e)||this.channels.push(e)}remove(e){let t=this.channels.indexOf(e);t>-1&&this.channels.splice(t,1)}notify(e,...t){this.state=e==="restored"?"connected":e,t.length===1?this.channels.forEach(s=>s[e](t[0])):this.channels.forEach(s=>s[e]())}pending(e){this._checkIntent(e);let t=this._pendings[0];return!t||t.intent!==e?Promise.resolve():t.promise}ensureResubscribed(){this.disposed||(this.intent=void 0,this.ensureSubscribed())}ensureSubscribed(){if(this.intent==="subscribed")return;if(this.disposed)throw Error("Subscription is disposed");this.intent="subscribed",!this._mergeWithPending("unsubscribed")&&this.subscriber(this)}maybeUnsubscribe(){this.disposed||this.intent==="unsubscribed"||this.channels.length>0||(this.intent="unsubscribed",this._mergeWithPending("subscribed"))||this.unsubscriber(this)}async acquire(e){this._checkIntent(e);let t,i={promise:new Promise(o=>{t=o}),intent:e,release:()=>{this._pendings.splice(this._pendings.indexOf(i),1),t(i)},canceled:!1,acquired:!1},r=this._pendingTop;return this._pendings.push(i),r&&await r.promise,this.gvl&&await this.gvl.acquire(i,e),i.acquired=!0,i}close(e){this.disposed=!0,this.intent=void 0,this.notify("closed",e)}_checkIntent(e){if(!(e==="unsubscribed"||e==="subscribed"))throw Error(`Unknown subscription intent: ${e}`)}get _pendingTop(){return this._pendings.length?this._pendings[this._pendings.length-1]:void 0}_mergeWithPending(e){let t=this._pendingTop;return!t||t.acquired||t.intent!==e?!1:(this._pendings.pop(),t.canceled=!0,!0)}}class ne{constructor(){this.queue=[]}async acquire(e,t){t==="subscribed"&&(this.queue.push(e.promise.then(()=>{this.queue.splice(this.queue.indexOf(e),1)})),this.queue.length>1&&await this.queue[this.queue.length-2])}}class re{constructor(e){e.concurrentSubscribes===!1&&(this.glv=new ne),this._subscriptions={},this._localToRemote={}}all(){return Object.values(this._subscriptions)}get(e){return this._subscriptions[e]}create(e,{subscribe:t,unsubscribe:s}){let i=this._subscriptions[e]=new ie(e);return i.remoteId=this._localToRemote[e],i.subscriber=t,i.unsubscriber=s,i.gvl=this.glv,i}remove(e){delete this._subscriptions[e],delete this._localToRemote[e]}storeRemoteId(e,t){this._localToRemote[e]=t;let s=this.get(e);s&&(s.remoteId=t)}}class oe{constructor(e={}){this.subscriptions=new re(e),this._pendingMessages=[],this._remoteToLocal={}}subscribe(e,t){this._remoteToLocal[t]=e,this.subscriptions.storeRemoteId(e,t),this.flush(t)}unsubscribe(e){let t=this.subscriptions.get(e);if(!t)return;let s=t.remoteId;s&&delete this._remoteToLocal[s],this.subscriptions.remove(e)}transmit(e,t,s){let i=this._remoteToLocal[e];if(!i){this._pendingMessages.push([e,t,s]);return}let r=this.subscriptions.get(i);r&&r.channels.forEach(o=>{o.receive(t,s)})}notify(e,t,s){let i=this._remoteToLocal[e];if(!i)return;let r=this.subscriptions.get(i);r&&r.channels.forEach(o=>o.emit(t,s))}close(){this._pendingMessages.length=0}get size(){return this.channels.length}get channels(){return this.subscriptions.all().flatMap(e=>e.channels)}flush(e){let t=[];for(let s of this._pendingMessages)s[0]===e?this.transmit(s[0],s[1],s[2]):t.push(s);this._pendingMessages=t}}const W={debug:0,info:1,warn:2,error:3};class J{constructor(e){this.level=e||"warn"}log(e,t,s){W[e]<W[this.level]||this.writeLogEntry(e,t,s)}writeLogEntry(){throw Error("Not implemented")}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)}}class L extends J{writeLogEntry(){}}class ce{encode(e){return JSON.stringify(e)}decode(e){try{return JSON.parse(e)}catch{}}}let ae=0;class V{constructor(e={}){let{logger:t}=e;this.logger=t||new L,this.pendingSubscriptions={},this.pendingUnsubscriptions={},this.subscribeCooldownInterval=e.subscribeCooldownInterval||250,this.subscribeRetryInterval=e.subscribeRetryInterval||5e3}attached(e){this.cable=e}subscribe(e,t){let s={channel:e};t&&Object.assign(s,t);let i=D(s);if(this.pendingUnsubscriptions[i]){let o=this.subscribeCooldownInterval*1.5;return this.logger.debug(`unsubscribed recently, cooldown for ${o}`,i),new Promise(c=>{setTimeout(()=>{c(this.subscribe(e,t))},o)})}if(this.pendingSubscriptions[i])return this.logger.warn("subscription is already pending, skipping",i),Promise.reject(Error("Already subscribing"));let r=this.subscribeRetryInterval;return new Promise((o,c)=>{let d=++ae;this.pendingSubscriptions[i]={resolve:o,reject:c,id:d},this.cable.send(this.buildSubscribeRequest(i)),this.maybeRetrySubscribe(d,i,r)})}buildSubscribeRequest(e){return{command:"subscribe",identifier:e}}maybeRetrySubscribe(e,t,s){setTimeout(()=>{let i=this.pendingSubscriptions[t];i&&i.id===e&&(this.logger.warn(`no subscription ack received in ${s}ms, retrying subscribe`,t),this.cable.send(this.buildSubscribeRequest(t)),this.maybeExpireSubscribe(e,t,s))},s)}maybeExpireSubscribe(e,t,s){setTimeout(()=>{let i=this.pendingSubscriptions[t];i&&i.id===e&&(delete this.pendingSubscriptions[t],i.reject(new ee(`Haven't received subscription ack in ${s*2}ms for ${t}`)))},s)}unsubscribe(e){return this.cable.send({command:"unsubscribe",identifier:e}),this.pendingUnsubscriptions[e]=!0,setTimeout(()=>{delete this.pendingUnsubscriptions[e]},this.subscribeCooldownInterval),Promise.resolve()}perform(e,t,s){return t==="$whisper"?this.whisper(e,s):(s||(s={}),s.action||(s.action=t),this.cable.send({command:"message",identifier:e,data:JSON.stringify(s)}),Promise.resolve())}whisper(e,t){return this.cable.send({command:"whisper",identifier:e,data:t}),Promise.resolve()}receive(e){if(typeof e!="object"){this.logger.error("unsupported message format",{message:e});return}let{type:t,identifier:s,message:i,reason:r,reconnect:o}=e;if(t==="ping")return this.cable.keepalive(e.message);if(this.cable.keepalive(),t==="welcome"){let c=e.sid;return c&&this.cable.setSessionId(c),this.cable.connected()}if(t==="disconnect"){let c=new _(r);this.reset(c),o===!1?this.cable.closed(c):this.cable.disconnected(c);return}if(t==="confirm_subscription"){let c=this.pendingSubscriptions[s];if(!c){this.logger.error("subscription not found, unsubscribing",{type:t,identifier:s}),this.unsubscribe(s);return}return delete this.pendingSubscriptions[s],c.resolve(s)}if(t==="reject_subscription"){let c=this.pendingSubscriptions[s];return c?(delete this.pendingSubscriptions[s],c.reject(new Q)):this.logger.error("subscription not found",{type:t,identifier:s})}if(i)return{identifier:s,message:i};this.logger.warn(`unknown message type: ${t}`,{message:e})}reset(e){for(let t in this.pendingSubscriptions)this.pendingSubscriptions[t].reject(e);this.pendingSubscriptions={}}recoverableClosure(){return!1}}const H=()=>Date.now()/1e3|0;class he extends V{constructor(e={}){super(e),this.streamsPositions={},this.subscriptionStreams={},this.pendingHistory={},this.pendingPresence={},this.presenceInfo={},this.restoreSince=e.historyTimestamp,this.disableSessionRecovery=e.disableSessionRecovery,this.restoreSince===void 0&&(this.restoreSince=H()),this.sessionId=void 0,this.sendPongs=e.pongs}reset(e){for(let t in this.pendingPresence)this.pendingPresence[t].reject(e);return this.pendingPresence={},super.reset()}receive(e){if(typeof e!="object"){this.logger.error("unsupported message format",{message:e});return}let{type:t,identifier:s,message:i}=e;if(t==="disconnect")return delete this.sessionId,this.cable.setSessionId(""),super.receive(e);if(t==="reject_subscription")return super.receive(e);if(t==="confirm_subscription")return this.subscriptionStreams[s]||(this.subscriptionStreams[s]=new Set),super.receive(e);if(t==="ping")return this.restoreSince&&(this.restoreSince=H()),this.sendPongs&&this.sendPong(),this.cable.keepalive(e.message);if(this.cable.keepalive(),t==="confirm_history"){this.logger.debug("history result received",e),this.cable.notify("history_received",s);return}if(t==="reject_history"){this.logger.warn("failed to retrieve history",e),this.cable.notify("history_not_found",s);return}if(t==="welcome"){if(this.disableSessionRecovery||(this.sessionId=e.sid,this.sessionId&&this.cable.setSessionId(this.sessionId)),e.restored){let r=e.restored_ids||Object.keys(this.subscriptionStreams);for(let o of r)this.cable.send({identifier:o,command:"history",history:this.historyRequestFor(o)}),this.presenceInfo[o]&&this.cable.send({identifier:o,command:"join",presence:this.presenceInfo[o]});return this.cable.restored(r)}return this.cable.connected(this.sessionId)}if(t==="presence"){let r=i.type;if(r==="info"){let o=this.pendingPresence[s];o&&(delete this.pendingPresence[s],o.resolve(i))}else if(r==="error"){let o=this.pendingPresence[s];o&&(delete this.pendingPresence[s],o.reject(new Error("failed to retrieve presence")))}return{type:t,identifier:s,message:i}}if(i){let r=this.trackStreamPosition(s,e.stream_id,e.epoch,e.offset);return{identifier:s,message:i,meta:r}}this.logger.warn(`unknown message type: ${t}`,{message:e})}perform(e,t,s){switch(t){case"$presence:join":return this.join(e,s);case"$presence:leave":return this.leave(e,s);case"$presence:info":return this.presence(e,s)}return super.perform(e,t,s)}unsubscribe(e){return delete this.presenceInfo[e],super.unsubscribe(e)}buildSubscribeRequest(e){let t=super.buildSubscribeRequest(e),s=this.historyRequestFor(e);s&&(t.history=s,this.pendingHistory[e]=!0);let i=this.presenceInfo[e];return i&&(t.presence=i),t}recoverableClosure(){return!!this.sessionId}historyRequestFor(e){let t={},s=!1;if(this.subscriptionStreams[e])for(let i of this.subscriptionStreams[e]){let r=this.streamsPositions[i];r&&(s=!0,t[i]=r)}if(!(!s&&!this.restoreSince))return{since:this.restoreSince,streams:t}}trackStreamPosition(e,t,s,i){if(!(!t||!s))return this.subscriptionStreams[e]||(this.subscriptionStreams[e]=new Set),this.subscriptionStreams[e].add(t),this.streamsPositions[t]={epoch:s,offset:i},{stream:t,epoch:s,offset:i}}async sendPong(){await new Promise(e=>setTimeout(e,0)),this.cable.state==="connected"&&this.cable.send({command:"pong"})}async join(e,t){return this.presenceInfo[e]=t,this.cable.send({command:"join",identifier:e,presence:t}),Promise.resolve()}async leave(e,t){return delete this.presenceInfo[e],this.cable.send({command:"leave",identifier:e,presence:t}),Promise.resolve()}presence(e,t){return this.pendingPresence[e]?(this.logger.warn("presence is already pending, skipping",e),Promise.reject(Error("presence request is already pending"))):new Promise((s,i)=>{this.pendingPresence[e]={resolve:s,reject:i},this.cable.send({command:"presence",identifier:e,data:t})})}}class le extends E{constructor(){super("No connection","closed"),this.name="NoConnectionError"}}class B extends G{constructor(e,t){super(t),this.channelId=e}set channelId(e){this._channelId=e}get channelId(){return this._channelId}}F(B,"identifier","__ghost__");const ue="$pubsub";class R extends G{async perform(e,t){if(e.startsWith("$"))return super.perform(e,t);throw Error("not implemented")}}F(R,"identifier",ue);const k=Symbol("state");class de{constructor({transport:e,protocol:t,encoder:s,logger:i,lazy:r,hubOptions:o,performFailures:c,transportConfigurator:d}){this.emitter=f(),this.transport=e,this.encoder=s,this.logger=i||new L,this.protocol=t,this.performFailures=c||"throw",this.protocol.attached(this),this.hub=new oe(o||{}),this[k]="idle",this.handleClose=this.handleClose.bind(this),this.handleIncoming=this.handleIncoming.bind(this),this.transportConfigurator=d,this.transport.on("close",this.handleClose),this.transport.on("data",this.handleIncoming),this.initialConnect=!0,this.recovering=!1,r===!1&&this.connect().catch(()=>{})}get state(){return this[k]}async connect(){if(this.state==="connected")return Promise.resolve();if(this.state==="connecting")return this.pendingConnect();let e=this.state==="idle";this[k]="connecting";let t=this.pendingConnect();this.logger.debug("connecting");try{this.transportConfigurator&&await this.transportConfigurator(this.transport,{initial:e}),await this.transport.open()}catch(s){this.handleClose(s)}return t}setSessionId(e){this.sessionId=e,this.transport.setParam("sid",e)}connected(){if(this.state==="connected")return;this.logger.info("connected"),this[k]="connected",this.recovering&&this.hub.subscriptions.all().forEach(t=>t.notify("disconnected",new _("recovery_failed"))),this.hub.subscriptions.all().forEach(t=>this._resubscribe(t));let e=!1;this.recovering=!1,this.initialConnect?(this.initialConnect=!1,this.emit("connect",{reconnect:!1,restored:e})):this.emit("connect",{reconnect:!0,restored:e})}restored(e){this.logger.info("connection recovered",{remoteIds:e}),this[k]="connected",this.hub.subscriptions.all().forEach(i=>{e&&i.remoteId&&e.includes(i.remoteId)?i.notify("restored"):(i.notify("disconnected",new _("recovery_failed")),this._resubscribe(i))});let t=!this.initialConnect,s=!0;this.recovering=!1,this.initialConnect=!1,this.emit("connect",{reconnect:t,restored:s})}notify(e,t,s){t&&typeof t!="string"&&(s=t,t=void 0),t?this.hub.notify(t,"info",{type:e,data:s}):this.emit("info",{type:e,data:s})}handleClose(e){this.logger.debug("transport closed",{error:e}),this.disconnected(new _(e,"transport_closed"))}disconnected(e){(this.state==="connected"||this.state==="connecting")&&(this.logger.info("disconnected",{reason:e}),this[k]="disconnected",this.recovering=this.protocol.recoverableClosure(e),this.recovering?this.hub.subscriptions.all().forEach(t=>t.notify("connecting")):this.hub.subscriptions.all().forEach(t=>{t.notify("disconnected",e)}),this.protocol.reset(e),this.hub.close(),this.transport.close(),this.emit("disconnect",e))}closed(e){if(this.state==="closed"||this.state==="idle")return;let t;e&&(t=e instanceof _?e:new _(e,void 0)),this.logger.info("closed",{reason:e||"user"}),this[k]="closed";let s=t||new _("cable_closed");this.hub.subscriptions.all().forEach(i=>i.notify("disconnected",s)),this.hub.close(),this.protocol.reset(),this.transport.close(),this.initialConnect=!0,this.emit("close",t)}disconnect(){this.closed()}handleIncoming(e){if(this.state==="closed"||this.state==="idle")return;let t=this.encoder.decode(e);if(t===void 0){this.logger.error("failed to decode message",{message:e});return}this.logger.debug("incoming data",t);let s=this.protocol.receive(t);if(s){this.logger.debug("processed incoming message",s);let{type:i,identifier:r,message:o,meta:c}=s;i?this.hub.notify(r,i,o):this.hub.transmit(r,o,c)}}send(e){if(this.state==="closed")throw Error("Cable is closed");let t=this.encoder.encode(e);if(t===void 0){this.logger.error("failed to encode message",{message:e});return}this.logger.debug("outgoing message",e),this.transport.send(t)}keepalive(e){this.emit("keepalive",e)}streamFrom(e){let t=new R({stream_name:e});return this.subscribe(t)}streamFromSigned(e){let t=new R({signed_stream_name:e});return this.subscribe(t)}subscribeTo(e,t){let s,i;return typeof e=="string"&&(i=e,e=B),s=i?new e(i,t):new e(t),this.subscribe(s)}subscribe(e){if(!e.attached(this))return e;let t=e.identifier;e.connecting();let s=this.hub.subscriptions.get(t)||this.hub.subscriptions.create(t,{subscribe:i=>this._subscribe(i,e.channelId,e.params),unsubscribe:i=>this._unsubscribe(i)});return s.add(e),s.intent==="subscribed"&&s.state==="connected"&&e.connected(),s.ensureSubscribed(),e}async _resubscribe(e){e.intent!=="subscribed"||!e.channels[0]||(e.notify("connecting"),e.ensureResubscribed())}async _subscribe(e,t,s){let i=e.id;if(this.state==="idle"&&this.connect().catch(()=>{}),this.state!=="connected"){this.logger.debug("cancel subscribe, no connection",{identifier:i});return}this.logger.debug("acquiring subscribe lock",{identifier:i});let r=await e.acquire("subscribed");if(r.canceled){this.logger.debug("subscribe lock has been canceled",{identifier:i}),r.release();return}if(this.logger.debug("subscribe lock has been acquired",{identifier:i}),e.intent!=="subscribed"){this.logger.debug("cancel subscribe request, already unsubscribed"),r.release();return}if(this.state!=="connected"){this.logger.debug("cancel subscribe, no connection",{identifier:i}),r.release();return}if(e.state==="connected"){this.logger.debug("already connected, skip subscribe command",{identifier:i}),e.notify("connected"),r.release();return}let o={identifier:t,params:s};this.logger.debug("subscribing",o);try{let c=await this.protocol.subscribe(t,s);this.hub.subscribe(i,c),this.logger.debug("subscribed",{...o,remoteId:c}),e.notify("connected")}catch(c){if(c){if(c instanceof Q&&this.logger.warn("rejected",o),c instanceof _){this.logger.debug("disconnected during subscription; will retry on connect",o),r.release();return}this.logger.error("failed to subscribe",{error:c,...o})}e.close(c),this.hub.unsubscribe(i)}r.release()}unsubscribe(e){let t=e.identifier,s=this.hub.subscriptions.get(t);if(!s)throw Error(`Subscription not found: ${t}`);s.remove(e),e.closed(),s.maybeUnsubscribe()}async _unsubscribe(e){let t=e.id;this.logger.debug("acquiring unsubscribe lock",{identifier:t});let s=await e.acquire("unsubscribed");if(s.canceled){this.logger.debug("unsubscribe lock has been canceled",{identifier:t}),s.release();return}if(this.logger.debug("unsubscribe lock has been acquired",{identifier:t}),e.intent!=="unsubscribed"){this.logger.debug("cancel unsubscribe, no longer needed",{identifier:t,intent:e.intent}),s.release();return}if(e.state==="disconnected"||e.state==="closed"){this.logger.debug(`already ${e.state} connected, skip unsubscribe command`,{identifier:t}),s.release();return}let i=e.remoteId;if(this.logger.debug("unsubscribing...",{remoteId:i}),this.state!=="connected"){this.logger.debug("unsubscribe skipped (cable is not connected)",{id:t}),e.close(),this.hub.unsubscribe(t),s.release();return}try{await this.protocol.unsubscribe(i),this.logger.debug("unsubscribed remotely",{id:t})}catch(r){r&&(r instanceof _?this.logger.debug("cable disconnected during the unsubscribe command execution",{id:t,error:r}):this.logger.error("unsubscribe failed",{id:t,error:r}))}e.intent==="unsubscribed"?(e.close(),this.hub.unsubscribe(t)):e.state="closed",s.release()}async perform(e,t,s){if(this.performFailures==="throw")return this._perform(e,t,s);try{return await this._perform(e,t,s)}catch(i){this.performFailures==="warn"&&this.logger.warn("perform failed",{error:i});return}}async _perform(e,t,s){if(this.state==="connecting"&&await this.pendingConnect(),this.state==="closed"||this.state==="disconnected")throw new le;let i=this.hub.subscriptions.get(e);if(!i)throw Error(`Subscription not found: ${e}`);if(await i.pending("subscribed"),i.intent!=="subscribed")throw Error(`Subscription is closed: ${e}`);let r=i.remoteId,o={id:r,action:t,payload:s};this.logger.debug("perform",o);try{let c=await this.protocol.perform(r,t,s);return c&&this.logger.debug("perform result",{message:c,request:o}),c}catch(c){throw this.logger.error("perform failed",{error:c,request:o}),c}}on(e,t){return this.emitter.on(e,t)}once(e,t){let s=this.emitter.on(e,(...i)=>{s(),t(...i)});return s}emit(e,...t){return this.emitter.emit(e,...t)}pendingConnect(){return this._pendingConnect?this._pendingConnect:(this._pendingConnect=new Promise((e,t)=>{let s=[()=>delete this._pendingConnect];s.push(this.on("connect",()=>{s.forEach(i=>i()),e()})),s.push(this.on("close",i=>{s.forEach(r=>r()),t(i)})),s.push(this.on("disconnect",i=>{s.forEach(r=>r()),t(i)}))}),this._pendingConnect)}}const pe={maxMissingPings:2,maxReconnectAttempts:1/0},x=()=>Date.now(),K=(n,e)=>{e=e||{};let{backoffRate:t,jitterRatio:s,maxInterval:i}=e;return t=t||2,s===void 0&&(s=.5),r=>{let o=n*t**r,c=o*t,d=o+(c-o)*Math.random(),p=2*(Math.random()-.5)*s;return d=d*(1+p),i&&i<d&&(d=i),d}};let X=class{constructor({pingInterval:e,...t}){if(this.pingInterval=e,!this.pingInterval)throw Error(`Incorrect pingInterval is provided: ${e}`);if(t=Object.assign({},pe,t),this.strategy=t.reconnectStrategy,!this.strategy)throw Error("Reconnect strategy must be provided");this.maxMissingPings=t.maxMissingPings,this.maxReconnectAttempts=t.maxReconnectAttempts,this.logger=t.logger||new L,this.state="pending_connect",this.attempts=0,this.disconnectedAt=x()}watch(e){this.target=e,this.initListeners()}reconnectNow(){return this.state==="connected"||this.state==="pending_connect"||this.state==="closed"?!1:(this.cancelReconnect(),this.state="pending_connect",this.target.connect().catch(e=>{this.logger.info("Failed at reconnecting: "+e)}),!0)}initListeners(){this.unbind=[],this.unbind.push(this.target.on("connect",()=>{this.attempts=0,this.pingedAt=x(),this.state="connected",this.cancelReconnect(),this.startPolling()})),this.unbind.push(this.target.on("disconnect",()=>{this.disconnectedAt=x(),this.state="disconnected",this.stopPolling(),this.scheduleReconnect()})),this.unbind.push(this.target.on("close",()=>{this.disconnectedAt=x(),this.state="closed",this.cancelReconnect(),this.stopPolling()})),this.unbind.push(this.target.on("keepalive",()=>{this.pingedAt=x()})),this.unbind.push(()=>{this.cancelReconnect(),this.stopPolling()})}dispose(){delete this.target,this.unbind&&this.unbind.forEach(e=>e()),delete this.unbind}startPolling(){this.pollId&&clearTimeout(this.pollId);let e=this.pingInterval+(Math.random()-.5)*this.pingInterval*.5;this.pollId=setTimeout(()=>{this.checkStale(),this.state==="connected"&&this.startPolling()},e)}stopPolling(){this.pollId&&clearTimeout(this.pollId)}checkStale(){let e=x()-this.pingedAt;e>this.maxMissingPings*this.pingInterval&&(this.logger.warn(`Stale connection: ${e}ms without pings`),this.state="pending_disconnect",this.target.disconnected(new te))}scheduleReconnect(){if(this.attempts>=this.maxReconnectAttempts){this.target.close();return}let e=this.strategy(this.attempts);this.attempts++,this.logger.info(`Reconnecting in ${e}ms (${this.attempts} attempt)`),this.state="pending_reconnect",this.reconnnectId=setTimeout(()=>this.reconnectNow(),e)}cancelReconnect(){this.reconnnectId&&(clearTimeout(this.reconnnectId),delete this.reconnnectId)}};class fe{constructor(e,t={}){this.transports=e,this.transport=null,this.emitter=f(),this.unbind=[],this.logger=t.logger||new L}displayName(){return"fallbacked transport"}async open(){for(let e=0;e<this.transports.length;e++){let t=this.transports[e];try{this.transport=t,this.resetListeners(),this.logger.debug(`Trying to connect via ${t.displayName()}`),await t.open(),this.logger.debug(`Connected via ${t.displayName()}`);return}catch(s){this.logger.debug(`Failed to connect via ${t.displayName()}: ${s.message}`)}}throw this.transport=null,this.resetListeners(),new Error("Couldn't connect via any available transport")}send(e){if(!this.transport)throw new Error("No transport is open");this.transport.send(e)}async close(){if(!this.transport)throw new Error("No transport is open");await this.transport.close(),this.transport=null}setURL(){throw new Error("Not implemented. Set URL for each transport separately")}setParam(e,t){this.transports.forEach(s=>{s.setParam(e,t)})}setToken(e,t){this.transports.forEach(s=>{s.setToken(e,t)})}on(e,t){return this.emitter.on(e,t)}once(e,t){let s=this.emitter.on(e,(...i)=>{s(),t(...i)});return s}get url(){return this.transport?this.transport.url:""}resetListeners(){this.unbind.forEach(e=>e()),this.unbind.length=0,this.transport&&this.unbind.push(this.transport.on("open",()=>{this.emitter.emit("open")}),this.transport.on("data",e=>{this.emitter.emit("data",e)}),this.transport.on("close",e=>{this.emitter.emit("close",e)}),this.transport.on("error",e=>{this.emitter.emit("error",e)}))}}class be{constructor(e,t={}){this.url=e;let s=t.websocketImplementation;if(s)this.Impl=s;else if(typeof WebSocket<"u")this.Impl=WebSocket;else throw new Error("No WebSocket support");this.connected=!1,this.emitter=f();let{format:i,subprotocol:r,authStrategy:o}=t;this.format=i||"text",this.connectionOptions=t.websocketOptions,this.authStrategy=o||"param",this.authProtocol="",this.subprotocol=r}displayName(){return"WebSocket("+this.url+")"}open(){let e=this.subprotocol;return this.authStrategy==="sub-protocol"&&(e=[this.subprotocol,this.authProtocol]),this.connectionOptions?this.ws=new this.Impl(this.url,e,this.connectionOptions):this.ws=new this.Impl(this.url,e),this.ws.binaryType="arraybuffer",this.initListeners(),new Promise((t,s)=>{let i=[];i.push(this.once("open",()=>{i.forEach(r=>r()),t()})),i.push(this.once("close",()=>{i.forEach(r=>r()),s(Error("WS connection closed"))}))})}setURL(e){this.url=e}setParam(e,t){let s=new URL(this.url);s.searchParams.set(e,t);let i=`${s.protocol}//${s.host}${s.pathname}?${s.searchParams}`;this.setURL(i)}setToken(e,t="jid"){if(this.authStrategy==="param")this.setParam(t,e);else if(this.authStrategy==="header"){this.connectionOptions=this.connectionOptions||{},this.connectionOptions.headers=this.connectionOptions.headers||{};let s=`x-${t}`.toLowerCase();s=Object.keys(this.connectionOptions.headers).find(r=>r.toLowerCase()===s)||s,this.connectionOptions.headers[s]=e}else if(this.authStrategy==="sub-protocol")this.authProtocol=`anycable-token.${e}`;else throw new Error("Unknown auth strategy: "+this.authStrategy)}send(e){if(!this.ws||!this.connected)throw Error("WebSocket is not connected");this.ws.send(e)}close(){this.ws?this.onclose():this.connected=!1}on(e,t){return this.emitter.on(e,t)}once(e,t){let s=this.emitter.on(e,(...i)=>{s(),t(...i)});return s}initListeners(){this.ws.onerror=e=>{this.connected&&this.emitter.emit("error",e.error||new Error("WS Error"))},this.ws.onclose=()=>{this.onclose()},this.ws.onmessage=e=>{let t=e.data;this.format==="binary"&&(t=new Uint8Array(t)),this.emitter.emit("data",t)},this.ws.onopen=()=>{this.connected=!0,this.emitter.emit("open")}}onclose(){this.ws.onclose=void 0,this.ws.onmessage=void 0,this.ws.onopen=void 0,this.ws.close(),delete this.ws,this.connected=!1,this.emitter.emit("close")}}const Y={protocol:"actioncable-v1-json",pingInterval:3e3,maxReconnectAttempts:1/0,maxMissingPings:2,logLevel:"warn",lazy:!0};function ge(n,e){if(typeof n=="object"&&typeof e>"u"&&(e=n,n=void 0),e=e||{},!n&&!e.transport)throw Error("URL or transport must be specified");e=Object.assign({},Y,e);let{protocol:t,websocketImplementation:s,websocketFormat:i,websocketOptions:r,websocketAuthStrategy:o,fallbacks:c,logLevel:d,logger:p,transport:w,encoder:q,lazy:P,monitor:I,pingInterval:$,reconnectStrategy:j,maxMissingPings:A,maxReconnectAttempts:M,subprotocol:z,tokenRefresher:a,historyTimestamp:h,protocolOptions:l,concurrentSubscribes:v,performFailures:m,transportConfigurator:y,auth:b}=e;if(p=p||new L(d),typeof t=="string"){z=z||t;let T=t.substring(0,t.lastIndexOf("-")),U=t.substring(t.lastIndexOf("-")+1);if(l=l||{},T==="actioncable-v1")t=new V({logger:p,...l});else if(T==="actioncable-v1-ext")t=new he({logger:p,historyTimestamp:h,...l});else throw Error(`Protocol is not supported yet: ${t}`);if(U==="json")q=q||new ce,i=i||"text";else if(U==="msgpack"){if(i="binary",!q)throw Error("Msgpack encoder must be specified explicitly. Use `@anycable/msgpack-encoder` package or build your own")}else if(U==="protobuf"){if(i=i||"binary",!q)throw Error("Protobuf encoder must be specified explicitly. Use `@anycable/protobuf-encoder` package or build your own")}else throw Error(`Protocol is not supported yet: ${t}`)}if(!t)throw Error("Protocol must be specified");w=w||new be(n,{websocketImplementation:s,websocketOptions:r,subprotocol:z,authStrategy:o,format:i}),c&&(w=new fe([w,...c],{logger:p})),b&&b.token&&w.setToken(b.token,b.param||"jid"),j=j||K($),I!==!1&&(I=I||new X({pingInterval:$,reconnectStrategy:j,maxMissingPings:A,maxReconnectAttempts:M,logger:p}));let u={concurrentSubscribes:v},g=new de({protocol:t,transport:w,encoder:q,logger:p,lazy:P,hubOptions:u,performFailures:m,transportConfigurator:y});return I&&(I.watch(g),g.monitor=I),a&&me(g,async()=>{try{await a(w)}catch(T){return p.error("Failed to refresh authentication token: "+T),!1}return g.connect().catch(()=>{}),!0}),g}function me(n,e){let t=!1;n.on("connect",()=>t=!1),n.on("close",async s=>{if(s){if(t){n.logger.warn("Token auto-refresh is disabled",s);return}s.reason==="token_expired"&&(t=!0,await e())}})}class ye extends J{writeLogEntry(e,t,s){s?console[e](t,s):console[e](t)}}class we extends X{watch(e){super.watch(e),this.initActivityListeners()}initActivityListeners(){if(typeof document<"u"&&typeof window<"u"&&document.addEventListener&&window.addEventListener){let e=()=>{document.hidden||this.reconnectNow()&&this.logger.debug("Trigger reconnect due to visibility change")},t=i=>{this.reconnectNow()&&this.logger.debug("Trigger reconnect",{event:i})},s=()=>this.disconnect(new _("page_frozen"));document.addEventListener("visibilitychange",e,!1),window.addEventListener("focus",t,!1),window.addEventListener("online",t,!1),window.addEventListener("resume",t,!1),window.addEventListener("freeze",s,!1),this.unbind.push(()=>{document.removeEventListener("visibilitychange",e,!1),window.removeEventListener("focus",t,!1),window.removeEventListener("online",t,!1),window.removeEventListener("resume",t,!1),window.removeEventListener("freeze",s,!1)})}}disconnect(e){this.state==="disconnected"||this.state==="closed"||(this.logger.info("Disconnecting",{reason:e.message}),this.cancelReconnect(),this.stopPolling(),this.state="pending_disconnect",this.target.disconnected(e))}}const Se=["cable","action-cable"],ve="/cable",O=(n,e)=>{for(let t of Se){let s=n.head.querySelector(`meta[name='${t}-${e}']`);if(s)return s.getAttribute("content")}},Z=n=>n.match(/wss?:\/\//)?n:typeof window<"u"?`${window.location.protocol.replace("http","ws")}//${window.location.host}${n}`:n,_e=()=>{if(typeof document<"u"&&document.head){let n=O(document,"url");if(n)return Z(n)}return Z(ve)},Ie=()=>{if(typeof document<"u"&&document.head){let n=O(document,"history-timestamp");if(n)return n|0}},Ee=()=>{if(typeof document<"u"&&document.head)return O(document,"token")},qe=()=>{if(typeof document<"u"&&document.head)return O(document,"token-param")};function Pe(n,e){typeof n=="object"&&typeof e>"u"&&(e=n,n=void 0),n=n||_e(),e=e||{},e.historyTimestamp||(e.historyTimestamp=Ie());let t=Ee();if(t){let p=qe();e.auth=Object.assign({token:t,param:p},e.auth||{})}e=Object.assign({},Y,e);let{logLevel:s,logger:i,pingInterval:r,reconnectStrategy:o,maxMissingPings:c,maxReconnectAttempts:d}=e;return i=e.logger=e.logger||new ye(s),o=e.reconnectStrategy=e.reconnectStrategy||K(r),e.monitor!==!1&&(e.monitor=e.monitor||new we({pingInterval:r,reconnectStrategy:o,maxMissingPings:c,maxReconnectAttempts:d,logger:i})),ge(n,e)}const Ce={answer:"/.netlify/functions/quiz-answer",sync:"/.netlify/functions/quiz-sync"};function ke(n){if(typeof n!="object"||n===null)return!1;const e=n;return typeof e.sessionId=="string"&&(e.activeQuizId===null||typeof e.activeQuizId=="string")&&typeof e.results=="object"&&e.results!==null}function Te(n){if(typeof n!="object"||n===null)return!1;const e=n;return typeof e.quizId=="string"&&typeof e.answer=="string"&&typeof e.sessionId=="string"}class xe{constructor(e){this.resultsChannel=null,this.activeQuizId=null,this.results={},this.voters={},this.online=0,this.submitted={},this.listeners=[],this.syncTimer=null,this.syncPending=!1,this.incomingSyncTimer=null,this.incomingSyncData=null,this.role=e.role,this.quizGroupId=e.quizGroupId,this.sessionId=e.sessionId||this.getOrCreateSessionId(),this.endpoints={...Ce,...e.endpoints};const t=e.role==="participant"?6e4:3e5;this.cable=Pe(e.wsUrl,{protocol:"actioncable-v1-ext-json",protocolOptions:{historyTimestamp:Math.floor((Date.now()-t)/1e3)}}),this.syncChannel=this.cable.streamFrom(`quiz:${e.quizGroupId}:sync`),this.syncChannel.on("message",this.onSyncMessage.bind(this));const s=this.syncChannel.presence;if(typeof s.stateFromInfo=="function"){const i=s.stateFromInfo.bind(s);s.stateFromInfo=r=>{if(!(r!=null&&r.records)){if(r&&typeof r=="object"){const{type:o,...c}=r;return c}return{}}return i(r)}}this.syncChannel.on("presence",this.onPresence.bind(this)),e.role==="participant"&&this.syncChannel.presence.join(this.sessionId,{id:this.sessionId}),this.syncChannel.presence.info().then(i=>{i&&(this.online=Object.keys(i).length,this.notifyStateChange())}).catch(()=>{}),e.role==="presenter"&&(this.resultsChannel=this.cable.streamFrom(`quiz:${e.quizGroupId}:results`),this.resultsChannel.on("message",this.onResultsMessage.bind(this)),this.restoreState(),this.activeQuizId&&this.sendSync()),e.role==="participant"&&this.restoreSubmitted()}subscribe(e){return this.listeners.push(e),e(this.getState()),()=>{const t=this.listeners.indexOf(e);t!==-1&&this.listeners.splice(t,1)}}getState(){return{activeQuizId:this.activeQuizId,results:structuredClone(this.results),online:this.online,submitted:{...this.submitted}}}getQuizState(e){return this.results[e]||{votes:{},total:0}}hasVoted(e){return e in this.submitted}getVotedAnswer(e){return this.submitted[e]??null}setActiveQuiz(e){this.role==="presenter"&&this.activeQuizId!==e&&(this.activeQuizId=e,this.saveState(),this.sendSync())}async submitAnswer(e,t){if(this.hasVoted(e))return!1;try{const s=await fetch(this.endpoints.answer,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({quizId:e,answer:t,sessionId:this.sessionId,quizGroupId:this.quizGroupId})});return s.ok&&(this.submitted[e]=t,this.saveSubmitted(),this.notifyStateChange()),s.ok}catch{return!1}}disconnect(){this.role==="participant"&&this.syncChannel.presence.leave(),this.cable.disconnect()}onSyncMessage(e){const t=this.parse(e);ke(t)&&t.sessionId!==this.sessionId&&this.role==="participant"&&this.applySyncThrottled(t)}applySyncThrottled(e){if(this.incomingSyncTimer){this.incomingSyncData=e;return}this.applySync(e),this.incomingSyncData=null,this.incomingSyncTimer=setTimeout(()=>{this.incomingSyncTimer=null,this.incomingSyncData&&(this.applySync(this.incomingSyncData),this.incomingSyncData=null)},200)}applySync(e){var t;this.activeQuizId=e.activeQuizId,this.results=e.results;for(const s of Object.keys(this.submitted))(((t=e.results[s])==null?void 0:t.total)??0)===0&&this.clearVotedAnswer(s);this.notifyStateChange()}onResultsMessage(e){if(this.role!=="presenter")return;const t=this.parse(e);if(!Te(t)||t.sessionId===this.sessionId)return;const{quizId:s,answer:i,sessionId:r}=t;this.voters[s]||(this.voters[s]=new Set),!this.voters[s].has(r)&&(this.voters[s].add(r),this.results[s]||(this.results[s]={votes:{},total:0}),this.results[s].votes[i]=(this.results[s].votes[i]||0)+1,this.results[s].total+=1,this.saveState(),this.notifyStateChange(),this.sendSyncThrottled())}async onPresence(){try{const e=await this.syncChannel.presence.info();e&&(this.online=Object.keys(e).length,this.notifyStateChange(),this.role==="presenter"&&this.activeQuizId&&this.sendSyncThrottled())}catch{}}sendSyncThrottled(){if(this.syncTimer){this.syncPending=!0;return}this.sendSync(),this.syncTimer=setTimeout(()=>{this.syncTimer=null,this.syncPending&&(this.syncPending=!1,this.sendSync())},200)}async sendSync(){if(this.role==="presenter")try{await fetch(this.endpoints.sync,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({activeQuizId:this.activeQuizId,sessionId:this.sessionId,quizGroupId:this.quizGroupId,results:this.results})})}catch{}}getOrCreateSessionId(){const e=`quiz-session-${this.quizGroupId}`;let t=sessionStorage.getItem(e);return t||(t=crypto.randomUUID(),sessionStorage.setItem(e,t)),t}saveState(){if(this.role==="presenter")try{sessionStorage.setItem(`quiz-presenter-${this.quizGroupId}`,JSON.stringify({activeQuizId:this.activeQuizId,results:this.results,voters:Object.fromEntries(Object.entries(this.voters).map(([e,t])=>[e,[...t]]))}))}catch(e){console.warn("[QuizManager] saveState failed:",e)}}restoreState(){try{const e=sessionStorage.getItem(`quiz-presenter-${this.quizGroupId}`);if(!e)return;const t=JSON.parse(e);if(t.activeQuizId&&(this.activeQuizId=t.activeQuizId),t.results&&(this.results=t.results),t.voters)for(const[s,i]of Object.entries(t.voters))this.voters[s]=new Set(i)}catch{}}saveSubmitted(){try{sessionStorage.setItem(`quiz-submitted-${this.quizGroupId}`,JSON.stringify(this.submitted))}catch{}}restoreSubmitted(){try{const e=sessionStorage.getItem(`quiz-submitted-${this.quizGroupId}`);if(!e)return;this.submitted=JSON.parse(e)}catch{}}clearVotedAnswer(e){delete this.submitted[e],this.saveSubmitted()}notifyStateChange(){const e=this.getState();for(const t of this.listeners)t(e)}parse(e){if(typeof e=="string")try{return JSON.parse(e)}catch{return{}}return e&&typeof e=="object"?e:{}}}const N=new Map;function ze(n){return N.has(n.quizGroupId)||N.set(n.quizGroupId,new xe({...n,role:"participant"})),N.get(n.quizGroupId)}function Le(n,e){const t=document.querySelector(n);if(!t)throw new Error(`[live-quiz] Element not found: ${n}`);const{questions:s,brandText:i,footerText:r="Powered by AnyCable"}=e;if(t.innerHTML="",t.classList.add("lq-participant"),i){const a=document.createElement("p");a.className="lq-participant__brand",a.textContent=i,t.appendChild(a)}const o=document.createElement("div");o.className="lq-participant__stats";const c=document.createElement("span");c.className="lq-participant__online",c.textContent="0";const d=document.createElement("span");d.className="lq-participant__answered",d.textContent="0",o.append(c," online · ",d," answered"),t.appendChild(o);const p=document.createElement("div");p.className="lq-participant__waiting",p.textContent="Waiting for the next question...",t.appendChild(p);const w={};let q=0;for(const a of s){const h=document.createElement("div");h.className="lq-participant__section lq-participant__section--hidden",h.dataset.quizId=a.quizId;const l=document.createElement("p");l.className="lq-participant__number",l.textContent=`Question ${q+1} of ${s.length}`,h.appendChild(l);const v=document.createElement("h2");v.className="lq-participant__question",v.textContent=a.question,h.appendChild(v);const m=document.createElement("div");m.className="lq-participant__options";for(const b of a.options){const u=document.createElement("button");u.type="button",u.className="lq-participant__btn",u.dataset.answer=b.label;const g=document.createElement("span");g.className="lq-participant__btn-label",g.textContent=b.label;const T=document.createElement("span");T.textContent=b.text,u.append(g,T),m.appendChild(u)}h.appendChild(m);const y=document.createElement("p");y.className="lq-participant__status",y.setAttribute("role","status"),y.setAttribute("aria-live","polite"),h.appendChild(y),t.appendChild(h),w[a.quizId]=h,q++}if(r){const a=document.createElement("p");a.className="lq-participant__footer",a.textContent=r,t.appendChild(a)}const P=ze({wsUrl:e.wsUrl,quizGroupId:e.quizGroupId,endpoints:e.endpoints});let I=null;function $(a){I=a;for(const[h,l]of Object.entries(w))h===a?l.classList.remove("lq-participant__section--hidden"):l.classList.add("lq-participant__section--hidden");a?p.style.display="none":p.style.display=""}function j(a,h){var u;const l=w[a];if(!l)return;const v=l.querySelectorAll(".lq-participant__btn"),m=l.querySelector(".lq-participant__status");for(const g of v)g.disabled=!0,g.setAttribute("aria-disabled","true"),g.dataset.answer===h?g.classList.add("lq-participant__btn--selected"):g.classList.add("lq-participant__btn--faded");const y=((u=l.querySelector(`[data-answer="${h}"] span:last-child`))==null?void 0:u.textContent)||h;m.textContent="";const b=document.createElement("strong");b.textContent=y,m.append(b," — submitted!")}function A(a){const h=w[a];if(!h)return;const l=h.querySelectorAll(".lq-participant__btn"),v=h.querySelector(".lq-participant__status");for(const m of l)m.disabled=!1,m.removeAttribute("aria-disabled"),m.classList.remove("lq-participant__btn--selected","lq-participant__btn--faded");v.textContent=""}const M=P.subscribe(a=>{a.activeQuizId!==void 0&&$(a.activeQuizId),c.textContent=String(a.online),I&&a.results[I]&&(d.textContent=String(a.results[I].total));for(const h of s){const l=a.submitted[h.quizId];l?j(h.quizId,l):A(h.quizId)}});for(const a of s){const h=w[a.quizId];if(!h)continue;const l=h.querySelectorAll(".lq-participant__btn"),v=h.querySelector(".lq-participant__status");async function m(y){for(const u of l)u.disabled=!0,u.setAttribute("aria-disabled","true"),u.dataset.answer===y?u.classList.add("lq-participant__btn--selected"):u.classList.add("lq-participant__btn--faded");if(v.textContent="Sending...",!await P.submitAnswer(a.quizId,y)&&!P.hasVoted(a.quizId)){v.textContent="Something went wrong. Try again!";for(const u of l)u.disabled=!1,u.removeAttribute("aria-disabled"),u.classList.remove("lq-participant__btn--selected","lq-participant__btn--faded")}}for(const y of l)y.addEventListener("click",()=>{const b=y.dataset.answer;b&&!P.hasVoted(a.quizId)&&m(b)})}function z(){P.disconnect()}return window.addEventListener("pagehide",z),{destroy(){M(),window.removeEventListener("pagehide",z),P.disconnect(),t.innerHTML="",t.classList.remove("lq-participant")}}}S.createParticipantUI=Le,Object.defineProperty(S,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,3 @@
1
+ /** Animate a numeric counter from its current text to a target value. */
2
+ export declare function animateCount(el: HTMLElement, target: number): void;
3
+ //# sourceMappingURL=animate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"animate.d.ts","sourceRoot":"","sources":["../../../src/dom/animate.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,wBAAgB,YAAY,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CA2BlE"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Generate a QR code image element at runtime.
3
+ * Returns an <img> with the QR as a data URL.
4
+ */
5
+ export declare function renderQR(url: string, size?: number): Promise<HTMLImageElement>;
6
+ //# sourceMappingURL=render-qr.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-qr.d.ts","sourceRoot":"","sources":["../../../src/dom/render-qr.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAsB,QAAQ,CAC5B,GAAG,EAAE,MAAM,EACX,IAAI,SAAM,GACT,OAAO,CAAC,gBAAgB,CAAC,CAc3B"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Inject question UI into a `<section data-quiz-id>` slide.
3
+ * Reads data attributes, builds the full quiz DOM.
4
+ */
5
+ export declare function renderQuestion(slide: HTMLElement, quizUrl: string | undefined, titleText?: string): Promise<void>;
6
+ //# sourceMappingURL=render-question.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-question.d.ts","sourceRoot":"","sources":["../../../src/dom/render-question.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,SAAS,SAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CA6Ff"}
@@ -0,0 +1,15 @@
1
+ import type { VoteState } from "../quiz-types";
2
+ /**
3
+ * Inject results bar chart into a `<section data-quiz-results>` slide.
4
+ * Reads data-quiz-results (quizId) and data-quiz-options for options metadata.
5
+ */
6
+ export declare function renderResults(slide: HTMLElement): void;
7
+ /**
8
+ * Update result bars with current vote state.
9
+ */
10
+ export declare function updateResultBars(wrapper: HTMLElement, state: VoteState): void;
11
+ /**
12
+ * Animate result bars entrance when slide becomes visible.
13
+ */
14
+ export declare function animateResultBars(wrapper: HTMLElement, state: VoteState): void;
15
+ //# sourceMappingURL=render-results.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-results.d.ts","sourceRoot":"","sources":["../../../src/dom/render-results.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,eAAe,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAuEtD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,SAAS,GACf,IAAI,CAiBN;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,SAAS,GACf,IAAI,CAiCN"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * live-quiz — Live audience quiz plugin for Reveal.js
3
+ *
4
+ * Usage:
5
+ * import Reveal from 'reveal.js';
6
+ * import RevealLiveQuiz from 'live-quiz';
7
+ * import 'live-quiz/style.css';
8
+ *
9
+ * Reveal.initialize({
10
+ * plugins: [RevealLiveQuiz],
11
+ * liveQuiz: {
12
+ * wsUrl: 'wss://your-cable.fly.dev/cable',
13
+ * quizGroupId: 'my-talk',
14
+ * quizUrl: 'https://my-talk.example.com/quiz',
15
+ * }
16
+ * });
17
+ */
18
+ import "./live-quiz.css";
19
+ import { createPlugin } from "./plugin";
20
+ export type { LiveQuizConfig } from "./plugin";
21
+ export type { QuizEndpoints, QuizState, VoteState, StateCallback, } from "./quiz-manager";
22
+ export type { QuizOption } from "./quiz-types";
23
+ export { getQuizPresenter, getQuizParticipant, removeQuizPresenter, removeQuizParticipant, QuizManager, } from "./quiz-manager";
24
+ export { animateCount } from "./dom/animate";
25
+ export default createPlugin;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EACV,aAAa,EACb,SAAS,EACT,SAAS,EACT,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,WAAW,GACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE7C,eAAe,YAAY,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Reveal.js Live Quiz plugin core.
3
+ *
4
+ * Scans slides for data-quiz-id / data-quiz-results attributes,
5
+ * injects DOM, listens to slidechanged, and bridges state from QuizManager.
6
+ */
7
+ import type { QuizEndpoints } from "./quiz-manager";
8
+ export interface LiveQuizConfig {
9
+ /** AnyCable WebSocket URL */
10
+ wsUrl: string;
11
+ /** Unique ID grouping quizzes in this talk */
12
+ quizGroupId: string;
13
+ /** URL for the audience quiz page (shown as QR code) */
14
+ quizUrl?: string;
15
+ /** Custom endpoint URLs for answer/sync functions */
16
+ endpoints?: Partial<QuizEndpoints>;
17
+ /** Title shown on quiz slides (default: "Pop quiz!") */
18
+ titleText?: string;
19
+ }
20
+ interface RevealApi {
21
+ getConfig(): Record<string, unknown>;
22
+ getRevealElement(): HTMLElement;
23
+ on(event: string, cb: (...args: unknown[]) => void): void;
24
+ off(event: string, cb: (...args: unknown[]) => void): void;
25
+ sync(): void;
26
+ }
27
+ export declare function createPlugin(): {
28
+ id: string;
29
+ init: (reveal: RevealApi) => Promise<void>;
30
+ destroy: () => void;
31
+ };
32
+ export {};
33
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAOpD,MAAM,WAAW,cAAc;IAC7B,6BAA6B;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,wDAAwD;IACxD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IACnC,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAGD,UAAU,SAAS;IACjB,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,gBAAgB,IAAI,WAAW,CAAC;IAChC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC1D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAC3D,IAAI,IAAI,IAAI,CAAC;CACd;AAED,wBAAgB,YAAY;;mBAmEH,SAAS,KAAG,OAAO,CAAC,IAAI,CAAC;;EA6EjD"}
@@ -0,0 +1,74 @@
1
+ export type { VoteState, SyncPayload, AnswerPayload, QuizState, StateCallback, } from "./quiz-types";
2
+ import type { VoteState, SyncPayload, AnswerPayload, QuizState, StateCallback } from "./quiz-types";
3
+ export interface QuizEndpoints {
4
+ answer: string;
5
+ sync: string;
6
+ }
7
+ export declare function isValidSyncPayload(data: unknown): data is SyncPayload;
8
+ export declare function isValidAnswerPayload(data: unknown): data is AnswerPayload;
9
+ export declare class QuizManager {
10
+ private cable;
11
+ private syncChannel;
12
+ private resultsChannel;
13
+ private role;
14
+ private quizGroupId;
15
+ private sessionId;
16
+ private endpoints;
17
+ private activeQuizId;
18
+ private results;
19
+ private voters;
20
+ private online;
21
+ private submitted;
22
+ private listeners;
23
+ private syncTimer;
24
+ private syncPending;
25
+ private incomingSyncTimer;
26
+ private incomingSyncData;
27
+ constructor(config: {
28
+ wsUrl: string;
29
+ quizGroupId: string;
30
+ role: "presenter" | "participant";
31
+ sessionId?: string;
32
+ endpoints?: Partial<QuizEndpoints>;
33
+ });
34
+ subscribe(cb: StateCallback): () => void;
35
+ getState(): QuizState;
36
+ getQuizState(quizId: string): VoteState;
37
+ hasVoted(quizId: string): boolean;
38
+ getVotedAnswer(quizId: string): string | null;
39
+ /** Presenter: set the active quiz (called when slide enters viewport) */
40
+ setActiveQuiz(quizId: string): void;
41
+ /** Participant: submit an answer */
42
+ submitAnswer(quizId: string, answer: string): Promise<boolean>;
43
+ disconnect(): void;
44
+ private onSyncMessage;
45
+ private applySyncThrottled;
46
+ private applySync;
47
+ private onResultsMessage;
48
+ private onPresence;
49
+ private sendSyncThrottled;
50
+ private sendSync;
51
+ private getOrCreateSessionId;
52
+ private saveState;
53
+ private restoreState;
54
+ private saveSubmitted;
55
+ private restoreSubmitted;
56
+ private clearVotedAnswer;
57
+ private notifyStateChange;
58
+ private parse;
59
+ }
60
+ export declare function getQuizPresenter(config: {
61
+ wsUrl: string;
62
+ quizGroupId: string;
63
+ endpoints?: Partial<QuizEndpoints>;
64
+ }): QuizManager;
65
+ /** Remove a presenter instance from the singleton cache. */
66
+ export declare function removeQuizPresenter(quizGroupId: string): void;
67
+ export declare function getQuizParticipant(config: {
68
+ wsUrl: string;
69
+ quizGroupId: string;
70
+ endpoints?: Partial<QuizEndpoints>;
71
+ }): QuizManager;
72
+ /** Remove a participant instance from the singleton cache. */
73
+ export declare function removeQuizParticipant(quizGroupId: string): void;
74
+ //# sourceMappingURL=quiz-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quiz-manager.d.ts","sourceRoot":"","sources":["../../src/quiz-manager.ts"],"names":[],"mappings":"AAgBA,YAAY,EACV,SAAS,EACT,WAAW,EACX,aAAa,EACb,SAAS,EACT,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EACV,SAAS,EACT,WAAW,EACX,aAAa,EACb,SAAS,EACT,aAAa,EACd,MAAM,cAAc,CAAC;AAItB,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AASD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,WAAW,CASrE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,aAAa,CAQzE;AAID,qBAAa,WAAW;IACtB,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,cAAc,CAAwB;IAC9C,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,SAAS,CAAgB;IAGjC,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,MAAM,CAAK;IACnB,OAAO,CAAC,SAAS,CAA8B;IAG/C,OAAO,CAAC,SAAS,CAAuB;IAGxC,OAAO,CAAC,SAAS,CAA8C;IAC/D,OAAO,CAAC,WAAW,CAAS;IAG5B,OAAO,CAAC,iBAAiB,CAA8C;IACvE,OAAO,CAAC,gBAAgB,CAA4B;gBAExC,MAAM,EAAE;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,IAAI,EAAE,WAAW,GAAG,aAAa,CAAC;QAClC,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;KACpC;IAkFD,SAAS,CAAC,EAAE,EAAE,aAAa,GAAG,MAAM,IAAI;IASxC,QAAQ,IAAI,SAAS;IASrB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS;IAIvC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIjC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI7C,yEAAyE;IACzE,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAQnC,oCAAoC;IAC9B,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAyBpE,UAAU,IAAI,IAAI;IASlB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,SAAS;IAcjB,OAAO,CAAC,gBAAgB;YA2BV,UAAU;IAkBxB,OAAO,CAAC,iBAAiB;YAeX,QAAQ;IAoBtB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,SAAS;IAkBjB,OAAO,CAAC,YAAY;IAmBpB,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,gBAAgB;IAYxB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,KAAK;CAWd;AAMD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CACpC,GAAG,WAAW,CAQd;AAED,4DAA4D;AAC5D,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAE7D;AAID,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;CACpC,GAAG,WAAW,CAQd;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAE/D"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared types for the quiz engine.
3
+ *
4
+ * Used by QuizManager (client) and serverless functions (server).
5
+ */
6
+ export interface VoteState {
7
+ votes: Record<string, number>;
8
+ total: number;
9
+ }
10
+ export interface SyncPayload {
11
+ activeQuizId: string | null;
12
+ sessionId: string;
13
+ results: Record<string, VoteState>;
14
+ }
15
+ export interface AnswerPayload {
16
+ quizId: string;
17
+ answer: string;
18
+ sessionId: string;
19
+ }
20
+ export interface QuizState {
21
+ activeQuizId: string | null;
22
+ results: Record<string, VoteState>;
23
+ online: number;
24
+ submitted: Record<string, string>;
25
+ }
26
+ export type StateCallback = (state: QuizState) => void;
27
+ /** Option shape used in data-quiz-options JSON. */
28
+ export interface QuizOption {
29
+ label: string;
30
+ text: string;
31
+ correct?: boolean;
32
+ }
33
+ //# sourceMappingURL=quiz-types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quiz-types.d.ts","sourceRoot":"","sources":["../../src/quiz-types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;AAEvD,mDAAmD;AACnD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB"}
@@ -0,0 +1,33 @@
1
+ # Backend Functions
2
+
3
+ These serverless functions broadcast quiz events via AnyCable. Copy the folder for your platform into your project.
4
+
5
+ ## Environment Variables
6
+
7
+ | Variable | Description |
8
+ |---|---|
9
+ | `ANYCABLE_BROADCAST_URL` | AnyCable HTTP broadcast endpoint (from AnyCable Plus dashboard) |
10
+
11
+ **Never put this in your code.** Set it in your platform's dashboard.
12
+
13
+ ## Netlify
14
+
15
+ 1. Copy `functions/netlify/` contents to `netlify/functions/` in your project
16
+ 2. Set env vars in **Netlify dashboard > Site settings > Environment variables**
17
+ 3. Deploy — endpoints are `/.netlify/functions/quiz-answer` and `/.netlify/functions/quiz-sync` (the defaults)
18
+
19
+ ## Vercel
20
+
21
+ 1. Copy `functions/vercel/` contents to `api/` in your project
22
+ 2. Set env vars in **Vercel dashboard > Settings > Environment Variables**
23
+ 3. Deploy — endpoints are `/api/quiz-answer` and `/api/quiz-sync`
24
+ 4. Configure the plugin to use Vercel endpoints:
25
+
26
+ ```js
27
+ liveQuiz: {
28
+ endpoints: {
29
+ answer: '/api/quiz-answer',
30
+ sync: '/api/quiz-sync',
31
+ }
32
+ }
33
+ ```
@@ -0,0 +1,6 @@
1
+ {
2
+ "private": true,
3
+ "dependencies": {
4
+ "@anycable/serverless-js": "^0.2.2"
5
+ }
6
+ }
@@ -0,0 +1,43 @@
1
+ import type { Context } from "@netlify/functions";
2
+ import {
3
+ broadcastTo,
4
+ jsonResponse,
5
+ requirePost,
6
+ requireString,
7
+ } from "./shared.mts";
8
+
9
+ export default async (req: Request, _context: Context) => {
10
+ const methodError = requirePost(req);
11
+ if (methodError) return methodError;
12
+
13
+ let body: Record<string, unknown>;
14
+ try {
15
+ body = await req.json();
16
+ } catch {
17
+ return jsonResponse({ error: "Invalid JSON" }, 400);
18
+ }
19
+
20
+ const { quizId, answer, sessionId, quizGroupId } = body;
21
+
22
+ for (const [val, name] of [
23
+ [quizId, "quizId"],
24
+ [answer, "answer"],
25
+ [sessionId, "sessionId"],
26
+ [quizGroupId, "quizGroupId"],
27
+ ] as const) {
28
+ const err = requireString(val, name);
29
+ if (err) return err;
30
+ }
31
+
32
+ try {
33
+ await broadcastTo(`quiz:${quizGroupId}:results`, {
34
+ quizId,
35
+ answer,
36
+ sessionId,
37
+ });
38
+ } catch {
39
+ return jsonResponse({ error: "Broadcast failed" }, 502);
40
+ }
41
+
42
+ return jsonResponse({ ok: true });
43
+ };
@@ -0,0 +1,41 @@
1
+ import type { Context } from "@netlify/functions";
2
+ import {
3
+ broadcastTo,
4
+ jsonResponse,
5
+ requirePost,
6
+ requireString,
7
+ } from "./shared.mts";
8
+
9
+ export default async (req: Request, _context: Context) => {
10
+ const methodError = requirePost(req);
11
+ if (methodError) return methodError;
12
+
13
+ let body: Record<string, unknown>;
14
+ try {
15
+ body = await req.json();
16
+ } catch {
17
+ return jsonResponse({ error: "Invalid JSON" }, 400);
18
+ }
19
+
20
+ const { activeQuizId, sessionId, quizGroupId, results } = body;
21
+
22
+ for (const [val, name] of [
23
+ [sessionId, "sessionId"],
24
+ [quizGroupId, "quizGroupId"],
25
+ ] as const) {
26
+ const err = requireString(val, name);
27
+ if (err) return err;
28
+ }
29
+
30
+ try {
31
+ await broadcastTo(`quiz:${quizGroupId}:sync`, {
32
+ activeQuizId,
33
+ sessionId,
34
+ results,
35
+ });
36
+ } catch {
37
+ return jsonResponse({ error: "Broadcast failed" }, 502);
38
+ }
39
+
40
+ return jsonResponse({ ok: true });
41
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Shared utilities for quiz serverless functions.
3
+ * Copy these files to your project's functions directory.
4
+ */
5
+ import { broadcaster } from "@anycable/serverless-js";
6
+
7
+ const broadcastURL =
8
+ process.env.ANYCABLE_BROADCAST_URL || "http://127.0.0.1:8090/_broadcast";
9
+ const broadcastKey = process.env.ANYCABLE_BROADCAST_KEY || "";
10
+
11
+ export const broadcastTo = broadcaster(broadcastURL, broadcastKey);
12
+
13
+ export function jsonResponse(
14
+ body: Record<string, unknown>,
15
+ status = 200,
16
+ ): Response {
17
+ return new Response(JSON.stringify(body), {
18
+ status,
19
+ headers: { "Content-Type": "application/json" },
20
+ });
21
+ }
22
+
23
+ export function requirePost(req: Request): Response | null {
24
+ if (req.method !== "POST") {
25
+ return jsonResponse({ error: "Method not allowed" }, 405);
26
+ }
27
+ return null;
28
+ }
29
+
30
+ export function requireString(
31
+ value: unknown,
32
+ name: string,
33
+ ): Response | null {
34
+ if (typeof value !== "string" || value.length === 0) {
35
+ return jsonResponse({ error: `Missing field: ${name}` }, 400);
36
+ }
37
+ return null;
38
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "private": true,
3
+ "dependencies": {
4
+ "@anycable/serverless-js": "^0.2.2"
5
+ }
6
+ }
@@ -0,0 +1,37 @@
1
+ import { broadcastTo, jsonResponse, requirePost, requireString } from "./shared";
2
+
3
+ export default async function handler(req: Request) {
4
+ const methodError = requirePost(req);
5
+ if (methodError) return methodError;
6
+
7
+ let body: Record<string, unknown>;
8
+ try {
9
+ body = await req.json();
10
+ } catch {
11
+ return jsonResponse({ error: "Invalid JSON" }, 400);
12
+ }
13
+
14
+ const { quizId, answer, sessionId, quizGroupId } = body;
15
+
16
+ for (const [val, name] of [
17
+ [quizId, "quizId"],
18
+ [answer, "answer"],
19
+ [sessionId, "sessionId"],
20
+ [quizGroupId, "quizGroupId"],
21
+ ] as const) {
22
+ const err = requireString(val, name);
23
+ if (err) return err;
24
+ }
25
+
26
+ try {
27
+ await broadcastTo(`quiz:${quizGroupId}:results`, {
28
+ quizId,
29
+ answer,
30
+ sessionId,
31
+ });
32
+ } catch {
33
+ return jsonResponse({ error: "Broadcast failed" }, 502);
34
+ }
35
+
36
+ return jsonResponse({ ok: true });
37
+ }
@@ -0,0 +1,35 @@
1
+ import { broadcastTo, jsonResponse, requirePost, requireString } from "./shared";
2
+
3
+ export default async function handler(req: Request) {
4
+ const methodError = requirePost(req);
5
+ if (methodError) return methodError;
6
+
7
+ let body: Record<string, unknown>;
8
+ try {
9
+ body = await req.json();
10
+ } catch {
11
+ return jsonResponse({ error: "Invalid JSON" }, 400);
12
+ }
13
+
14
+ const { activeQuizId, sessionId, quizGroupId, results } = body;
15
+
16
+ for (const [val, name] of [
17
+ [sessionId, "sessionId"],
18
+ [quizGroupId, "quizGroupId"],
19
+ ] as const) {
20
+ const err = requireString(val, name);
21
+ if (err) return err;
22
+ }
23
+
24
+ try {
25
+ await broadcastTo(`quiz:${quizGroupId}:sync`, {
26
+ activeQuizId,
27
+ sessionId,
28
+ results,
29
+ });
30
+ } catch {
31
+ return jsonResponse({ error: "Broadcast failed" }, 502);
32
+ }
33
+
34
+ return jsonResponse({ ok: true });
35
+ }
@@ -0,0 +1,34 @@
1
+ import { broadcaster } from "@anycable/serverless-js";
2
+
3
+ const broadcastURL =
4
+ process.env.ANYCABLE_BROADCAST_URL || "http://127.0.0.1:8090/_broadcast";
5
+ const broadcastKey = process.env.ANYCABLE_BROADCAST_KEY || "";
6
+
7
+ export const broadcastTo = broadcaster(broadcastURL, broadcastKey);
8
+
9
+ export function jsonResponse(
10
+ body: Record<string, unknown>,
11
+ status = 200,
12
+ ): Response {
13
+ return new Response(JSON.stringify(body), {
14
+ status,
15
+ headers: { "Content-Type": "application/json" },
16
+ });
17
+ }
18
+
19
+ export function requirePost(req: Request): Response | null {
20
+ if (req.method !== "POST") {
21
+ return jsonResponse({ error: "Method not allowed" }, 405);
22
+ }
23
+ return null;
24
+ }
25
+
26
+ export function requireString(
27
+ value: unknown,
28
+ name: string,
29
+ ): Response | null {
30
+ if (typeof value !== "string" || value.length === 0) {
31
+ return jsonResponse({ error: `Missing field: ${name}` }, 400);
32
+ }
33
+ return null;
34
+ }