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