@modelriver/client 1.0.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 ADDED
@@ -0,0 +1,409 @@
1
+ # @modelriver/client
2
+
3
+ Official ModelRiver client SDK for real-time AI response streaming via WebSockets.
4
+
5
+ ## Features
6
+
7
+ - **WebSocket streaming** - Receive AI responses in real-time via Phoenix Channels
8
+ - **Auto-reconnection** - Automatically reconnects on connection loss
9
+ - **Persistence** - Survives page refreshes with localStorage persistence
10
+ - **Framework adapters** - First-class support for React, Vue, Angular, and Svelte
11
+ - **CDN ready** - Use via script tag without a build step
12
+ - **TypeScript** - Full type definitions included
13
+ - **Lightweight** - ~15KB minified (including Phoenix.js)
14
+
15
+ ## Installation
16
+
17
+ ### npm / yarn / pnpm
18
+
19
+ ```bash
20
+ npm install @modelriver/client
21
+ # or
22
+ yarn add @modelriver/client
23
+ # or
24
+ pnpm add @modelriver/client
25
+ ```
26
+
27
+ ### CDN
28
+
29
+ ```html
30
+ <script src="https://cdn.modelriver.com/client/v1.0.0/modelriver.min.js"></script>
31
+ <!-- or latest -->
32
+ <script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>
33
+ ```
34
+
35
+ ## Quick Start
36
+
37
+ ### 1. Get a token from your backend
38
+
39
+ Your backend calls the ModelRiver API and receives a WebSocket token:
40
+
41
+ ```javascript
42
+ // Your backend endpoint
43
+ const response = await fetch('/api/ai/request', {
44
+ method: 'POST',
45
+ body: JSON.stringify({ message: 'Hello AI' }),
46
+ });
47
+ const { ws_token } = await response.json();
48
+ ```
49
+
50
+ ### 2. Connect to ModelRiver
51
+
52
+ ```javascript
53
+ import { ModelRiverClient } from '@modelriver/client';
54
+
55
+ const client = new ModelRiverClient({
56
+ baseUrl: 'wss://api.modelriver.com/socket',
57
+ });
58
+
59
+ client.on('response', (data) => {
60
+ console.log('AI Response:', data);
61
+ });
62
+
63
+ client.on('error', (error) => {
64
+ console.error('Error:', error);
65
+ });
66
+
67
+ client.connect({ wsToken: ws_token });
68
+ ```
69
+
70
+ ## Framework Usage
71
+
72
+ ### React
73
+
74
+ ```tsx
75
+ import { useModelRiver } from '@modelriver/client/react';
76
+
77
+ function ChatComponent() {
78
+ const {
79
+ connect,
80
+ disconnect,
81
+ response,
82
+ error,
83
+ isConnected,
84
+ steps
85
+ } = useModelRiver({
86
+ baseUrl: 'wss://api.modelriver.com/socket',
87
+ persist: true,
88
+ });
89
+
90
+ const handleSend = async () => {
91
+ const { ws_token } = await yourBackendAPI.createRequest(message);
92
+ connect({ wsToken: ws_token });
93
+ };
94
+
95
+ return (
96
+ <div>
97
+ <button onClick={handleSend} disabled={isConnected}>
98
+ Send
99
+ </button>
100
+
101
+ {/* Show workflow progress */}
102
+ {steps.map((step) => (
103
+ <div key={step.id} className={step.status}>
104
+ {step.name}
105
+ </div>
106
+ ))}
107
+
108
+ {/* Show response */}
109
+ {response && (
110
+ <pre>{JSON.stringify(response.data, null, 2)}</pre>
111
+ )}
112
+
113
+ {/* Show error */}
114
+ {error && <p className="error">{error}</p>}
115
+ </div>
116
+ );
117
+ }
118
+ ```
119
+
120
+ ### Vue
121
+
122
+ ```vue
123
+ <script setup>
124
+ import { useModelRiver } from '@modelriver/client/vue';
125
+
126
+ const {
127
+ connect,
128
+ disconnect,
129
+ response,
130
+ error,
131
+ isConnected,
132
+ steps
133
+ } = useModelRiver({
134
+ baseUrl: 'wss://api.modelriver.com/socket',
135
+ });
136
+
137
+ async function handleSend() {
138
+ const { ws_token } = await yourBackendAPI.createRequest(message);
139
+ connect({ wsToken: ws_token });
140
+ }
141
+ </script>
142
+
143
+ <template>
144
+ <div>
145
+ <button @click="handleSend" :disabled="isConnected">Send</button>
146
+
147
+ <div v-for="step in steps" :key="step.id" :class="step.status">
148
+ {{ step.name }}
149
+ </div>
150
+
151
+ <pre v-if="response">{{ response.data }}</pre>
152
+ <p v-if="error" class="error">{{ error }}</p>
153
+ </div>
154
+ </template>
155
+ ```
156
+
157
+ ### Angular
158
+
159
+ ```typescript
160
+ import { Component, OnDestroy } from '@angular/core';
161
+ import { ModelRiverService } from '@modelriver/client/angular';
162
+
163
+ @Component({
164
+ selector: 'app-chat',
165
+ providers: [ModelRiverService],
166
+ template: `
167
+ <button (click)="send()" [disabled]="modelRiver.isConnected">
168
+ Send
169
+ </button>
170
+
171
+ <div *ngFor="let step of modelRiver.steps$ | async" [class]="step.status">
172
+ {{ step.name }}
173
+ </div>
174
+
175
+ <pre *ngIf="modelRiver.response$ | async as res">
176
+ {{ res.data | json }}
177
+ </pre>
178
+
179
+ <p *ngIf="modelRiver.error$ | async as err" class="error">
180
+ {{ err }}
181
+ </p>
182
+ `,
183
+ })
184
+ export class ChatComponent implements OnDestroy {
185
+ constructor(public modelRiver: ModelRiverService) {
186
+ this.modelRiver.init({
187
+ baseUrl: 'wss://api.modelriver.com/socket'
188
+ });
189
+ }
190
+
191
+ async send() {
192
+ const { ws_token } = await this.backendService.createRequest(message);
193
+ this.modelRiver.connect({ wsToken: ws_token });
194
+ }
195
+
196
+ ngOnDestroy() {
197
+ this.modelRiver.destroy();
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Svelte
203
+
204
+ ```svelte
205
+ <script>
206
+ import { createModelRiver } from '@modelriver/client/svelte';
207
+ import { onDestroy } from 'svelte';
208
+
209
+ const modelRiver = createModelRiver({
210
+ baseUrl: 'wss://api.modelriver.com/socket',
211
+ });
212
+
213
+ const { response, error, isConnected, steps, connect, disconnect } = modelRiver;
214
+
215
+ async function send() {
216
+ const { ws_token } = await backendAPI.createRequest(message);
217
+ connect({ wsToken: ws_token });
218
+ }
219
+
220
+ onDestroy(() => disconnect());
221
+ </script>
222
+
223
+ <button on:click={send} disabled={$isConnected}>Send</button>
224
+
225
+ {#each $steps as step}
226
+ <div class={step.status}>{step.name}</div>
227
+ {/each}
228
+
229
+ {#if $response}
230
+ <pre>{JSON.stringify($response.data, null, 2)}</pre>
231
+ {/if}
232
+
233
+ {#if $error}
234
+ <p class="error">{$error}</p>
235
+ {/if}
236
+ ```
237
+
238
+ ### Vanilla JavaScript (CDN)
239
+
240
+ ```html
241
+ <!DOCTYPE html>
242
+ <html>
243
+ <head>
244
+ <script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>
245
+ </head>
246
+ <body>
247
+ <button id="send">Send</button>
248
+ <pre id="response"></pre>
249
+
250
+ <script>
251
+ const client = new ModelRiver.ModelRiverClient({
252
+ baseUrl: 'wss://api.modelriver.com/socket',
253
+ });
254
+
255
+ client.on('response', (data) => {
256
+ document.getElementById('response').textContent =
257
+ JSON.stringify(data, null, 2);
258
+ });
259
+
260
+ client.on('error', (error) => {
261
+ console.error('Error:', error);
262
+ });
263
+
264
+ document.getElementById('send').addEventListener('click', async () => {
265
+ // Get token from your backend
266
+ const res = await fetch('/api/ai/request', { method: 'POST' });
267
+ const { ws_token } = await res.json();
268
+
269
+ client.connect({ wsToken: ws_token });
270
+ });
271
+ </script>
272
+ </body>
273
+ </html>
274
+ ```
275
+
276
+ ## API Reference
277
+
278
+ ### ModelRiverClient
279
+
280
+ #### Constructor Options
281
+
282
+ ```typescript
283
+ interface ModelRiverClientOptions {
284
+ baseUrl?: string; // WebSocket URL (default: 'wss://api.modelriver.com/socket')
285
+ debug?: boolean; // Enable debug logging (default: false)
286
+ persist?: boolean; // Enable localStorage persistence (default: true)
287
+ storageKeyPrefix?: string; // Storage key prefix (default: 'modelriver_')
288
+ heartbeatInterval?: number; // Heartbeat interval in ms (default: 30000)
289
+ requestTimeout?: number; // Request timeout in ms (default: 300000)
290
+ }
291
+ ```
292
+
293
+ #### Methods
294
+
295
+ | Method | Description |
296
+ |--------|-------------|
297
+ | `connect({ wsToken })` | Connect to WebSocket with token |
298
+ | `disconnect()` | Disconnect from WebSocket |
299
+ | `reset()` | Reset state and clear stored data |
300
+ | `reconnect()` | Reconnect using stored token |
301
+ | `getState()` | Get current client state |
302
+ | `hasPendingRequest()` | Check if there's a pending request |
303
+ | `on(event, callback)` | Add event listener (returns unsubscribe function) |
304
+ | `off(event, callback)` | Remove event listener |
305
+ | `destroy()` | Clean up all resources |
306
+
307
+ #### Events
308
+
309
+ | Event | Payload | Description |
310
+ |-------|---------|-------------|
311
+ | `connecting` | - | Connection attempt started |
312
+ | `connected` | - | Successfully connected |
313
+ | `disconnected` | `reason?: string` | Disconnected from WebSocket |
314
+ | `response` | `AIResponse` | AI response received |
315
+ | `error` | `Error \| string` | Error occurred |
316
+ | `step` | `WorkflowStep` | Workflow step updated |
317
+ | `channel_joined` | - | Successfully joined channel |
318
+ | `channel_error` | `reason: string` | Channel join failed |
319
+
320
+ ### Types
321
+
322
+ ```typescript
323
+ interface AIResponse {
324
+ status: string;
325
+ channel_id?: string;
326
+ data?: unknown;
327
+ meta?: {
328
+ workflow?: string;
329
+ status?: string;
330
+ duration_ms?: number;
331
+ usage?: {
332
+ prompt_tokens?: number;
333
+ completion_tokens?: number;
334
+ total_tokens?: number;
335
+ };
336
+ };
337
+ error?: {
338
+ message: string;
339
+ details?: unknown;
340
+ };
341
+ }
342
+
343
+ interface WorkflowStep {
344
+ id: string;
345
+ name: string;
346
+ status: 'pending' | 'loading' | 'success' | 'error';
347
+ duration?: number;
348
+ errorMessage?: string;
349
+ }
350
+ ```
351
+
352
+ ## How It Works
353
+
354
+ 1. **Your backend** calls the ModelRiver API to create an AI request
355
+ 2. **ModelRiver** returns a `ws_token` (JWT) containing connection details
356
+ 3. **Your frontend** uses this SDK to connect to ModelRiver's WebSocket
357
+ 4. **AI responses** are streamed in real-time to your frontend
358
+ 5. **The SDK** handles reconnection, heartbeats, and error recovery
359
+
360
+ ```
361
+ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
362
+ │ Frontend │ │ Your Backend │ │ ModelRiver │
363
+ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
364
+ │ │ │
365
+ │ 1. Request AI │ │
366
+ │─────────────────────>│ │
367
+ │ │ 2. Create request │
368
+ │ │─────────────────────>│
369
+ │ │ │
370
+ │ │ 3. Return ws_token │
371
+ │ │<─────────────────────│
372
+ │ 4. Return token │ │
373
+ │<─────────────────────│ │
374
+ │ │ │
375
+ │ 5. Connect WebSocket (SDK) │
376
+ │─────────────────────────────────────────────>│
377
+ │ │ │
378
+ │ 6. Stream AI response │
379
+ │<─────────────────────────────────────────────│
380
+ │ │ │
381
+ ```
382
+
383
+ ## Security
384
+
385
+ The `ws_token` is a short-lived JWT that:
386
+ - Contains `project_id`, `channel_id`, and `topic`
387
+ - Is decoded client-side (signature verified server-side)
388
+ - Expires after 5 minutes
389
+ - Should never be exposed in client-side code directly
390
+
391
+ **Important**: Always obtain tokens from your backend. Never expose your ModelRiver API key in frontend code.
392
+
393
+ ## Browser Support
394
+
395
+ - Chrome 60+
396
+ - Firefox 55+
397
+ - Safari 12+
398
+ - Edge 79+
399
+
400
+ ## License
401
+
402
+ MIT
403
+
404
+ ## Links
405
+
406
+ - [Documentation](https://modelriver.com/docs)
407
+ - [API Reference](https://modelriver.com/docs/api)
408
+ - [Dashboard](https://modelriver.com/dashboard)
409
+ - [GitHub Issues](https://github.com/modelriver/client/issues)
@@ -0,0 +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).ModelRiver={})}(this,function(e){"use strict";var t=e=>{if("function"==typeof e)return e;return function(){return e}},s="undefined"!=typeof self?self:null,i="undefined"!=typeof window?window:null,n=s||i||globalThis,o=0,r=1,h=2,a=3,c="closed",l="errored",u="joined",d="joining",p="leaving",g="phx_close",f="phx_error",m="phx_join",b="phx_reply",k="phx_leave",T="longpoll",v="websocket",C=4,E="base64url.bearer.phx.",y=class{constructor(e,t,s,i){this.channel=e,this.event=t,this.payload=s||function(){return{}},this.receivedResp=null,this.timeout=i,this.timeoutTimer=null,this.recHooks=[],this.sent=!1}resend(e){this.timeout=e,this.reset(),this.send()}send(){this.hasReceived("timeout")||(this.startTimeout(),this.sent=!0,this.channel.socket.push({topic:this.channel.topic,event:this.event,payload:this.payload(),ref:this.ref,join_ref:this.channel.joinRef()}))}receive(e,t){return this.hasReceived(e)&&t(this.receivedResp.response),this.recHooks.push({status:e,callback:t}),this}reset(){this.cancelRefEvent(),this.ref=null,this.refEvent=null,this.receivedResp=null,this.sent=!1}matchReceive({status:e,response:t,_ref:s}){this.recHooks.filter(t=>t.status===e).forEach(e=>e.callback(t))}cancelRefEvent(){this.refEvent&&this.channel.off(this.refEvent)}cancelTimeout(){clearTimeout(this.timeoutTimer),this.timeoutTimer=null}startTimeout(){this.timeoutTimer&&this.cancelTimeout(),this.ref=this.channel.socket.makeRef(),this.refEvent=this.channel.replyEventName(this.ref),this.channel.on(this.refEvent,e=>{this.cancelRefEvent(),this.cancelTimeout(),this.receivedResp=e,this.matchReceive(e)}),this.timeoutTimer=setTimeout(()=>{this.trigger("timeout",{})},this.timeout)}hasReceived(e){return this.receivedResp&&this.receivedResp.status===e}trigger(e,t){this.channel.trigger(this.refEvent,{status:e,response:t})}},w=class{constructor(e,t){this.callback=e,this.timerCalc=t,this.timer=null,this.tries=0}reset(){this.tries=0,clearTimeout(this.timer)}scheduleTimeout(){clearTimeout(this.timer),this.timer=setTimeout(()=>{this.tries=this.tries+1,this.callback()},this.timerCalc(this.tries+1))}},R=class{constructor(e,s,i){this.state=c,this.topic=e,this.params=t(s||{}),this.socket=i,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new y(this,m,this.params,this.timeout),this.pushBuffer=[],this.stateChangeRefs=[],this.rejoinTimer=new w(()=>{this.socket.isConnected()&&this.rejoin()},this.socket.rejoinAfterMs),this.stateChangeRefs.push(this.socket.onError(()=>this.rejoinTimer.reset())),this.stateChangeRefs.push(this.socket.onOpen(()=>{this.rejoinTimer.reset(),this.isErrored()&&this.rejoin()})),this.joinPush.receive("ok",()=>{this.state=u,this.rejoinTimer.reset(),this.pushBuffer.forEach(e=>e.send()),this.pushBuffer=[]}),this.joinPush.receive("error",()=>{this.state=l,this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.onClose(()=>{this.rejoinTimer.reset(),this.socket.hasLogger()&&this.socket.log("channel",`close ${this.topic} ${this.joinRef()}`),this.state=c,this.socket.remove(this)}),this.onError(e=>{this.socket.hasLogger()&&this.socket.log("channel",`error ${this.topic}`,e),this.isJoining()&&this.joinPush.reset(),this.state=l,this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.joinPush.receive("timeout",()=>{this.socket.hasLogger()&&this.socket.log("channel",`timeout ${this.topic} (${this.joinRef()})`,this.joinPush.timeout),new y(this,k,t({}),this.timeout).send(),this.state=l,this.joinPush.reset(),this.socket.isConnected()&&this.rejoinTimer.scheduleTimeout()}),this.on(b,(e,t)=>{this.trigger(this.replyEventName(t),e)})}join(e=this.timeout){if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}onClose(e){this.on(g,e)}onError(e){return this.on(f,t=>e(t))}on(e,t){let s=this.bindingRef++;return this.bindings.push({event:e,ref:s,callback:t}),s}off(e,t){this.bindings=this.bindings.filter(s=>!(s.event===e&&(void 0===t||t===s.ref)))}canPush(){return this.socket.isConnected()&&this.isJoined()}push(e,t,s=this.timeout){if(t=t||{},!this.joinedOnce)throw new Error(`tried to push '${e}' to '${this.topic}' before joining. Use channel.join() before pushing events`);let i=new y(this,e,function(){return t},s);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}leave(e=this.timeout){this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=p;let s=()=>{this.socket.hasLogger()&&this.socket.log("channel",`leave ${this.topic}`),this.trigger(g,"leave")},i=new y(this,k,t({}),e);return i.receive("ok",()=>s()).receive("timeout",()=>s()),i.send(),this.canPush()||i.trigger("ok",{}),i}onMessage(e,t,s){return t}isMember(e,t,s,i){return this.topic===e&&(!i||i===this.joinRef()||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:s,joinRef:i}),!1))}joinRef(){return this.joinPush.ref}rejoin(e=this.timeout){this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=d,this.joinPush.resend(e))}trigger(e,t,s,i){let n=this.onMessage(e,t,s,i);if(t&&!n)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");let o=this.bindings.filter(t=>t.event===e);for(let e=0;e<o.length;e++){o[e].callback(n,s,i||this.joinRef())}}replyEventName(e){return`chan_reply_${e}`}isClosed(){return this.state===c}isErrored(){return this.state===l}isJoined(){return this.state===u}isJoining(){return this.state===d}isLeaving(){return this.state===p}},S=class{static request(e,t,s,i,o,r,h){if(n.XDomainRequest){let s=new n.XDomainRequest;return this.xdomainRequest(s,e,t,i,o,r,h)}if(n.XMLHttpRequest){let a=new n.XMLHttpRequest;return this.xhrRequest(a,e,t,s,i,o,r,h)}if(n.fetch&&n.AbortController)return this.fetchRequest(e,t,s,i,o,r,h);throw new Error("No suitable XMLHttpRequest implementation found")}static fetchRequest(e,t,s,i,o,r,h){let a={method:e,headers:s,body:i},c=null;return o&&(c=new AbortController,setTimeout(()=>c.abort(),o),a.signal=c.signal),n.fetch(t,a).then(e=>e.text()).then(e=>this.parseJSON(e)).then(e=>h&&h(e)).catch(e=>{"AbortError"===e.name&&r?r():h&&h(null)}),c}static xdomainRequest(e,t,s,i,n,o,r){return e.timeout=n,e.open(t,s),e.onload=()=>{let t=this.parseJSON(e.responseText);r&&r(t)},o&&(e.ontimeout=o),e.onprogress=()=>{},e.send(i),e}static xhrRequest(e,t,s,i,n,o,r,h){e.open(t,s,!0),e.timeout=o;for(let[t,s]of Object.entries(i))e.setRequestHeader(t,s);return e.onerror=()=>h&&h(null),e.onreadystatechange=()=>{if(e.readyState===C&&h){let t=this.parseJSON(e.responseText);h(t)}},r&&(e.ontimeout=r),e.send(n),e}static parseJSON(e){if(!e||""===e)return null;try{return JSON.parse(e)}catch{return console&&console.log("failed to parse JSON response",e),null}}static serialize(e,t){let s=[];for(var i in e){if(!Object.prototype.hasOwnProperty.call(e,i))continue;let n=t?`${t}[${i}]`:i,o=e[i];"object"==typeof o?s.push(this.serialize(o,n)):s.push(encodeURIComponent(n)+"="+encodeURIComponent(o))}return s.join("&")}static appendParams(e,t){if(0===Object.keys(t).length)return e;let s=e.match(/\?/)?"&":"?";return`${e}${s}${this.serialize(t)}`}},j=class{constructor(e,t){t&&2===t.length&&t[1].startsWith(E)&&(this.authToken=atob(t[1].slice(21))),this.endPoint=null,this.token=null,this.skipHeartbeat=!0,this.reqs=new Set,this.awaitingBatchAck=!1,this.currentBatch=null,this.currentBatchTimer=null,this.batchBuffer=[],this.onopen=function(){},this.onerror=function(){},this.onmessage=function(){},this.onclose=function(){},this.pollEndpoint=this.normalizeEndpoint(e),this.readyState=o,setTimeout(()=>this.poll(),0)}normalizeEndpoint(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+v),"$1/"+T)}endpointURL(){return S.appendParams(this.pollEndpoint,{token:this.token})}closeAndRetry(e,t,s){this.close(e,t,s),this.readyState=o}ontimeout(){this.onerror("timeout"),this.closeAndRetry(1005,"timeout",!1)}isActive(){return this.readyState===r||this.readyState===o}poll(){const e={Accept:"application/json"};this.authToken&&(e["X-Phoenix-AuthToken"]=this.authToken),this.ajax("GET",e,null,()=>this.ontimeout(),e=>{if(e){var{status:t,token:s,messages:i}=e;if(410===t&&null!==this.token)return this.onerror(410),void this.closeAndRetry(3410,"session_gone",!1);this.token=s}else t=0;switch(t){case 200:i.forEach(e=>{setTimeout(()=>this.onmessage({data:e}),0)}),this.poll();break;case 204:this.poll();break;case 410:this.readyState=r,this.onopen({}),this.poll();break;case 403:this.onerror(403),this.close(1008,"forbidden",!1);break;case 0:case 500:this.onerror(500),this.closeAndRetry(1011,"internal server error",500);break;default:throw new Error(`unhandled poll status ${t}`)}})}send(e){"string"!=typeof e&&(e=(e=>{let t="",s=new Uint8Array(e),i=s.byteLength;for(let e=0;e<i;e++)t+=String.fromCharCode(s[e]);return btoa(t)})(e)),this.currentBatch?this.currentBatch.push(e):this.awaitingBatchAck?this.batchBuffer.push(e):(this.currentBatch=[e],this.currentBatchTimer=setTimeout(()=>{this.batchSend(this.currentBatch),this.currentBatch=null},0))}batchSend(e){this.awaitingBatchAck=!0,this.ajax("POST",{"Content-Type":"application/x-ndjson"},e.join("\n"),()=>this.onerror("timeout"),e=>{this.awaitingBatchAck=!1,e&&200===e.status?this.batchBuffer.length>0&&(this.batchSend(this.batchBuffer),this.batchBuffer=[]):(this.onerror(e&&e.status),this.closeAndRetry(1011,"internal server error",!1))})}close(e,t,s){for(let e of this.reqs)e.abort();this.readyState=a;let i=Object.assign({code:1e3,reason:void 0,wasClean:!0},{code:e,reason:t,wasClean:s});this.batchBuffer=[],clearTimeout(this.currentBatchTimer),this.currentBatchTimer=null,"undefined"!=typeof CloseEvent?this.onclose(new CloseEvent("close",i)):this.onclose(i)}ajax(e,t,s,i,n){let o;o=S.request(e,this.endpointURL(),t,s,this.timeout,()=>{this.reqs.delete(o),i()},e=>{this.reqs.delete(o),this.isActive()&&n(e)}),this.reqs.add(o)}},A={HEADER_LENGTH:1,META_LENGTH:4,KINDS:{push:0,reply:1,broadcast:2},encode(e,t){if(e.payload.constructor===ArrayBuffer)return t(this.binaryEncode(e));{let s=[e.join_ref,e.ref,e.topic,e.event,e.payload];return t(JSON.stringify(s))}},decode(e,t){if(e.constructor===ArrayBuffer)return t(this.binaryDecode(e));{let[s,i,n,o,r]=JSON.parse(e);return t({join_ref:s,ref:i,topic:n,event:o,payload:r})}},binaryEncode(e){let{join_ref:t,ref:s,event:i,topic:n,payload:o}=e,r=this.META_LENGTH+t.length+s.length+n.length+i.length,h=new ArrayBuffer(this.HEADER_LENGTH+r),a=new DataView(h),c=0;a.setUint8(c++,this.KINDS.push),a.setUint8(c++,t.length),a.setUint8(c++,s.length),a.setUint8(c++,n.length),a.setUint8(c++,i.length),Array.from(t,e=>a.setUint8(c++,e.charCodeAt(0))),Array.from(s,e=>a.setUint8(c++,e.charCodeAt(0))),Array.from(n,e=>a.setUint8(c++,e.charCodeAt(0))),Array.from(i,e=>a.setUint8(c++,e.charCodeAt(0)));var l=new Uint8Array(h.byteLength+o.byteLength);return l.set(new Uint8Array(h),0),l.set(new Uint8Array(o),h.byteLength),l.buffer},binaryDecode(e){let t=new DataView(e),s=t.getUint8(0),i=new TextDecoder;switch(s){case this.KINDS.push:return this.decodePush(e,t,i);case this.KINDS.reply:return this.decodeReply(e,t,i);case this.KINDS.broadcast:return this.decodeBroadcast(e,t,i)}},decodePush(e,t,s){let i=t.getUint8(1),n=t.getUint8(2),o=t.getUint8(3),r=this.HEADER_LENGTH+this.META_LENGTH-1,h=s.decode(e.slice(r,r+i));r+=i;let a=s.decode(e.slice(r,r+n));r+=n;let c=s.decode(e.slice(r,r+o));return r+=o,{join_ref:h,ref:null,topic:a,event:c,payload:e.slice(r,e.byteLength)}},decodeReply(e,t,s){let i=t.getUint8(1),n=t.getUint8(2),o=t.getUint8(3),r=t.getUint8(4),h=this.HEADER_LENGTH+this.META_LENGTH,a=s.decode(e.slice(h,h+i));h+=i;let c=s.decode(e.slice(h,h+n));h+=n;let l=s.decode(e.slice(h,h+o));h+=o;let u=s.decode(e.slice(h,h+r));h+=r;let d=e.slice(h,e.byteLength);return{join_ref:a,ref:c,topic:l,event:b,payload:{status:u,response:d}}},decodeBroadcast(e,t,s){let i=t.getUint8(1),n=t.getUint8(2),o=this.HEADER_LENGTH+2,r=s.decode(e.slice(o,o+i));o+=i;let h=s.decode(e.slice(o,o+n));return o+=n,{join_ref:null,ref:null,topic:r,event:h,payload:e.slice(o,e.byteLength)}}},_=class{constructor(e,s={}){this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.fallbackRef=null,this.timeout=s.timeout||1e4,this.transport=s.transport||n.WebSocket||j,this.primaryPassedHealthCheck=!1,this.longPollFallbackMs=s.longPollFallbackMs,this.fallbackTimer=null,this.sessionStore=s.sessionStorage||n&&n.sessionStorage,this.establishedConnections=0,this.defaultEncoder=A.encode.bind(A),this.defaultDecoder=A.decode.bind(A),this.closeWasClean=!1,this.disconnecting=!1,this.binaryType=s.binaryType||"arraybuffer",this.connectClock=1,this.pageHidden=!1,this.transport!==j?(this.encode=s.encode||this.defaultEncoder,this.decode=s.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder);let o=null;i&&i.addEventListener&&(i.addEventListener("pagehide",e=>{this.conn&&(this.disconnect(),o=this.connectClock)}),i.addEventListener("pageshow",e=>{o===this.connectClock&&(o=null,this.connect())}),i.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState?this.pageHidden=!0:(this.pageHidden=!1,this.isConnected()||this.teardown(()=>this.connect()))})),this.heartbeatIntervalMs=s.heartbeatIntervalMs||3e4,this.rejoinAfterMs=e=>s.rejoinAfterMs?s.rejoinAfterMs(e):[1e3,2e3,5e3][e-1]||1e4,this.reconnectAfterMs=e=>s.reconnectAfterMs?s.reconnectAfterMs(e):[10,50,100,150,200,250,500,1e3,2e3][e-1]||5e3,this.logger=s.logger||null,!this.logger&&s.debug&&(this.logger=(e,t,s)=>{console.log(`${e}: ${t}`,s)}),this.longpollerTimeout=s.longpollerTimeout||2e4,this.params=t(s.params||{}),this.endPoint=`${e}/${v}`,this.vsn=s.vsn||"2.0.0",this.heartbeatTimeoutTimer=null,this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new w(()=>{if(this.pageHidden)return this.log("Not reconnecting as page is hidden!"),void this.teardown();this.teardown(()=>this.connect())},this.reconnectAfterMs),this.authToken=s.authToken}getLongPollTransport(){return j}replaceTransport(e){this.connectClock++,this.closeWasClean=!0,clearTimeout(this.fallbackTimer),this.reconnectTimer.reset(),this.conn&&(this.conn.close(),this.conn=null),this.transport=e}protocol(){return location.protocol.match(/^https/)?"wss":"ws"}endPointURL(){let e=S.appendParams(S.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?`${this.protocol()}:${e}`:`${this.protocol()}://${location.host}${e}`}disconnect(e,t,s){this.connectClock++,this.disconnecting=!0,this.closeWasClean=!0,clearTimeout(this.fallbackTimer),this.reconnectTimer.reset(),this.teardown(()=>{this.disconnecting=!1,e&&e()},t,s)}connect(e){e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=t(e)),this.conn&&!this.disconnecting||(this.longPollFallbackMs&&this.transport!==j?this.connectWithFallback(j,this.longPollFallbackMs):this.transportConnect())}log(e,t,s){this.logger&&this.logger(e,t,s)}hasLogger(){return null!==this.logger}onOpen(e){let t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}onClose(e){let t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}onError(e){let t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}onMessage(e){let t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}ping(e){if(!this.isConnected())return!1;let t=this.makeRef(),s=Date.now();this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:t});let i=this.onMessage(n=>{n.ref===t&&(this.off([i]),e(Date.now()-s))});return!0}transportConnect(){let e;this.connectClock++,this.closeWasClean=!1,this.authToken&&(e=["phoenix",`${E}${btoa(this.authToken).replace(/=/g,"")}`]),this.conn=new this.transport(this.endPointURL(),e),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=()=>this.onConnOpen(),this.conn.onerror=e=>this.onConnError(e),this.conn.onmessage=e=>this.onConnMessage(e),this.conn.onclose=e=>this.onConnClose(e)}getSession(e){return this.sessionStore&&this.sessionStore.getItem(e)}storeSession(e,t){this.sessionStore&&this.sessionStore.setItem(e,t)}connectWithFallback(e,t=2500){clearTimeout(this.fallbackTimer);let s,i=!1,n=!0,o=t=>{this.log("transport",`falling back to ${e.name}...`,t),this.off([undefined,s]),n=!1,this.replaceTransport(e),this.transportConnect()};if(this.getSession(`phx:fallback:${e.name}`))return o("memorized");this.fallbackTimer=setTimeout(o,t),s=this.onError(e=>{this.log("transport","error",e),n&&!i&&(clearTimeout(this.fallbackTimer),o(e))}),this.fallbackRef&&this.off([this.fallbackRef]),this.fallbackRef=this.onOpen(()=>{if(i=!0,!n)return this.primaryPassedHealthCheck||this.storeSession(`phx:fallback:${e.name}`,"true"),this.log("transport",`established ${e.name} fallback`);clearTimeout(this.fallbackTimer),this.fallbackTimer=setTimeout(o,t),this.ping(e=>{this.log("transport","connected to primary after",e),this.primaryPassedHealthCheck=!0,clearTimeout(this.fallbackTimer)})}),this.transportConnect()}clearHeartbeats(){clearTimeout(this.heartbeatTimer),clearTimeout(this.heartbeatTimeoutTimer)}onConnOpen(){this.hasLogger()&&this.log("transport",`${this.transport.name} connected to ${this.endPointURL()}`),this.closeWasClean=!1,this.disconnecting=!1,this.establishedConnections++,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach(([,e])=>e())}heartbeatTimeout(){this.pendingHeartbeatRef&&(this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),this.triggerChanError(),this.closeWasClean=!1,this.teardown(()=>this.reconnectTimer.scheduleTimeout(),1e3,"heartbeat timeout"))}resetHeartbeat(){this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,this.clearHeartbeats(),this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs))}teardown(e,t,s){if(!this.conn)return e&&e();let i=this.connectClock;this.waitForBufferDone(()=>{i===this.connectClock&&(this.conn&&(t?this.conn.close(t,s||""):this.conn.close()),this.waitForSocketClosed(()=>{i===this.connectClock&&(this.conn&&(this.conn.onopen=function(){},this.conn.onerror=function(){},this.conn.onmessage=function(){},this.conn.onclose=function(){},this.conn=null),e&&e())}))})}waitForBufferDone(e,t=1){5!==t&&this.conn&&this.conn.bufferedAmount?setTimeout(()=>{this.waitForBufferDone(e,t+1)},150*t):e()}waitForSocketClosed(e,t=1){5!==t&&this.conn&&this.conn.readyState!==a?setTimeout(()=>{this.waitForSocketClosed(e,t+1)},150*t):e()}onConnClose(e){this.conn&&(this.conn.onclose=()=>{});let t=e&&e.code;this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),this.clearHeartbeats(),this.closeWasClean||1e3===t||this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach(([,t])=>t(e))}onConnError(e){this.hasLogger()&&this.log("transport",e);let t=this.transport,s=this.establishedConnections;this.stateChangeCallbacks.error.forEach(([,i])=>{i(e,t,s)}),(t===this.transport||s>0)&&this.triggerChanError()}triggerChanError(){this.channels.forEach(e=>{e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(f)})}connectionState(){switch(this.conn&&this.conn.readyState){case o:return"connecting";case r:return"open";case h:return"closing";default:return"closed"}}isConnected(){return"open"===this.connectionState()}remove(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter(t=>t!==e)}off(e){for(let t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter(([t])=>-1===e.indexOf(t))}channel(e,t={}){let s=new R(e,t,this);return this.channels.push(s),s}push(e){if(this.hasLogger()){let{topic:t,event:s,payload:i,ref:n,join_ref:o}=e;this.log("push",`${t} ${s} (${o}, ${n})`,i)}this.isConnected()?this.encode(e,e=>this.conn.send(e)):this.sendBuffer.push(()=>this.encode(e,e=>this.conn.send(e)))}makeRef(){let e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}sendHeartbeat(){this.pendingHeartbeatRef&&!this.isConnected()||(this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef}),this.heartbeatTimeoutTimer=setTimeout(()=>this.heartbeatTimeout(),this.heartbeatIntervalMs))}flushSendBuffer(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach(e=>e()),this.sendBuffer=[])}onConnMessage(e){this.decode(e.data,e=>{let{topic:t,event:s,payload:i,ref:n,join_ref:o}=e;n&&n===this.pendingHeartbeatRef&&(this.clearHeartbeats(),this.pendingHeartbeatRef=null,this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs)),this.hasLogger()&&this.log("receive",`${i.status||""} ${t} ${s} ${n&&"("+n+")"||""}`,i);for(let e=0;e<this.channels.length;e++){const r=this.channels[e];r.isMember(t,s,i,o)&&r.trigger(s,i,n,o)}for(let t=0;t<this.stateChangeCallbacks.message.length;t++){let[,s]=this.stateChangeCallbacks.message[t];s(e)}})}leaveOpenTopic(e){let t=this.channels.find(t=>t.topic===e&&(t.isJoined()||t.isJoining()));t&&(this.hasLogger()&&this.log("transport",`leaving duplicate topic "${e}"`),t.leave())}};const H="wss://api.modelriver.com/socket",$=3e5,L="active_request";function P(e){if(!e||"string"!=typeof e)throw new Error("Invalid token: token must be a non-empty string");const t=e.split(".");if(3!==t.length)throw new Error("Invalid token: JWT must have 3 parts");try{const e=JSON.parse(function(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");const s=t.length%4;s&&(t+="=".repeat(4-s));try{return atob(t)}catch{throw new Error("Invalid base64url string")}}(t[1]));if(!e.project_id||!e.channel_id)throw new Error("Invalid token: missing required fields (project_id, channel_id)");const s=e.topic||`ai_response:${e.project_id}:${e.channel_id}`;return{project_id:e.project_id,channel_id:e.channel_id,topic:s,exp:e.exp}}catch(e){if(e instanceof Error&&e.message.startsWith("Invalid token:"))throw e;throw new Error("Invalid token: failed to decode payload")}}function I(e){return!!e.exp&&Date.now()>=1e3*e.exp}function U(e,t){return`${e.endsWith("/websocket")?e:`${e}/websocket`}?token=${encodeURIComponent(t)}&vsn=2.0.0`}function x(){try{const e="__modelriver_test__";return localStorage.setItem(e,"test"),localStorage.removeItem(e),!0}catch{return!1}}function M(e){if(!x())return null;try{const t=localStorage.getItem(`${e}${L}`);if(!t)return null;const s=JSON.parse(t);return Date.now()-s.timestamp>$?(B(e),null):s}catch{return null}}function B(e){if(x())try{localStorage.removeItem(`${e}${L}`)}catch{}}e.DEFAULT_BASE_URL=H,e.DEFAULT_HEARTBEAT_INTERVAL=3e4,e.DEFAULT_REQUEST_TIMEOUT=$,e.ModelRiverClient=class{constructor(e={}){this.socket=null,this.channel=null,this.heartbeatInterval=null,this.connectionState="disconnected",this.steps=[],this.response=null,this.error=null,this.currentToken=null,this.currentWsToken=null,this.isConnecting=!1,this.listeners=new Map,this.options={baseUrl:e.baseUrl??H,debug:e.debug??!1,persist:e.persist??!0,storageKeyPrefix:e.storageKeyPrefix??"modelriver_",heartbeatInterval:e.heartbeatInterval??3e4,requestTimeout:e.requestTimeout??$},this.logger=function(e){const t="[ModelRiver]";return{log:(...s)=>{e&&console.log(t,...s)},warn:(...s)=>{e&&console.warn(t,...s)},error:(...e)=>{console.error(t,...e)}}}(this.options.debug),this.logger.log("Client initialized with options:",this.options)}getState(){return{connectionState:this.connectionState,isConnected:"connected"===this.connectionState,isConnecting:this.isConnecting,steps:[...this.steps],response:this.response,error:this.error,hasPendingRequest:this.hasPendingRequest()}}hasPendingRequest(){if(!this.options.persist)return!1;return null!==M(this.options.storageKeyPrefix)}connect(e){if(this.isConnecting)return void this.logger.warn("Connection already in progress, skipping...");const{wsToken:t}=e;let s;try{s=P(t),this.logger.log("Token decoded:",{projectId:s.project_id,channelId:s.channel_id,topic:s.topic})}catch(e){const t=e instanceof Error?e.message:"Invalid token";return this.setError(t),void this.emit("error",t)}if(I(s)){const e="Token has expired";return this.setError(e),void this.emit("error",e)}this.isConnecting=!0,this.currentToken=s,this.currentWsToken=t,this.emit("connecting"),this.cleanupConnection(),this.steps=[{id:"queue",name:"Queueing request",status:"pending"},{id:"process",name:"Processing AI request",status:"pending"},{id:"receive",name:"Waiting for response",status:"pending"},{id:"complete",name:"Response received",status:"pending"}],this.error=null,this.response=null,this.options.persist&&function(e,t,s,i){if(!x())return;const n={channelId:s,timestamp:Date.now(),projectId:t,wsToken:i};try{localStorage.setItem(`${e}${L}`,JSON.stringify(n))}catch{}}(this.options.storageKeyPrefix,s.project_id,s.channel_id,t),this.updateStepAndEmit("queue",{status:"loading"});const i=U(this.options.baseUrl,t);this.logger.log("Connecting to:",i.replace(t,"***TOKEN***")),this.socket=new _(this.options.baseUrl,{params:{token:t}}),this.socket.onOpen(()=>{this.logger.log("Socket connected"),this.connectionState="connected",this.isConnecting=!1,this.emit("connected"),this.joinChannel(s.topic)}),this.socket.onError(e=>{this.logger.error("Socket error:",e),this.connectionState="error",this.isConnecting=!1;const t="WebSocket connection error";this.setError(t),this.updateStepAndEmit("queue",{status:"error",errorMessage:t}),this.emit("error",t)}),this.socket.onClose(e=>{this.logger.log("Socket closed:",e),this.connectionState="disconnected",this.isConnecting=!1,this.stopHeartbeat(),this.emit("disconnected","Socket closed")}),this.socket.connect()}joinChannel(e){this.socket&&(this.logger.log("Joining channel:",e),this.channel=this.socket.channel(e,{}),this.channel.join().receive("ok",()=>{this.logger.log("Channel joined successfully"),this.updateStepAndEmit("queue",{status:"success",duration:100}),this.updateStepAndEmit("process",{status:"loading"}),this.updateStepAndEmit("receive",{status:"loading"}),this.emit("channel_joined"),this.startHeartbeat()}).receive("error",e=>{const t=e?.reason||"unknown";this.logger.error("Channel join failed:",t);let s="Failed to join channel";"unauthorized_project_access"===t?s="Unauthorized: You do not have access to this project":"invalid_channel_format"===t?s="Invalid channel format":"invalid_project_uuid"===t||"invalid_channel_uuid"===t?s="Invalid project or channel ID":"unknown"!==t&&(s=`Channel join failed: ${t}`),this.setError(s),this.updateStepAndEmit("queue",{status:"error",errorMessage:s}),this.emit("channel_error",t)}),this.channel.on("response",e=>{this.logger.log("AI Response received:",e),this.handleResponse(e)}),this.channel.on("error",e=>{const t=e?.message||"An error occurred";this.logger.error("Channel error:",t),this.handleError(t)}))}handleResponse(e){if("success"===e.status||"SUCCESS"===e.status||"success"===e.meta?.status||"ok"===e.status)this.updateStepAndEmit("process",{status:"success",duration:e.meta?.duration_ms}),this.updateStepAndEmit("receive",{status:"success",duration:50}),this.updateStepAndEmit("complete",{status:"success"}),this.response=e;else{const t=e.error?.message||"Unknown error";this.updateStepAndEmit("process",{status:"error",errorMessage:t}),this.updateStepAndEmit("receive",{status:"error"}),this.updateStepAndEmit("complete",{status:"error"}),this.setError(t)}this.options.persist&&B(this.options.storageKeyPrefix),this.emit("response",e),setTimeout(()=>{this.cleanupConnection()},1e3)}handleError(e){this.setError(e),this.updateStepAndEmit("process",{status:"error",errorMessage:e}),this.emit("error",e),this.options.persist&&B(this.options.storageKeyPrefix)}disconnect(){this.logger.log("Disconnecting..."),this.isConnecting=!1,this.cleanupConnection(),this.options.persist&&B(this.options.storageKeyPrefix),this.emit("disconnected","Manual disconnect")}reset(){this.logger.log("Resetting..."),this.disconnect(),this.steps=[],this.response=null,this.error=null,this.currentToken=null,this.currentWsToken=null}reconnect(){if(!this.options.persist)return this.logger.warn("Persistence is disabled, cannot reconnect"),!1;const e=M(this.options.storageKeyPrefix);return e?(this.logger.log("Reconnecting with stored token..."),this.connect({wsToken:e.wsToken}),!0):(this.logger.log("No active request found for reconnection"),!1)}on(e,t){return this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t),()=>{this.listeners.get(e)?.delete(t)}}off(e,t){this.listeners.get(e)?.delete(t)}emit(e,...t){const s=this.listeners.get(e);s&&s.forEach(s=>{try{s(...t)}catch(t){this.logger.error(`Error in ${e} listener:`,t)}})}updateStepAndEmit(e,t){this.steps=function(e,t,s){return e.map(e=>e.id===t?{...e,...s}:e)}(this.steps,e,t);const s=this.steps.find(t=>t.id===e);s&&this.emit("step",s)}setError(e){this.error=e}startHeartbeat(){this.stopHeartbeat(),this.heartbeatInterval=setInterval(()=>{this.channel&&this.channel.push("heartbeat",{})},this.options.heartbeatInterval)}stopHeartbeat(){this.heartbeatInterval&&(clearInterval(this.heartbeatInterval),this.heartbeatInterval=null)}cleanupConnection(){if(this.stopHeartbeat(),this.channel){try{this.channel.leave()}catch{}this.channel=null}if(this.socket){try{this.socket.disconnect()}catch{}this.socket=null}this.connectionState="disconnected"}destroy(){this.reset(),this.listeners.clear()}},e.buildWebSocketUrl=U,e.decodeToken=P,e.isStorageAvailable=x,e.isTokenExpired=I});
2
+ //# sourceMappingURL=modelriver.umd.js.map