@modelriver/client 1.1.2 → 1.1.35
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 +105 -28
- package/cdn/v1.1.35/modelriver.min.js +2 -0
- package/cdn/{v1.1.2 → v1.1.35}/modelriver.min.js.map +1 -1
- package/dist/angular.cjs +87 -8
- package/dist/angular.cjs.map +1 -1
- package/dist/angular.mjs +87 -8
- package/dist/angular.mjs.map +1 -1
- package/dist/client.d.ts +17 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +87 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +87 -8
- package/dist/index.mjs.map +1 -1
- package/dist/modelriver.umd.js +1 -1
- package/dist/modelriver.umd.js.map +1 -1
- package/dist/react.cjs +87 -8
- package/dist/react.cjs.map +1 -1
- package/dist/react.mjs +87 -8
- package/dist/react.mjs.map +1 -1
- package/dist/svelte.cjs +87 -8
- package/dist/svelte.cjs.map +1 -1
- package/dist/svelte.mjs +87 -8
- package/dist/svelte.mjs.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/vue.cjs +87 -8
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.mjs +87 -8
- package/dist/vue.mjs.map +1 -1
- package/package.json +1 -1
- package/cdn/v1.1.2/modelriver.min.js +0 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Official ModelRiver client SDK for real-time AI response streaming via WebSocket
|
|
|
6
6
|
|
|
7
7
|
- **WebSocket streaming** - Receive AI responses in real-time via Phoenix Channels
|
|
8
8
|
- **Auto-reconnection** - Automatically reconnects on connection loss
|
|
9
|
-
- **Persistence** - Survives page refreshes with localStorage
|
|
9
|
+
- **Persistence + reconnect** - Survives page refreshes with localStorage + backend reconnect
|
|
10
10
|
- **Framework adapters** - First-class support for React, Vue, Angular, and Svelte
|
|
11
11
|
- **CDN ready** - Use via script tag without a build step
|
|
12
12
|
- **TypeScript** - Full type definitions included
|
|
@@ -27,16 +27,16 @@ pnpm add @modelriver/client
|
|
|
27
27
|
### CDN
|
|
28
28
|
|
|
29
29
|
```html
|
|
30
|
-
<script src="https://cdn.modelriver.com/client/v1.
|
|
30
|
+
<script src="https://cdn.modelriver.com/client/v1.1.35/modelriver.min.js"></script>
|
|
31
31
|
<!-- or latest -->
|
|
32
32
|
<script src="https://cdn.modelriver.com/client/latest/modelriver.min.js"></script>
|
|
33
33
|
```
|
|
34
34
|
|
|
35
35
|
## Quick Start
|
|
36
36
|
|
|
37
|
-
### 1. Get
|
|
37
|
+
### 1. Get async connection details from your backend
|
|
38
38
|
|
|
39
|
-
Your backend calls the ModelRiver `/api/ai/async` endpoint and receives connection details:
|
|
39
|
+
Your backend calls the ModelRiver `/api/v1/ai/async` endpoint and receives connection details:
|
|
40
40
|
|
|
41
41
|
```javascript
|
|
42
42
|
// Your backend endpoint proxies to ModelRiver
|
|
@@ -45,18 +45,19 @@ const response = await fetch('/api/ai/request', {
|
|
|
45
45
|
body: JSON.stringify({ message: 'Hello AI' }),
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
// Response from /api/ai/async:
|
|
48
|
+
// Response from /api/v1/ai/async:
|
|
49
49
|
// {
|
|
50
50
|
// "message": "success",
|
|
51
51
|
// "status": "pending",
|
|
52
52
|
// "channel_id": "a1b2c3d4-...",
|
|
53
|
+
// "ws_token": "one-time-websocket-token",
|
|
53
54
|
// "websocket_url": "wss://api.modelriver.com/socket",
|
|
54
|
-
// "websocket_channel": "ai_response:a1b2c3d4-..."
|
|
55
|
+
// "websocket_channel": "ai_response:PROJECT_ID:a1b2c3d4-..."
|
|
55
56
|
// }
|
|
56
|
-
const { channel_id, websocket_url, websocket_channel } = await response.json();
|
|
57
|
+
const { channel_id, ws_token, websocket_url, websocket_channel } = await response.json();
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
### 2. Connect to ModelRiver
|
|
60
|
+
### 2. Connect to ModelRiver WebSocket
|
|
60
61
|
|
|
61
62
|
```javascript
|
|
62
63
|
import { ModelRiverClient } from '@modelriver/client';
|
|
@@ -73,7 +74,12 @@ client.on('error', (error) => {
|
|
|
73
74
|
console.error('Error:', error);
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
client.connect({
|
|
77
|
+
client.connect({
|
|
78
|
+
channelId: channel_id,
|
|
79
|
+
wsToken: ws_token,
|
|
80
|
+
websocketUrl: websocket_url,
|
|
81
|
+
websocketChannel: websocket_channel,
|
|
82
|
+
});
|
|
77
83
|
```
|
|
78
84
|
|
|
79
85
|
## Framework Usage
|
|
@@ -97,8 +103,19 @@ function ChatComponent() {
|
|
|
97
103
|
});
|
|
98
104
|
|
|
99
105
|
const handleSend = async () => {
|
|
100
|
-
const {
|
|
101
|
-
|
|
106
|
+
const {
|
|
107
|
+
channel_id,
|
|
108
|
+
ws_token,
|
|
109
|
+
websocket_url,
|
|
110
|
+
websocket_channel,
|
|
111
|
+
} = await yourBackendAPI.createRequest(message); // calls /api/v1/ai/async
|
|
112
|
+
|
|
113
|
+
connect({
|
|
114
|
+
channelId: channel_id,
|
|
115
|
+
wsToken: ws_token,
|
|
116
|
+
websocketUrl: websocket_url,
|
|
117
|
+
websocketChannel: websocket_channel,
|
|
118
|
+
});
|
|
102
119
|
};
|
|
103
120
|
|
|
104
121
|
return (
|
|
@@ -144,8 +161,19 @@ const {
|
|
|
144
161
|
});
|
|
145
162
|
|
|
146
163
|
async function handleSend() {
|
|
147
|
-
const {
|
|
148
|
-
|
|
164
|
+
const {
|
|
165
|
+
channel_id,
|
|
166
|
+
ws_token,
|
|
167
|
+
websocket_url,
|
|
168
|
+
websocket_channel,
|
|
169
|
+
} = await yourBackendAPI.createRequest(message); // calls /api/v1/ai/async
|
|
170
|
+
|
|
171
|
+
connect({
|
|
172
|
+
channelId: channel_id,
|
|
173
|
+
wsToken: ws_token,
|
|
174
|
+
websocketUrl: websocket_url,
|
|
175
|
+
websocketChannel: websocket_channel,
|
|
176
|
+
});
|
|
149
177
|
}
|
|
150
178
|
</script>
|
|
151
179
|
|
|
@@ -198,8 +226,19 @@ export class ChatComponent implements OnDestroy {
|
|
|
198
226
|
}
|
|
199
227
|
|
|
200
228
|
async send() {
|
|
201
|
-
const {
|
|
202
|
-
|
|
229
|
+
const {
|
|
230
|
+
channel_id,
|
|
231
|
+
ws_token,
|
|
232
|
+
websocket_url,
|
|
233
|
+
websocket_channel,
|
|
234
|
+
} = await this.backendService.createRequest(message); // calls /api/v1/ai/async
|
|
235
|
+
|
|
236
|
+
this.modelRiver.connect({
|
|
237
|
+
channelId: channel_id,
|
|
238
|
+
wsToken: ws_token,
|
|
239
|
+
websocketUrl: websocket_url,
|
|
240
|
+
websocketChannel: websocket_channel,
|
|
241
|
+
});
|
|
203
242
|
}
|
|
204
243
|
|
|
205
244
|
ngOnDestroy() {
|
|
@@ -222,8 +261,19 @@ export class ChatComponent implements OnDestroy {
|
|
|
222
261
|
const { response, error, isConnected, steps, connect, disconnect } = modelRiver;
|
|
223
262
|
|
|
224
263
|
async function send() {
|
|
225
|
-
const {
|
|
226
|
-
|
|
264
|
+
const {
|
|
265
|
+
channel_id,
|
|
266
|
+
ws_token,
|
|
267
|
+
websocket_url,
|
|
268
|
+
websocket_channel,
|
|
269
|
+
} = await backendAPI.createRequest(message); // calls /api/v1/ai/async
|
|
270
|
+
|
|
271
|
+
connect({
|
|
272
|
+
channelId: channel_id,
|
|
273
|
+
wsToken: ws_token,
|
|
274
|
+
websocketUrl: websocket_url,
|
|
275
|
+
websocketChannel: websocket_channel,
|
|
276
|
+
});
|
|
227
277
|
}
|
|
228
278
|
|
|
229
279
|
onDestroy(() => disconnect());
|
|
@@ -271,11 +321,21 @@ export class ChatComponent implements OnDestroy {
|
|
|
271
321
|
});
|
|
272
322
|
|
|
273
323
|
document.getElementById('send').addEventListener('click', async () => {
|
|
274
|
-
// Get
|
|
324
|
+
// Get async connection info from your backend
|
|
275
325
|
const res = await fetch('/api/ai/request', { method: 'POST' });
|
|
276
|
-
const {
|
|
326
|
+
const {
|
|
327
|
+
channel_id,
|
|
328
|
+
ws_token,
|
|
329
|
+
websocket_url,
|
|
330
|
+
websocket_channel,
|
|
331
|
+
} = await res.json(); // your backend calls /api/v1/ai/async
|
|
277
332
|
|
|
278
|
-
client.connect({
|
|
333
|
+
client.connect({
|
|
334
|
+
channelId: channel_id,
|
|
335
|
+
wsToken: ws_token,
|
|
336
|
+
websocketUrl: websocket_url,
|
|
337
|
+
websocketChannel: websocket_channel,
|
|
338
|
+
});
|
|
279
339
|
});
|
|
280
340
|
</script>
|
|
281
341
|
</body>
|
|
@@ -291,6 +351,7 @@ export class ChatComponent implements OnDestroy {
|
|
|
291
351
|
```typescript
|
|
292
352
|
interface ModelRiverClientOptions {
|
|
293
353
|
baseUrl?: string; // WebSocket URL (default: 'wss://api.modelriver.com/socket')
|
|
354
|
+
apiBaseUrl?: string; // Optional HTTP base URL for backend reconnect (/api/v1/ai/reconnect)
|
|
294
355
|
debug?: boolean; // Enable debug logging (default: false)
|
|
295
356
|
persist?: boolean; // Enable localStorage persistence (default: true)
|
|
296
357
|
storageKeyPrefix?: string; // Storage key prefix (default: 'modelriver_')
|
|
@@ -307,6 +368,7 @@ interface ModelRiverClientOptions {
|
|
|
307
368
|
| `disconnect()` | Disconnect from WebSocket |
|
|
308
369
|
| `reset()` | Reset state and clear stored data |
|
|
309
370
|
| `reconnect()` | Reconnect using stored channel ID |
|
|
371
|
+
| `reconnectWithBackend()` | Call your backend `/api/v1/ai/reconnect` to get a fresh `ws_token` and reconnect |
|
|
310
372
|
| `getState()` | Get current client state |
|
|
311
373
|
| `hasPendingRequest()` | Check if there's a pending request |
|
|
312
374
|
| `on(event, callback)` | Add event listener (returns unsubscribe function) |
|
|
@@ -334,6 +396,7 @@ interface AsyncResponse {
|
|
|
334
396
|
message: string; // "success"
|
|
335
397
|
status: 'pending'; // Always "pending" for async
|
|
336
398
|
channel_id: string; // Unique channel ID
|
|
399
|
+
ws_token: string; // One-time WebSocket token for authentication
|
|
337
400
|
websocket_url: string; // WebSocket URL to connect to
|
|
338
401
|
websocket_channel: string; // Full channel name (e.g., "ai_response:uuid")
|
|
339
402
|
instructions?: {
|
|
@@ -377,12 +440,13 @@ interface WorkflowStep {
|
|
|
377
440
|
|
|
378
441
|
## How It Works
|
|
379
442
|
|
|
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`
|
|
443
|
+
1. **Your backend** calls ModelRiver's `/api/v1/ai/async` endpoint
|
|
444
|
+
2. **ModelRiver** returns `channel_id`, `ws_token`, `websocket_url`, and `websocket_channel`
|
|
445
|
+
3. **Your backend** returns these fields to the frontend (never the API key)
|
|
446
|
+
4. **Your frontend** uses this SDK to connect via WebSocket using `channel_id` + `ws_token`
|
|
384
447
|
5. **AI responses** are delivered in real-time to your frontend
|
|
385
|
-
6. **The SDK** handles
|
|
448
|
+
6. **The SDK** handles heartbeats, channel joins, and automatic reconnection for transient network issues.
|
|
449
|
+
7. For **page refresh recovery**, use the persistence + reconnect helpers (`persist`, `hasPendingRequest`, `reconnect`, `reconnectWithBackend`) together with your backend `/api/v1/ai/reconnect` endpoint.
|
|
386
450
|
|
|
387
451
|
```
|
|
388
452
|
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
@@ -409,14 +473,27 @@ interface WorkflowStep {
|
|
|
409
473
|
|
|
410
474
|
## Security
|
|
411
475
|
|
|
412
|
-
The `/api/ai/async` response contains:
|
|
476
|
+
The `/api/v1/ai/async` response contains:
|
|
413
477
|
- `channel_id` - Unique identifier for this request
|
|
478
|
+
- `ws_token` - Short-lived, one-time WebSocket token (per user + project)
|
|
414
479
|
- `websocket_url` - WebSocket endpoint URL
|
|
415
480
|
- `websocket_channel` - Channel name to join
|
|
416
481
|
|
|
417
|
-
The client SDK uses `channel_id`
|
|
482
|
+
The client SDK uses `channel_id` and `ws_token` to connect to the WebSocket.
|
|
483
|
+
The `ws_token` is:
|
|
484
|
+
|
|
485
|
+
- Short-lived (≈5 minutes)
|
|
486
|
+
- Single-use (consumed on first successful WebSocket authentication)
|
|
487
|
+
|
|
488
|
+
For page refresh recovery:
|
|
489
|
+
|
|
490
|
+
- The SDK persists the active request (by default) to `localStorage`
|
|
491
|
+
- On reload, you can:
|
|
492
|
+
- either call `client.reconnect()` to reuse the stored `ws_token` (if still valid)
|
|
493
|
+
- or call `client.reconnectWithBackend()` to have your backend issue a **fresh** `ws_token` via `/api/v1/ai/reconnect`
|
|
418
494
|
|
|
419
|
-
**Important**: Always obtain `channel_id` from your backend.
|
|
495
|
+
**Important**: Always obtain `channel_id` and `ws_token` from your backend.
|
|
496
|
+
Never expose your ModelRiver API key in frontend code. Your backend should be the only component that talks to ModelRiver's HTTP API (`/api/v1/ai/async`, `/api/v1/ai/reconnect`, etc.).
|
|
420
497
|
|
|
421
498
|
## Browser Support
|
|
422
499
|
|
|
@@ -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,n="undefined"!=typeof window?window:null,i=s||n||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",C="websocket",v=4,w="base64url.bearer.phx.",y=class{constructor(e,t,s,n){this.channel=e,this.event=t,this.payload=s||function(){return{}},this.receivedResp=null,this.timeout=n,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})}},E=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,n){this.state=c,this.topic=e,this.params=t(s||{}),this.socket=n,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 E(()=>{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 n=new y(this,e,function(){return t},s);return this.canPush()?n.send():(n.startTimeout(),this.pushBuffer.push(n)),n}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")},n=new y(this,k,t({}),e);return n.receive("ok",()=>s()).receive("timeout",()=>s()),n.send(),this.canPush()||n.trigger("ok",{}),n}onMessage(e,t,s){return t}isMember(e,t,s,n){return this.topic===e&&(!n||n===this.joinRef()||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:s,joinRef:n}),!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,n){let i=this.onMessage(e,t,s,n);if(t&&!i)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(i,s,n||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,n,o,r,h){if(i.XDomainRequest){let s=new i.XDomainRequest;return this.xdomainRequest(s,e,t,n,o,r,h)}if(i.XMLHttpRequest){let a=new i.XMLHttpRequest;return this.xhrRequest(a,e,t,s,n,o,r,h)}if(i.fetch&&i.AbortController)return this.fetchRequest(e,t,s,n,o,r,h);throw new Error("No suitable XMLHttpRequest implementation found")}static fetchRequest(e,t,s,n,o,r,h){let a={method:e,headers:s,body:n},c=null;return o&&(c=new AbortController,setTimeout(()=>c.abort(),o),a.signal=c.signal),i.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,n,i,o,r){return e.timeout=i,e.open(t,s),e.onload=()=>{let t=this.parseJSON(e.responseText);r&&r(t)},o&&(e.ontimeout=o),e.onprogress=()=>{},e.send(n),e}static xhrRequest(e,t,s,n,i,o,r,h){e.open(t,s,!0),e.timeout=o;for(let[t,s]of Object.entries(n))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)}},r&&(e.ontimeout=r),e.send(i),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 n in e){if(!Object.prototype.hasOwnProperty.call(e,n))continue;let i=t?`${t}[${n}]`:n,o=e[n];"object"==typeof o?s.push(this.serialize(o,i)):s.push(encodeURIComponent(i)+"="+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(w)&&(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("(.*)/"+C),"$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:n}=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:n.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),n=s.byteLength;for(let e=0;e<n;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 n=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",n)):this.onclose(n)}ajax(e,t,s,n,i){let o;o=S.request(e,this.endpointURL(),t,s,this.timeout,()=>{this.reqs.delete(o),n()},e=>{this.reqs.delete(o),this.isActive()&&i(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,n,i,o,r]=JSON.parse(e);return t({join_ref:s,ref:n,topic:i,event:o,payload:r})}},binaryEncode(e){let{join_ref:t,ref:s,event:n,topic:i,payload:o}=e,r=this.META_LENGTH+t.length+s.length+i.length+n.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++,i.length),a.setUint8(c++,n.length),Array.from(t,e=>a.setUint8(c++,e.charCodeAt(0))),Array.from(s,e=>a.setUint8(c++,e.charCodeAt(0))),Array.from(i,e=>a.setUint8(c++,e.charCodeAt(0))),Array.from(n,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),n=new TextDecoder;switch(s){case this.KINDS.push:return this.decodePush(e,t,n);case this.KINDS.reply:return this.decodeReply(e,t,n);case this.KINDS.broadcast:return this.decodeBroadcast(e,t,n)}},decodePush(e,t,s){let n=t.getUint8(1),i=t.getUint8(2),o=t.getUint8(3),r=this.HEADER_LENGTH+this.META_LENGTH-1,h=s.decode(e.slice(r,r+n));r+=n;let a=s.decode(e.slice(r,r+i));r+=i;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 n=t.getUint8(1),i=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+n));h+=n;let c=s.decode(e.slice(h,h+i));h+=i;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 n=t.getUint8(1),i=t.getUint8(2),o=this.HEADER_LENGTH+2,r=s.decode(e.slice(o,o+n));o+=n;let h=s.decode(e.slice(o,o+i));return o+=i,{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||i.WebSocket||j,this.primaryPassedHealthCheck=!1,this.longPollFallbackMs=s.longPollFallbackMs,this.fallbackTimer=null,this.sessionStore=s.sessionStorage||i&&i.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;n&&n.addEventListener&&(n.addEventListener("pagehide",e=>{this.conn&&(this.disconnect(),o=this.connectClock)}),n.addEventListener("pageshow",e=>{o===this.connectClock&&(o=null,this.connect())}),n.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}/${C}`,this.vsn=s.vsn||"2.0.0",this.heartbeatTimeoutTimer=null,this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new E(()=>{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 n=this.onMessage(i=>{i.ref===t&&(this.off([n]),e(Date.now()-s))});return!0}transportConnect(){let e;this.connectClock++,this.closeWasClean=!1,this.authToken&&(e=["phoenix",`${w}${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,n=!1,i=!0,o=t=>{this.log("transport",`falling back to ${e.name}...`,t),this.off([undefined,s]),i=!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),i&&!n&&(clearTimeout(this.fallbackTimer),o(e))}),this.fallbackRef&&this.off([this.fallbackRef]),this.fallbackRef=this.onOpen(()=>{if(n=!0,!i)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 n=this.connectClock;this.waitForBufferDone(()=>{n===this.connectClock&&(this.conn&&(t?this.conn.close(t,s||""):this.conn.close()),this.waitForSocketClosed(()=>{n===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(([,n])=>{n(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:n,ref:i,join_ref:o}=e;this.log("push",`${t} ${s} (${o}, ${i})`,n)}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:n,ref:i,join_ref:o}=e;i&&i===this.pendingHeartbeatRef&&(this.clearHeartbeats(),this.pendingHeartbeatRef=null,this.heartbeatTimer=setTimeout(()=>this.sendHeartbeat(),this.heartbeatIntervalMs)),this.hasLogger()&&this.log("receive",`${n.status||""} ${t} ${s} ${i&&"("+i+")"||""}`,n);for(let e=0;e<this.channels.length;e++){const r=this.channels[e];r.isMember(t,s,n,o)&&r.trigger(s,n,i,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 $="wss://api.modelriver.com/socket",H=3e5,P="active_request";function U(){try{const e="__modelriver_test__";return localStorage.setItem(e,"test"),localStorage.removeItem(e),!0}catch{return!1}}function I(e,t,s,n,i){if(!U())return;const o={channelId:t,wsToken:s,timestamp:Date.now(),websocketUrl:n,websocketChannel:i};try{localStorage.setItem(`${e}${P}`,JSON.stringify(o))}catch{}}function B(e){if(!U())return null;try{const t=localStorage.getItem(`${e}${P}`);if(!t)return null;const s=JSON.parse(t);return Date.now()-s.timestamp>H?(L(e),null):s}catch{return null}}function L(e){if(U())try{localStorage.removeItem(`${e}${P}`)}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.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,apiBaseUrl:e.apiBaseUrl??""},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!==B(this.options.storageKeyPrefix)}connect(e){if(this.isConnecting)return void this.logger.warn("Connection already in progress, skipping...");const{channelId:t,wsToken:s,websocketUrl:n,websocketChannel:i}=e;if(!t){const e="channelId is required";return this.setError(e),void this.emit("error",e)}if(!s){const e="wsToken is required for WebSocket authentication";return this.setError(e),void this.emit("error",e)}this.isConnecting=!0,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&&I(this.options.storageKeyPrefix,t,s,n,i),this.updateStepAndEmit("queue",{status:"pending"});const o=n||this.options.baseUrl,r=o.endsWith("/socket")?o:`${o}/socket`;this.logger.log("Connecting to:",r),this.socket=new _(r,{params:{token:s}}),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&&L(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&&L(this.options.storageKeyPrefix)}disconnect(){this.logger.log("Disconnecting..."),this.isConnecting=!1,this.cleanupConnection(),this.options.persist&&L(this.options.storageKeyPrefix),this.emit("disconnected","Manual disconnect")}reset(){this.logger.log("Resetting..."),this.disconnect(),this.steps=[],this.response=null,this.error=null,this.currentWebsocketChannel=null}reconnect(){if(!this.options.persist)return this.logger.warn("Persistence is disabled, cannot reconnect"),!1;const e=B(this.options.storageKeyPrefix);return e?e.wsToken?(this.logger.log("Reconnecting with stored channel ID..."),this.connect({channelId:e.channelId,wsToken:e.wsToken,websocketUrl:e.websocketUrl,websocketChannel:e.websocketChannel}),!0):(this.logger.warn("No wsToken found in stored request, cannot reconnect"),L(this.options.storageKeyPrefix),!1):(this.logger.log("No active request found for reconnection"),!1)}async reconnectWithBackend(){if(!this.options.persist)return this.logger.warn("Persistence is disabled, cannot reconnect with backend"),!1;if(!this.options.apiBaseUrl)return this.logger.warn("apiBaseUrl is not configured, cannot call /api/v1/ai/reconnect"),!1;const e=B(this.options.storageKeyPrefix);if(!e)return this.logger.log("No active request found for backend reconnection"),!1;const t=`${this.options.apiBaseUrl.replace(/\/+$/,"")}/api/v1/ai/reconnect`;try{const s=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({channel_id:e.channelId})});if(!s.ok)return this.logger.error("Backend reconnect failed with HTTP status",s.status),!1;const n=await s.json();return n?.channel_id&&n?.ws_token?(I(this.options.storageKeyPrefix,n.channel_id,n.ws_token,n.websocket_url,n.websocket_channel),this.connect({channelId:n.channel_id,wsToken:n.ws_token,websocketUrl:n.websocket_url,websocketChannel:n.websocket_channel}),!0):(this.logger.error("Backend reconnect response missing channel_id or ws_token",n),!1)}catch(e){return this.logger.error("Backend reconnect request failed",e),!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=U,e.isTokenExpired=function(e){return!!e.exp&&Date.now()>=1e3*e.exp}});
|
|
2
|
+
//# sourceMappingURL=modelriver.umd.js.map
|