@modelriver/client 1.1.0 → 1.1.2

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 CHANGED
@@ -34,17 +34,26 @@ pnpm add @modelriver/client
34
34
 
35
35
  ## Quick Start
36
36
 
37
- ### 1. Get a token from your backend
37
+ ### 1. Get channel ID from your backend
38
38
 
39
- Your backend calls the ModelRiver API and receives a WebSocket token:
39
+ Your backend calls the ModelRiver `/api/ai/async` endpoint and receives connection details:
40
40
 
41
41
  ```javascript
42
- // Your backend endpoint
42
+ // Your backend endpoint proxies to ModelRiver
43
43
  const response = await fetch('/api/ai/request', {
44
44
  method: 'POST',
45
45
  body: JSON.stringify({ message: 'Hello AI' }),
46
46
  });
47
- const { ws_token } = await response.json();
47
+
48
+ // Response from /api/ai/async:
49
+ // {
50
+ // "message": "success",
51
+ // "status": "pending",
52
+ // "channel_id": "a1b2c3d4-...",
53
+ // "websocket_url": "wss://api.modelriver.com/socket",
54
+ // "websocket_channel": "ai_response:a1b2c3d4-..."
55
+ // }
56
+ const { channel_id, websocket_url, websocket_channel } = await response.json();
48
57
  ```
49
58
 
50
59
  ### 2. Connect to ModelRiver
@@ -64,7 +73,7 @@ client.on('error', (error) => {
64
73
  console.error('Error:', error);
65
74
  });
66
75
 
67
- client.connect({ wsToken: ws_token });
76
+ client.connect({ channelId: channel_id, websocketUrl: websocket_url });
68
77
  ```
69
78
 
70
79
  ## Framework Usage
@@ -88,8 +97,8 @@ function ChatComponent() {
88
97
  });
89
98
 
90
99
  const handleSend = async () => {
91
- const { ws_token } = await yourBackendAPI.createRequest(message);
92
- connect({ wsToken: ws_token });
100
+ const { channel_id, websocket_url } = await yourBackendAPI.createRequest(message);
101
+ connect({ channelId: channel_id, websocketUrl: websocket_url });
93
102
  };
94
103
 
95
104
  return (
@@ -135,8 +144,8 @@ const {
135
144
  });
136
145
 
137
146
  async function handleSend() {
138
- const { ws_token } = await yourBackendAPI.createRequest(message);
139
- connect({ wsToken: ws_token });
147
+ const { channel_id, websocket_url } = await yourBackendAPI.createRequest(message);
148
+ connect({ channelId: channel_id, websocketUrl: websocket_url });
140
149
  }
141
150
  </script>
142
151
 
@@ -189,8 +198,8 @@ export class ChatComponent implements OnDestroy {
189
198
  }
190
199
 
191
200
  async send() {
192
- const { ws_token } = await this.backendService.createRequest(message);
193
- this.modelRiver.connect({ wsToken: ws_token });
201
+ const { channel_id, websocket_url } = await this.backendService.createRequest(message);
202
+ this.modelRiver.connect({ channelId: channel_id, websocketUrl: websocket_url });
194
203
  }
195
204
 
196
205
  ngOnDestroy() {
@@ -213,8 +222,8 @@ export class ChatComponent implements OnDestroy {
213
222
  const { response, error, isConnected, steps, connect, disconnect } = modelRiver;
214
223
 
215
224
  async function send() {
216
- const { ws_token } = await backendAPI.createRequest(message);
217
- connect({ wsToken: ws_token });
225
+ const { channel_id, websocket_url } = await backendAPI.createRequest(message);
226
+ connect({ channelId: channel_id, websocketUrl: websocket_url });
218
227
  }
219
228
 
220
229
  onDestroy(() => disconnect());
@@ -262,11 +271,11 @@ export class ChatComponent implements OnDestroy {
262
271
  });
263
272
 
264
273
  document.getElementById('send').addEventListener('click', async () => {
265
- // Get token from your backend
274
+ // Get channel ID from your backend
266
275
  const res = await fetch('/api/ai/request', { method: 'POST' });
267
- const { ws_token } = await res.json();
276
+ const { channel_id, websocket_url } = await res.json();
268
277
 
269
- client.connect({ wsToken: ws_token });
278
+ client.connect({ channelId: channel_id, websocketUrl: websocket_url });
270
279
  });
271
280
  </script>
272
281
  </body>
@@ -294,10 +303,10 @@ interface ModelRiverClientOptions {
294
303
 
295
304
  | Method | Description |
296
305
  |--------|-------------|
297
- | `connect({ wsToken })` | Connect to WebSocket with token |
306
+ | `connect({ channelId, websocketUrl?, websocketChannel? })` | Connect to WebSocket with channel ID |
298
307
  | `disconnect()` | Disconnect from WebSocket |
299
308
  | `reset()` | Reset state and clear stored data |
300
- | `reconnect()` | Reconnect using stored token |
309
+ | `reconnect()` | Reconnect using stored channel ID |
301
310
  | `getState()` | Get current client state |
302
311
  | `hasPendingRequest()` | Check if there's a pending request |
303
312
  | `on(event, callback)` | Add event listener (returns unsubscribe function) |
@@ -320,10 +329,27 @@ interface ModelRiverClientOptions {
320
329
  ### Types
321
330
 
322
331
  ```typescript
