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