@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/dist/vue.mjs ADDED
@@ -0,0 +1,746 @@
1
+ import { ref, onMounted, onUnmounted } from 'vue';
2
+ import { Socket } from 'phoenix';
3
+
4
+ /**
5
+ * ModelRiver Client SDK Utilities
6
+ *
7
+ * Helper functions for JWT decoding, localStorage operations, and URL building.
8
+ */
9
+ /**
10
+ * Default WebSocket base URL
11
+ */
12
+ const DEFAULT_BASE_URL = 'wss://api.modelriver.com/socket';
13
+ /**
14
+ * Default storage key prefix
15
+ */
16
+ const DEFAULT_STORAGE_KEY_PREFIX = 'modelriver_';
17
+ /**
18
+ * Default heartbeat interval (30 seconds)
19
+ */
20
+ const DEFAULT_HEARTBEAT_INTERVAL = 30000;
21
+ /**
22
+ * Default request timeout (5 minutes)
23
+ */
24
+ const DEFAULT_REQUEST_TIMEOUT = 300000;
25
+ /**
26
+ * Active request storage key suffix
27
+ */
28
+ const ACTIVE_REQUEST_KEY = 'active_request';
29
+ /**
30
+ * Decode a base64url string to a regular string
31
+ */
32
+ function base64UrlDecode(str) {
33
+ // Replace base64url characters with base64 characters
34
+ let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
35
+ // Pad with '=' to make length a multiple of 4
36
+ const padding = base64.length % 4;
37
+ if (padding) {
38
+ base64 += '='.repeat(4 - padding);
39
+ }
40
+ // Decode
41
+ try {
42
+ return atob(base64);
43
+ }
44
+ catch {
45
+ throw new Error('Invalid base64url string');
46
+ }
47
+ }
48
+ /**
49
+ * Decode a JWT token and extract the payload
50
+ * Note: This does NOT verify the signature - that's done server-side
51
+ */
52
+ function decodeToken(token) {
53
+ if (!token || typeof token !== 'string') {
54
+ throw new Error('Invalid token: token must be a non-empty string');
55
+ }
56
+ const parts = token.split('.');
57
+ if (parts.length !== 3) {
58
+ throw new Error('Invalid token: JWT must have 3 parts');
59
+ }
60
+ try {
61
+ const payload = JSON.parse(base64UrlDecode(parts[1]));
62
+ // Validate required fields
63
+ if (!payload.project_id || !payload.channel_id) {
64
+ throw new Error('Invalid token: missing required fields (project_id, channel_id)');
65
+ }
66
+ // Build topic if not present
67
+ const topic = payload.topic || `ai_response:${payload.project_id}:${payload.channel_id}`;
68
+ return {
69
+ project_id: payload.project_id,
70
+ channel_id: payload.channel_id,
71
+ topic,
72
+ exp: payload.exp,
73
+ };
74
+ }
75
+ catch (error) {
76
+ if (error instanceof Error && error.message.startsWith('Invalid token:')) {
77
+ throw error;
78
+ }
79
+ throw new Error('Invalid token: failed to decode payload');
80
+ }
81
+ }
82
+ /**
83
+ * Check if a token is expired
84
+ */
85
+ function isTokenExpired(payload) {
86
+ if (!payload.exp) {
87
+ return false; // No expiration set
88
+ }
89
+ // exp is in seconds, Date.now() is in milliseconds
90
+ return Date.now() >= payload.exp * 1000;
91
+ }
92
+ /**
93
+ * Build the WebSocket URL with token
94
+ */
95
+ function buildWebSocketUrl(baseUrl, token) {
96
+ const url = baseUrl.endsWith('/websocket') ? baseUrl : `${baseUrl}/websocket`;
97
+ return `${url}?token=${encodeURIComponent(token)}&vsn=2.0.0`;
98
+ }
99
+ /**
100
+ * Check if localStorage is available
101
+ */
102
+ function isStorageAvailable() {
103
+ try {
104
+ const testKey = '__modelriver_test__';
105
+ localStorage.setItem(testKey, 'test');
106
+ localStorage.removeItem(testKey);
107
+ return true;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ /**
114
+ * Save active request to localStorage
115
+ */
116
+ function saveActiveRequest(prefix, projectId, channelId, wsToken) {
117
+ if (!isStorageAvailable())
118
+ return;
119
+ const request = {
120
+ channelId,
121
+ timestamp: Date.now(),
122
+ projectId,
123
+ wsToken,
124
+ };
125
+ try {
126
+ localStorage.setItem(`${prefix}${ACTIVE_REQUEST_KEY}`, JSON.stringify(request));
127
+ }
128
+ catch {
129
+ // Storage might be full or disabled
130
+ }
131
+ }
132
+ /**
133
+ * Get active request from localStorage
134
+ * Returns null if not found or expired (older than 5 minutes)
135
+ */
136
+ function getActiveRequest(prefix) {
137
+ if (!isStorageAvailable())
138
+ return null;
139
+ try {
140
+ const stored = localStorage.getItem(`${prefix}${ACTIVE_REQUEST_KEY}`);
141
+ if (!stored)
142
+ return null;
143
+ const request = JSON.parse(stored);
144
+ // Check if request is less than 5 minutes old
145
+ const age = Date.now() - request.timestamp;
146
+ if (age > DEFAULT_REQUEST_TIMEOUT) {
147
+ clearActiveRequest(prefix);
148
+ return null;
149
+ }
150
+ return request;
151
+ }
152
+ catch {
153
+ return null;
154
+ }
155
+ }
156
+ /**
157
+ * Clear active request from localStorage
158
+ */
159
+ function clearActiveRequest(prefix) {
160
+ if (!isStorageAvailable())
161
+ return;
162
+ try {
163
+ localStorage.removeItem(`${prefix}${ACTIVE_REQUEST_KEY}`);
164
+ }
165
+ catch {
166
+ // Ignore errors
167
+ }
168
+ }
169
+ /**
170
+ * Create initial workflow steps
171
+ */
172
+ function createInitialSteps() {
173
+ return [
174
+ { id: 'queue', name: 'Queueing request', status: 'pending' },
175
+ { id: 'process', name: 'Processing AI request', status: 'pending' },
176
+ { id: 'receive', name: 'Waiting for response', status: 'pending' },
177
+ { id: 'complete', name: 'Response received', status: 'pending' },
178
+ ];
179
+ }
180
+ /**
181
+ * Update a step in the steps array
182
+ */
183
+ function updateStep(steps, id, updates) {
184
+ return steps.map((step) => step.id === id ? { ...step, ...updates } : step);
185
+ }
186
+ /**
187
+ * Logger utility for debug mode
188
+ */
189
+ function createLogger(debug) {
190
+ const prefix = '[ModelRiver]';
191
+ return {
192
+ log: (...args) => {
193
+ if (debug)
194
+ console.log(prefix, ...args);
195
+ },
196
+ warn: (...args) => {
197
+ if (debug)
198
+ console.warn(prefix, ...args);
199
+ },
200
+ error: (...args) => {
201
+ // Always log errors
202
+ console.error(prefix, ...args);
203
+ },
204
+ };
205
+ }
206
+
207
+ /**
208
+ * ModelRiver Client SDK
209
+ *
210
+ * Core client class for connecting to ModelRiver's WebSocket-based AI response streaming.
211
+ */
212
+ /**
213
+ * ModelRiver WebSocket Client
214
+ *
215
+ * Connects to ModelRiver's Phoenix Channels for real-time AI response streaming.
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const client = new ModelRiverClient({
220
+ * baseUrl: 'wss://api.modelriver.com/socket',
221
+ * debug: true,
222
+ * });
223
+ *
224
+ * client.on('response', (data) => {
225
+ * console.log('AI Response:', data);
226
+ * });
227
+ *
228
+ * client.connect({ wsToken: 'your-token-from-backend' });
229
+ * ```
230
+ */
231
+ class ModelRiverClient {
232
+ constructor(options = {}) {
233
+ this.socket = null;
234
+ this.channel = null;
235
+ this.heartbeatInterval = null;
236
+ this.connectionState = 'disconnected';
237
+ this.steps = [];
238
+ this.response = null;
239
+ this.error = null;
240
+ this.currentToken = null;
241
+ this.currentWsToken = null;
242
+ this.isConnecting = false;
243
+ // Event listeners
244
+ this.listeners = new Map();
245
+ this.options = {
246
+ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL,
247
+ debug: options.debug ?? false,
248
+ persist: options.persist ?? true,
249
+ storageKeyPrefix: options.storageKeyPrefix ?? DEFAULT_STORAGE_KEY_PREFIX,
250
+ heartbeatInterval: options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL,
251
+ requestTimeout: options.requestTimeout ?? DEFAULT_REQUEST_TIMEOUT,
252
+ };
253
+ this.logger = createLogger(this.options.debug);
254
+ this.logger.log('Client initialized with options:', this.options);
255
+ }
256
+ /**
257
+ * Get current client state
258
+ */
259
+ getState() {
260
+ return {
261
+ connectionState: this.connectionState,
262
+ isConnected: this.connectionState === 'connected',
263
+ isConnecting: this.isConnecting,
264
+ steps: [...this.steps],
265
+ response: this.response,
266
+ error: this.error,
267
+ hasPendingRequest: this.hasPendingRequest(),
268
+ };
269
+ }
270
+ /**
271
+ * Check if there's a pending request that can be reconnected
272
+ */
273
+ hasPendingRequest() {
274
+ if (!this.options.persist)
275
+ return false;
276
+ const request = getActiveRequest(this.options.storageKeyPrefix);
277
+ return request !== null;
278
+ }
279
+ /**
280
+ * Connect to WebSocket with token
281
+ */
282
+ connect(options) {
283
+ if (this.isConnecting) {
284
+ this.logger.warn('Connection already in progress, skipping...');
285
+ return;
286
+ }
287
+ const { wsToken } = options;
288
+ // Decode and validate token
289
+ let tokenPayload;
290
+ try {
291
+ tokenPayload = decodeToken(wsToken);
292
+ this.logger.log('Token decoded:', {
293
+ projectId: tokenPayload.project_id,
294
+ channelId: tokenPayload.channel_id,
295
+ topic: tokenPayload.topic,
296
+ });
297
+ }
298
+ catch (err) {
299
+ const errorMsg = err instanceof Error ? err.message : 'Invalid token';
300
+ this.setError(errorMsg);
301
+ this.emit('error', errorMsg);
302
+ return;
303
+ }
304
+ // Check token expiration
305
+ if (isTokenExpired(tokenPayload)) {
306
+ const errorMsg = 'Token has expired';
307
+ this.setError(errorMsg);
308
+ this.emit('error', errorMsg);
309
+ return;
310
+ }
311
+ this.isConnecting = true;
312
+ this.currentToken = tokenPayload;
313
+ this.currentWsToken = wsToken;
314
+ this.emit('connecting');
315
+ // Clean up any existing connection
316
+ this.cleanupConnection();
317
+ // Initialize steps
318
+ this.steps = createInitialSteps();
319
+ this.error = null;
320
+ this.response = null;
321
+ // Save to localStorage for persistence
322
+ if (this.options.persist) {
323
+ saveActiveRequest(this.options.storageKeyPrefix, tokenPayload.project_id, tokenPayload.channel_id, wsToken);
324
+ }
325
+ // Update queue step to loading
326
+ this.updateStepAndEmit('queue', { status: 'loading' });
327
+ // Create Phoenix Socket
328
+ const wsUrl = buildWebSocketUrl(this.options.baseUrl, wsToken);
329
+ this.logger.log('Connecting to:', wsUrl.replace(wsToken, '***TOKEN***'));
330
+ this.socket = new Socket(this.options.baseUrl, {
331
+ params: { token: wsToken },
332
+ });
333
+ this.socket.onOpen(() => {
334
+ this.logger.log('Socket connected');
335
+ this.connectionState = 'connected';
336
+ this.isConnecting = false;
337
+ this.emit('connected');
338
+ // Join channel
339
+ this.joinChannel(tokenPayload.topic);
340
+ });
341
+ this.socket.onError((error) => {
342
+ this.logger.error('Socket error:', error);
343
+ this.connectionState = 'error';
344
+ this.isConnecting = false;
345
+ const errorMsg = 'WebSocket connection error';
346
+ this.setError(errorMsg);
347
+ this.updateStepAndEmit('queue', { status: 'error', errorMessage: errorMsg });
348
+ this.emit('error', errorMsg);
349
+ });
350
+ this.socket.onClose((event) => {
351
+ this.logger.log('Socket closed:', event);
352
+ this.connectionState = 'disconnected';
353
+ this.isConnecting = false;
354
+ this.stopHeartbeat();
355
+ this.emit('disconnected', 'Socket closed');
356
+ });
357
+ this.socket.connect();
358
+ }
359
+ /**
360
+ * Join the Phoenix channel
361
+ */
362
+ joinChannel(topic) {
363
+ if (!this.socket)
364
+ return;
365
+ this.logger.log('Joining channel:', topic);
366
+ this.channel = this.socket.channel(topic, {});
367
+ this.channel.join()
368
+ .receive('ok', () => {
369
+ this.logger.log('Channel joined successfully');
370
+ this.updateStepAndEmit('queue', { status: 'success', duration: 100 });
371
+ this.updateStepAndEmit('process', { status: 'loading' });
372
+ this.updateStepAndEmit('receive', { status: 'loading' });
373
+ this.emit('channel_joined');
374
+ this.startHeartbeat();
375
+ })
376
+ .receive('error', (error) => {
377
+ const reason = error?.reason || 'unknown';
378
+ this.logger.error('Channel join failed:', reason);
379
+ let errorMsg = 'Failed to join channel';
380
+ if (reason === 'unauthorized_project_access') {
381
+ errorMsg = 'Unauthorized: You do not have access to this project';
382
+ }
383
+ else if (reason === 'invalid_channel_format') {
384
+ errorMsg = 'Invalid channel format';
385
+ }
386
+ else if (reason === 'invalid_project_uuid' || reason === 'invalid_channel_uuid') {
387
+ errorMsg = 'Invalid project or channel ID';
388
+ }
389
+ else if (reason !== 'unknown') {
390
+ errorMsg = `Channel join failed: ${reason}`;
391
+ }
392
+ this.setError(errorMsg);
393
+ this.updateStepAndEmit('queue', { status: 'error', errorMessage: errorMsg });
394
+ this.emit('channel_error', reason);
395
+ });
396
+ // Listen for AI response
397
+ this.channel.on('response', (payload) => {
398
+ this.logger.log('AI Response received:', payload);
399
+ this.handleResponse(payload);
400
+ });
401
+ // Listen for errors
402
+ this.channel.on('error', (payload) => {
403
+ const errorMsg = payload?.message || 'An error occurred';
404
+ this.logger.error('Channel error:', errorMsg);
405
+ this.handleError(errorMsg);
406
+ });
407
+ }
408
+ /**
409
+ * Handle AI response
410
+ */
411
+ handleResponse(payload) {
412
+ const isSuccess = payload.status === 'success' ||
413
+ payload.status === 'SUCCESS' ||
414
+ payload.meta?.status === 'success' ||
415
+ payload.status === 'ok';
416
+ if (isSuccess) {
417
+ this.updateStepAndEmit('process', { status: 'success', duration: payload.meta?.duration_ms });
418
+ this.updateStepAndEmit('receive', { status: 'success', duration: 50 });
419
+ this.updateStepAndEmit('complete', { status: 'success' });
420
+ this.response = payload;
421
+ }
422
+ else {
423
+ const errorMsg = payload.error?.message || 'Unknown error';
424
+ this.updateStepAndEmit('process', { status: 'error', errorMessage: errorMsg });
425
+ this.updateStepAndEmit('receive', { status: 'error' });
426
+ this.updateStepAndEmit('complete', { status: 'error' });
427
+ this.setError(errorMsg);
428
+ }
429
+ // Clear active request from localStorage
430
+ if (this.options.persist) {
431
+ clearActiveRequest(this.options.storageKeyPrefix);
432
+ }
433
+ // Emit response event
434
+ this.emit('response', payload);
435
+ // Close connection after receiving response
436
+ setTimeout(() => {
437
+ this.cleanupConnection();
438
+ }, 1000);
439
+ }
440
+ /**
441
+ * Handle error
442
+ */
443
+ handleError(errorMsg) {
444
+ this.setError(errorMsg);
445
+ this.updateStepAndEmit('process', { status: 'error', errorMessage: errorMsg });
446
+ this.emit('error', errorMsg);
447
+ if (this.options.persist) {
448
+ clearActiveRequest(this.options.storageKeyPrefix);
449
+ }
450
+ }
451
+ /**
452
+ * Disconnect from WebSocket
453
+ */
454
+ disconnect() {
455
+ this.logger.log('Disconnecting...');
456
+ this.isConnecting = false;
457
+ this.cleanupConnection();
458
+ if (this.options.persist) {
459
+ clearActiveRequest(this.options.storageKeyPrefix);
460
+ }
461
+ this.emit('disconnected', 'Manual disconnect');
462
+ }
463
+ /**
464
+ * Reset state and clear stored data
465
+ */
466
+ reset() {
467
+ this.logger.log('Resetting...');
468
+ this.disconnect();
469
+ this.steps = [];
470
+ this.response = null;
471
+ this.error = null;
472
+ this.currentToken = null;
473
+ this.currentWsToken = null;
474
+ }
475
+ /**
476
+ * Try to reconnect using stored token
477
+ */
478
+ reconnect() {
479
+ if (!this.options.persist) {
480
+ this.logger.warn('Persistence is disabled, cannot reconnect');
481
+ return false;
482
+ }
483
+ const activeRequest = getActiveRequest(this.options.storageKeyPrefix);
484
+ if (!activeRequest) {
485
+ this.logger.log('No active request found for reconnection');
486
+ return false;
487
+ }
488
+ this.logger.log('Reconnecting with stored token...');
489
+ this.connect({ wsToken: activeRequest.wsToken });
490
+ return true;
491
+ }
492
+ /**
493
+ * Add event listener
494
+ */
495
+ on(event, callback) {
496
+ if (!this.listeners.has(event)) {
497
+ this.listeners.set(event, new Set());
498
+ }
499
+ this.listeners.get(event).add(callback);
500
+ // Return unsubscribe function
501
+ return () => {
502
+ this.listeners.get(event)?.delete(callback);
503
+ };
504
+ }
505
+ /**
506
+ * Remove event listener
507
+ */
508
+ off(event, callback) {
509
+ this.listeners.get(event)?.delete(callback);
510
+ }
511
+ /**
512
+ * Emit event to listeners
513
+ */
514
+ emit(event, ...args) {
515
+ const callbacks = this.listeners.get(event);
516
+ if (callbacks) {
517
+ callbacks.forEach((callback) => {
518
+ try {
519
+ callback(...args);
520
+ }
521
+ catch (err) {
522
+ this.logger.error(`Error in ${event} listener:`, err);
523
+ }
524
+ });
525
+ }
526
+ }
527
+ /**
528
+ * Update step and emit event
529
+ */
530
+ updateStepAndEmit(id, updates) {
531
+ this.steps = updateStep(this.steps, id, updates);
532
+ const step = this.steps.find((s) => s.id === id);
533
+ if (step) {
534
+ this.emit('step', step);
535
+ }
536
+ }
537
+ /**
538
+ * Set error state
539
+ */
540
+ setError(error) {
541
+ this.error = error;
542
+ }
543
+ /**
544
+ * Start heartbeat
545
+ */
546
+ startHeartbeat() {
547
+ this.stopHeartbeat();
548
+ this.heartbeatInterval = setInterval(() => {
549
+ if (this.channel) {
550
+ this.channel.push('heartbeat', {});
551
+ }
552
+ }, this.options.heartbeatInterval);
553
+ }
554
+ /**
555
+ * Stop heartbeat
556
+ */
557
+ stopHeartbeat() {
558
+ if (this.heartbeatInterval) {
559
+ clearInterval(this.heartbeatInterval);
560
+ this.heartbeatInterval = null;
561
+ }
562
+ }
563
+ /**
564
+ * Clean up connection resources
565
+ */
566
+ cleanupConnection() {
567
+ this.stopHeartbeat();
568
+ if (this.channel) {
569
+ try {
570
+ this.channel.leave();
571
+ }
572
+ catch {
573
+ // Ignore errors during cleanup
574
+ }
575
+ this.channel = null;
576
+ }
577
+ if (this.socket) {
578
+ try {
579
+ this.socket.disconnect();
580
+ }
581
+ catch {
582
+ // Ignore errors during cleanup
583
+ }
584
+ this.socket = null;
585
+ }
586
+ this.connectionState = 'disconnected';
587
+ }
588
+ /**
589
+ * Destroy the client and clean up all resources
590
+ */
591
+ destroy() {
592
+ this.reset();
593
+ this.listeners.clear();
594
+ }
595
+ }
596
+
597
+ /**
598
+ * ModelRiver Vue Composable
599
+ *
600
+ * Vue composable for connecting to ModelRiver's WebSocket-based AI response streaming.
601
+ *
602
+ * @example
603
+ * ```vue
604
+ * <script setup>
605
+ * import { useModelRiver } from '@modelriver/client/vue';
606
+ *
607
+ * const { connect, response, error, isConnected, steps } = useModelRiver({
608
+ * baseUrl: 'wss://api.modelriver.com/socket',
609
+ * });
610
+ *
611
+ * async function handleSend() {
612
+ * const { ws_token } = await yourBackendAPI.createRequest(message);
613
+ * connect({ wsToken: ws_token });
614
+ * }
615
+ * </script>
616
+ *
617
+ * <template>
618
+ * <div>
619
+ * <button @click="handleSend">Send</button>
620
+ * <span v-if="isConnected">Connected</span>
621
+ * <pre v-if="response">{{ response }}</pre>
622
+ * <p v-if="error" class="error">{{ error }}</p>
623
+ * </div>
624
+ * </template>
625
+ * ```
626
+ */
627
+ /**
628
+ * Vue composable for ModelRiver WebSocket client
629
+ *
630
+ * @param options - Client configuration options
631
+ * @returns Composable return value with reactive state and methods
632
+ */
633
+ function useModelRiver(options = {}) {
634
+ // Reactive state
635
+ const connectionState = ref('disconnected');
636
+ const steps = ref([]);
637
+ const response = ref(null);
638
+ const error = ref(null);
639
+ const hasPendingRequest = ref(false);
640
+ const isConnected = ref(false);
641
+ const isConnecting = ref(false);
642
+ // Client instance
643
+ let client = null;
644
+ const unsubscribers = [];
645
+ // Initialize client
646
+ const initClient = () => {
647
+ client = new ModelRiverClient(options);
648
+ // Set up event listeners
649
+ unsubscribers.push(client.on('connecting', () => {
650
+ connectionState.value = 'connecting';
651
+ isConnecting.value = true;
652
+ isConnected.value = false;
653
+ }));
654
+ unsubscribers.push(client.on('connected', () => {
655
+ connectionState.value = 'connected';
656
+ isConnected.value = true;
657
+ isConnecting.value = false;
658
+ }));
659
+ unsubscribers.push(client.on('disconnected', () => {
660
+ connectionState.value = 'disconnected';
661
+ isConnected.value = false;
662
+ isConnecting.value = false;
663
+ }));
664
+ unsubscribers.push(client.on('response', (data) => {
665
+ response.value = data;
666
+ hasPendingRequest.value = false;
667
+ }));
668
+ unsubscribers.push(client.on('error', (err) => {
669
+ error.value = typeof err === 'string' ? err : err.message;
670
+ connectionState.value = 'error';
671
+ isConnecting.value = false;
672
+ }));
673
+ unsubscribers.push(client.on('step', () => {
674
+ if (client) {
675
+ const state = client.getState();
676
+ steps.value = [...state.steps];
677
+ }
678
+ }));
679
+ // Check for pending request
680
+ if (client.hasPendingRequest()) {
681
+ hasPendingRequest.value = true;
682
+ client.reconnect();
683
+ }
684
+ };
685
+ // Cleanup
686
+ const cleanup = () => {
687
+ unsubscribers.forEach((unsub) => unsub());
688
+ unsubscribers.length = 0;
689
+ if (client && !client.hasPendingRequest()) {
690
+ client.destroy();
691
+ }
692
+ client = null;
693
+ };
694
+ // Connect method
695
+ const connect = (connectOptions) => {
696
+ if (!client)
697
+ return;
698
+ error.value = null;
699
+ response.value = null;
700
+ steps.value = [];
701
+ hasPendingRequest.value = true;
702
+ client.connect(connectOptions);
703
+ };
704
+ // Disconnect method
705
+ const disconnect = () => {
706
+ if (!client)
707
+ return;
708
+ client.disconnect();
709
+ hasPendingRequest.value = false;
710
+ };
711
+ // Reset method
712
+ const reset = () => {
713
+ if (!client)
714
+ return;
715
+ client.reset();
716
+ connectionState.value = 'disconnected';
717
+ isConnected.value = false;
718
+ isConnecting.value = false;
719
+ steps.value = [];
720
+ response.value = null;
721
+ error.value = null;
722
+ hasPendingRequest.value = false;
723
+ };
724
+ // Lifecycle hooks
725
+ onMounted(() => {
726
+ initClient();
727
+ });
728
+ onUnmounted(() => {
729
+ cleanup();
730
+ });
731
+ return {
732
+ connectionState,
733
+ isConnected,
734
+ isConnecting,
735
+ steps,
736
+ response,
737
+ error,
738
+ hasPendingRequest,
739
+ connect,
740
+ disconnect,
741
+ reset,
742
+ };
743
+ }
744
+
745
+ export { useModelRiver };
746
+ //# sourceMappingURL=vue.mjs.map