332
+ // Response from /api/ai/async endpoint
333
+ interface AsyncResponse {
334
+ message: string; // "success"
335
+ status: 'pending'; // Always "pending" for async
336
+ channel_id: string; // Unique channel ID
337
+ websocket_url: string; // WebSocket URL to connect to
338
+ websocket_channel: string; // Full channel name (e.g., "ai_response:uuid")
339
+ instructions?: {
340
+ websocket?: string;
341
+ webhook?: string;
342
+ };
343
+ test_mode?: boolean; // Present in test mode
344
+ }
345
+
346
+ // AI response received via WebSocket
323
347
  interface AIResponse {
324
- status: string;
348
+ status: string; // "success" or "error"
325
349
  channel_id?: string;
326
- data?: unknown;
350
+ content?: string; // AI response text
351
+ model?: string; // Model used (e.g., "gpt-4")
352
+ data?: unknown; // Structured output data
327
353
  meta?: {
328
354
  workflow?: string;
329
355
  status?: string;
@@ -351,11 +377,12 @@ interface WorkflowStep {
351
377
 
352
378
  ## How It Works
353
379
 
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
380
+ 1. **Your backend** calls ModelRiver's `/api/ai/async` endpoint
381
+ 2. **ModelRiver** returns `channel_id`, `websocket_url`, and `websocket_channel`
382
+ 3. **Your backend** returns these fields to the frontend
383
+ 4. **Your frontend** uses this SDK to connect via WebSocket using `channel_id`
384
+ 5. **AI responses** are delivered in real-time to your frontend
385
+ 6. **The SDK** handles reconnection, heartbeats, and error recovery
359
386
 
360
387
  ```
361
388
  ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
@@ -367,9 +394,9 @@ interface WorkflowStep {
367
394
  │ │ 2. Create request │
368
395
  │ │─────────────────────>│
369
396
  │ │ │
370
- │ │ 3. Return ws_token
397
+ │ │ 3. Return channel_id
371
398
  │ │<─────────────────────│
372
- │ 4. Return token │ │
399
+ │ 4. Return channel_id│ │
373
400
  │<─────────────────────│ │
374
401
  │ │ │
375
402
  │ 5. Connect WebSocket (SDK) │
@@ -382,13 +409,14 @@ interface WorkflowStep {
382
409
 
383
410
  ## Security
384
411
 
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
412
+ The `/api/ai/async` response contains:
413
+ - `channel_id` - Unique identifier for this request
414
+ - `websocket_url` - WebSocket endpoint URL
415
+ - `websocket_channel` - Channel name to join
416
+
417
+ The client SDK uses `channel_id` directly to connect to the WebSocket. The `channel_id` is unique per request and is used to join the appropriate channel for receiving responses.
390
418
 
391
- **Important**: Always obtain tokens from your backend. Never expose your ModelRiver API key in frontend code.
419
+ **Important**: Always obtain `channel_id` from your backend. Never expose your ModelRiver API key in frontend code.
392
420
 
393
421
  ## Browser Support
394
422
 
@@ -1,2 +1,2 @@
1
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).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});
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,r=0,o=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",C="longpoll",T="websocket",v=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 r=this.bindings.filter(t=>t.event===e);for(let e=0;e<r.length;e++){r[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,r,o,h){if(n.XDomainRequest){let s=new n.XDomainRequest;return this.xdomainRequest(s,e,t,i,r,o,h)}if(n.XMLHttpRequest){let a=new n.XMLHttpRequest;return this.xhrRequest(a,e,t,s,i,r,o,h)}if(n.fetch&&n.AbortController)return this.fetchRequest(e,t,s,i,r,o,h);throw new Error("No suitable XMLHttpRequest implementation found")}static fetchRequest(e,t,s,i,r,o,h){let a={method:e,headers:s,body:i},c=null;return r&&(c=new AbortController,setTimeout(()=>c.abort(),r),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&&o?o():h&&h(null)}),c}static xdomainRequest(e,t,s,i,n,r,o){return e.timeout=n,e.open(t,s),e.onload=()=>{let t=this.parseJSON(e.responseText);o&&o(t)},r&&(e.ontimeout=r),e.onprogress=()=>{},e.send(i),e}static xhrRequest(e,t,s,i,n,r,o,h){e.open(t,s,!0),e.timeout=r;for(let[t,s]of Object.entries(i))e.setRequestHeader(t,s);return e.onerror=()=>h&&h(null),e.onreadystatechange=()=>{if(e.readyState===v&&h){let t=this.parseJSON(e.responseText);h(t)}},o&&(e.ontimeout=o),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,r=e[i];"object"==typeof r?s.push(this.serialize(r,n)):s.push(encodeURIComponent(n)+"="+encodeURIComponent(r))}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=r,setTimeout(()=>this.poll(),0)}normalizeEndpoint(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+T),"$1/"+C)}endpointURL(){return S.appendParams(this.pollEndpoint,{token:this.token})}closeAndRetry(e,t,s){this.close(e,t,s),this.readyState=r}ontimeout(){this.onerror("timeout"),this.closeAndRetry(1005,"timeout",!1)}isActive(){return this.readyState===o||this.readyState===r}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=o,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 r;r=S.request(e,this.endpointURL(),t,s,this.timeout,()=>{this.reqs.delete(r),i()},e=>{this.reqs.delete(r),this.isActive()&&n(e)}),this.reqs.add(r)}},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,r,o]=JSON.parse(e);return t({join_ref:s,ref:i,topic:n,event:r,payload:o})}},binaryEncode(e){let{join_ref:t,ref:s,event:i,topic:n,payload:r}=e,o=this.META_LENGTH+t.length+s.length+n.length+i.length,h=new ArrayBuffer(this.HEADER_LENGTH+o),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+r.byteLength);return l.set(new Uint8Array(h),0),l.set(new Uint8Array(r),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),r=t.getUint8(3),o=this.HEADER_LENGTH+this.META_LENGTH-1,h=s.decode(e.slice(o,o+i));o+=i;let a=s.decode(e.slice(o,o+n));o+=n;let c=s.decode(e.slice(o,o+r));return o+=r,{join_ref:h,ref:null,topic:a,event:c,payload:e.slice(o,e.byteLength)}},decodeReply(e,t,s){let i=t.getUint8(1),n=t.getUint8(2),r=t.getUint8(3),o=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+r));h+=r;let u=s.decode(e.slice(h,h+o));h+=o;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),r=this.HEADER_LENGTH+2,o=s.decode(e.slice(r,r+i));r+=i;let h=s.decode(e.slice(r,r+n));return r+=n,{join_ref:null,ref:null,topic:o,event:h,payload:e.slice(r,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 r=null;i&&i.addEventListener&&(i.addEventListener("pagehide",e=>{this.conn&&(this.disconnect(),r=this.connectClock)}),i.addEventListener("pageshow",e=>{r===this.connectClock&&(r=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}/${T}`,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,r=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 r("memorized");this.fallbackTimer=setTimeout(r,t),s=this.onError(e=>{this.log("transport","error",e),n&&!i&&(clearTimeout(this.fallbackTimer),r(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(r,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 r:return"connecting";case o: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:r}=e;this.log("push",`${t} ${s} (${r}, ${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:r}=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 o=this.channels[e];o.isMember(t,s,i,r)&&o.trigger(s,i,n,r)}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 $="wss://api.modelriver.com/socket",H=3e5,I="active_request";function L(){try{const e="__modelriver_test__";return localStorage.setItem(e,"test"),localStorage.removeItem(e),!0}catch{return!1}}function P(e){if(!L())return null;try{const t=localStorage.getItem(`${e}${I}`);if(!t)return null;const s=JSON.parse(t);return Date.now()-s.timestamp>H?(U(e),null):s}catch{return null}}function U(e){if(L())try{localStorage.removeItem(`${e}${I}`)}catch{}}e.DEFAULT_BASE_URL=$,e.DEFAULT_HEARTBEAT_INTERVAL=3e4,e.DEFAULT_REQUEST_TIMEOUT=H,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.currentChannelId=null,this.currentWebsocketChannel=null,this.isConnecting=!1,this.listeners=new Map,this.options={baseUrl:e.baseUrl??$,debug:e.debug??!1,persist:e.persist??!0,storageKeyPrefix:e.storageKeyPrefix??"modelriver_",heartbeatInterval:e.heartbeatInterval??3e4,requestTimeout:e.requestTimeout??H},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!==P(this.options.storageKeyPrefix)}connect(e){if(this.isConnecting)return void this.logger.warn("Connection already in progress, skipping...");const{channelId:t,websocketUrl:s,websocketChannel:i}=e;if(!t){const e="channelId is required";return this.setError(e),void this.emit("error",e)}this.isConnecting=!0,this.currentChannelId=t,this.currentWebsocketChannel=i||`ai_response:${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(!L())return;const n={channelId:t,timestamp:Date.now(),websocketUrl:s,websocketChannel:i};try{localStorage.setItem(`${e}${I}`,JSON.stringify(n))}catch{}}(this.options.storageKeyPrefix,t,s,i),this.updateStepAndEmit("queue",{status:"pending"});const n=s||this.options.baseUrl,r=n.endsWith("/socket")?n:`${n}/socket`;this.logger.log("Connecting to:",r),this.socket=new _(r,{params:{}}),this.socket.onOpen(()=>{this.logger.log("Socket connected"),this.connectionState="connected",this.isConnecting=!1,this.emit("connected"),this.joinChannel(this.currentWebsocketChannel)}),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:"pending"}),this.updateStepAndEmit("receive",{status:"pending"}),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&&U(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&&U(this.options.storageKeyPrefix)}disconnect(){this.logger.log("Disconnecting..."),this.isConnecting=!1,this.cleanupConnection(),this.options.persist&&U(this.options.storageKeyPrefix),this.emit("disconnected","Manual disconnect")}reset(){this.logger.log("Resetting..."),this.disconnect(),this.steps=[],this.response=null,this.error=null,this.currentChannelId=null,this.currentWebsocketChannel=null}reconnect(){if(!this.options.persist)return this.logger.warn("Persistence is disabled, cannot reconnect"),!1;const e=P(this.options.storageKeyPrefix);return e?(this.logger.log("Reconnecting with stored channel ID..."),this.connect({channelId:e.channelId,websocketUrl:e.websocketUrl,websocketChannel:e.websocketChannel}),!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=function(e,t){return`${e.endsWith("/websocket")?e:`${e}/websocket`}?token=${encodeURIComponent(t)}&vsn=2.0.0`},e.decodeToken=function(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")}},e.isStorageAvailable=L,e.isTokenExpired=function(e){return!!e.exp&&Date.now()>=1e3*e.exp}});
2
2
  //# sourceMappingURL=modelriver.umd.js.map