@redseat/api 0.3.12 → 0.3.14
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/client.md +9 -2
- package/dist/client.d.ts +27 -1
- package/dist/client.js +255 -26
- package/dist/interfaces.d.ts +11 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +3 -3
- package/dist/sse-types.d.ts +12 -0
- package/package.json +1 -1
- package/server.md +28 -8
package/client.md
CHANGED
|
@@ -329,6 +329,12 @@ Connects to the server's SSE endpoint for real-time updates.
|
|
|
329
329
|
- `maxReconnectAttempts?`: Maximum reconnect attempts (default: unlimited)
|
|
330
330
|
- `initialReconnectDelay?`: Initial reconnect delay in ms (default: `1000`)
|
|
331
331
|
- `maxReconnectDelay?`: Maximum reconnect delay in ms (default: `30000`)
|
|
332
|
+
- `reconnectOnPageVisible?`: Reconnect when tab becomes visible after being hidden (default: `true`)
|
|
333
|
+
- `reconnectVisibleAfterMs?`: Minimum hidden duration before reconnect on visible (default: `30000`)
|
|
334
|
+
- `reconnectOnOnline?`: Reconnect when browser goes online (default: `true`)
|
|
335
|
+
- `reconnectOnFocus?`: Reconnect on window focus when connection is unhealthy (default: `true`)
|
|
336
|
+
- `reconnectOnAuthError?`: Retry reconnect after auth (`401`) errors (default: `true`)
|
|
337
|
+
- `maxAuthReconnectAttempts?`: Maximum auth reconnect attempts (default: `5`)
|
|
332
338
|
|
|
333
339
|
**Example:**
|
|
334
340
|
```typescript
|
|
@@ -345,7 +351,9 @@ await client.connectSSE({
|
|
|
345
351
|
autoReconnect: true,
|
|
346
352
|
maxReconnectAttempts: 10,
|
|
347
353
|
initialReconnectDelay: 2000,
|
|
348
|
-
maxReconnectDelay: 60000
|
|
354
|
+
maxReconnectDelay: 60000,
|
|
355
|
+
reconnectOnPageVisible: true,
|
|
356
|
+
reconnectVisibleAfterMs: 45000
|
|
349
357
|
});
|
|
350
358
|
```
|
|
351
359
|
|
|
@@ -667,4 +675,3 @@ client.dispose();
|
|
|
667
675
|
- [ServerApi Documentation](server.md) - Uses RedseatClient for server operations
|
|
668
676
|
- [LibraryApi Documentation](libraries.md) - Uses RedseatClient for library operations
|
|
669
677
|
- [README](README.md) - Package overview
|
|
670
|
-
|
package/dist/client.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { IServer } from './interfaces.js';
|
|
|
5
5
|
import { SSEConnectionState, SSEConnectionOptions, SSEConnectionError, SSELibraryEvent, SSELibraryStatusEvent, SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEPeopleEvent, SSETagsEvent, SSEBackupsEvent, SSEBackupFilesEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSEPlayersListEvent, SSEWatchedEvent, SSEUnwatchedEvent, SSERequestProcessingEvent } from './sse-types.js';
|
|
6
6
|
export interface ClientOptions {
|
|
7
7
|
server: IServer;
|
|
8
|
-
getIdToken: () => Promise<string>;
|
|
8
|
+
getIdToken: (forceRefresh?: boolean) => Promise<string>;
|
|
9
9
|
refreshThreshold?: number;
|
|
10
10
|
timeout?: number;
|
|
11
11
|
redseatUrl?: string;
|
|
@@ -24,8 +24,15 @@ export declare class RedseatClient {
|
|
|
24
24
|
private disposed;
|
|
25
25
|
private sseAbortController?;
|
|
26
26
|
private sseReconnectTimeout?;
|
|
27
|
+
private sseActivityTimeout?;
|
|
27
28
|
private sseReconnectAttempts;
|
|
29
|
+
private sseAuthReconnectAttempts;
|
|
28
30
|
private sseOptions?;
|
|
31
|
+
private sseShouldReconnect;
|
|
32
|
+
private sseIsConnecting;
|
|
33
|
+
private sseLifecycleRegistered;
|
|
34
|
+
private sseHiddenAt?;
|
|
35
|
+
private readonly SSE_ACTIVITY_TIMEOUT;
|
|
29
36
|
private readonly _sseConnectionState;
|
|
30
37
|
private readonly _sseError;
|
|
31
38
|
private readonly _sseEvents;
|
|
@@ -94,6 +101,15 @@ export declare class RedseatClient {
|
|
|
94
101
|
* Returns public server info (IServer).
|
|
95
102
|
*/
|
|
96
103
|
getServer(serverId: string): Promise<IServer>;
|
|
104
|
+
private readonly onDocumentVisibilityChange;
|
|
105
|
+
private readonly onWindowPageShow;
|
|
106
|
+
private readonly onWindowOnline;
|
|
107
|
+
private readonly onWindowFocus;
|
|
108
|
+
private shouldAttemptReconnect;
|
|
109
|
+
private clearSSEReconnectTimeout;
|
|
110
|
+
private registerSSELifecycleListeners;
|
|
111
|
+
private unregisterSSELifecycleListeners;
|
|
112
|
+
private forceReconnectSSE;
|
|
97
113
|
/**
|
|
98
114
|
* Connects to the server's SSE endpoint for real-time updates.
|
|
99
115
|
* Automatically manages authentication and reconnection.
|
|
@@ -121,6 +137,16 @@ export declare class RedseatClient {
|
|
|
121
137
|
* Builds the SSE endpoint URL with optional library filters
|
|
122
138
|
*/
|
|
123
139
|
private buildSSEUrl;
|
|
140
|
+
/**
|
|
141
|
+
* Resets the activity timeout timer.
|
|
142
|
+
* Called when data is received from the SSE stream.
|
|
143
|
+
* If no data is received within the timeout period, triggers reconnection.
|
|
144
|
+
*/
|
|
145
|
+
private resetActivityTimeout;
|
|
146
|
+
/**
|
|
147
|
+
* Clears the activity timeout timer.
|
|
148
|
+
*/
|
|
149
|
+
private clearActivityTimeout;
|
|
124
150
|
/**
|
|
125
151
|
* Processes the SSE stream and emits events
|
|
126
152
|
*/
|
package/dist/client.js
CHANGED
|
@@ -38,6 +38,11 @@ export class RedseatClient {
|
|
|
38
38
|
constructor(options) {
|
|
39
39
|
this.disposed = false;
|
|
40
40
|
this.sseReconnectAttempts = 0;
|
|
41
|
+
this.sseAuthReconnectAttempts = 0;
|
|
42
|
+
this.sseShouldReconnect = false;
|
|
43
|
+
this.sseIsConnecting = false;
|
|
44
|
+
this.sseLifecycleRegistered = false;
|
|
45
|
+
this.SSE_ACTIVITY_TIMEOUT = 90000; // 90 seconds - reconnect if no data received
|
|
41
46
|
// RxJS subjects for SSE
|
|
42
47
|
this._sseConnectionState = new BehaviorSubject('disconnected');
|
|
43
48
|
this._sseError = new Subject();
|
|
@@ -65,6 +70,62 @@ export class RedseatClient {
|
|
|
65
70
|
this.watched$ = this.createEventStream('watched');
|
|
66
71
|
this.unwatched$ = this.createEventStream('unwatched');
|
|
67
72
|
this.requestProcessing$ = this.createEventStream('request_processing');
|
|
73
|
+
// ==================== SSE Methods ====================
|
|
74
|
+
this.onDocumentVisibilityChange = () => {
|
|
75
|
+
if (!this.shouldAttemptReconnect() || typeof document === 'undefined') {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (document.visibilityState === 'hidden') {
|
|
79
|
+
this.sseHiddenAt = Date.now();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!(this.sseOptions?.reconnectOnPageVisible ?? true)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const hiddenAt = this.sseHiddenAt;
|
|
86
|
+
this.sseHiddenAt = undefined;
|
|
87
|
+
if (hiddenAt === undefined) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const hiddenDuration = Date.now() - hiddenAt;
|
|
91
|
+
const minHiddenDuration = this.sseOptions?.reconnectVisibleAfterMs ?? 30000;
|
|
92
|
+
if (hiddenDuration >= minHiddenDuration) {
|
|
93
|
+
this.forceReconnectSSE();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
this.onWindowPageShow = (event) => {
|
|
97
|
+
if (!this.shouldAttemptReconnect()) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!(this.sseOptions?.reconnectOnPageVisible ?? true)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (event.persisted || this.sseConnectionState !== 'connected') {
|
|
104
|
+
this.forceReconnectSSE();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.onWindowOnline = () => {
|
|
108
|
+
if (!this.shouldAttemptReconnect()) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!(this.sseOptions?.reconnectOnOnline ?? true)) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (this.sseConnectionState !== 'connected') {
|
|
115
|
+
this.forceReconnectSSE();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
this.onWindowFocus = () => {
|
|
119
|
+
if (!this.shouldAttemptReconnect()) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!(this.sseOptions?.reconnectOnFocus ?? true)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (this.sseConnectionState !== 'connected') {
|
|
126
|
+
this.forceReconnectSSE();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
68
129
|
this.server = options.server;
|
|
69
130
|
this.redseatUrl = options.redseatUrl;
|
|
70
131
|
this.getIdToken = options.getIdToken;
|
|
@@ -167,7 +228,8 @@ export class RedseatClient {
|
|
|
167
228
|
}
|
|
168
229
|
this.tokenRefreshPromise = (async () => {
|
|
169
230
|
try {
|
|
170
|
-
|
|
231
|
+
// Force refresh the Firebase idToken to ensure it's not stale/cached
|
|
232
|
+
const idToken = await this.getIdToken(true);
|
|
171
233
|
// Use fetchServerToken which uses the global axios instance
|
|
172
234
|
// The token endpoint is on the frontend server, not the backend server
|
|
173
235
|
const newToken = await fetchServerToken(this.serverId, idToken, this.redseatUrl);
|
|
@@ -280,7 +342,52 @@ export class RedseatClient {
|
|
|
280
342
|
});
|
|
281
343
|
return response.data;
|
|
282
344
|
}
|
|
283
|
-
|
|
345
|
+
shouldAttemptReconnect() {
|
|
346
|
+
return !this.disposed && this.sseShouldReconnect && (this.sseOptions?.autoReconnect ?? true);
|
|
347
|
+
}
|
|
348
|
+
clearSSEReconnectTimeout() {
|
|
349
|
+
if (this.sseReconnectTimeout) {
|
|
350
|
+
clearTimeout(this.sseReconnectTimeout);
|
|
351
|
+
this.sseReconnectTimeout = undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
registerSSELifecycleListeners() {
|
|
355
|
+
if (this.sseLifecycleRegistered || typeof window === 'undefined' || typeof document === 'undefined') {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
document.addEventListener('visibilitychange', this.onDocumentVisibilityChange);
|
|
359
|
+
window.addEventListener('pageshow', this.onWindowPageShow);
|
|
360
|
+
window.addEventListener('online', this.onWindowOnline);
|
|
361
|
+
window.addEventListener('focus', this.onWindowFocus);
|
|
362
|
+
this.sseLifecycleRegistered = true;
|
|
363
|
+
if (document.visibilityState === 'hidden') {
|
|
364
|
+
this.sseHiddenAt = Date.now();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
unregisterSSELifecycleListeners() {
|
|
368
|
+
if (!this.sseLifecycleRegistered || typeof window === 'undefined' || typeof document === 'undefined') {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange);
|
|
372
|
+
window.removeEventListener('pageshow', this.onWindowPageShow);
|
|
373
|
+
window.removeEventListener('online', this.onWindowOnline);
|
|
374
|
+
window.removeEventListener('focus', this.onWindowFocus);
|
|
375
|
+
this.sseLifecycleRegistered = false;
|
|
376
|
+
this.sseHiddenAt = undefined;
|
|
377
|
+
}
|
|
378
|
+
forceReconnectSSE() {
|
|
379
|
+
if (!this.shouldAttemptReconnect()) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
this.clearSSEReconnectTimeout();
|
|
383
|
+
if (this.sseAbortController) {
|
|
384
|
+
this.sseAbortController.abort();
|
|
385
|
+
this.sseAbortController = undefined;
|
|
386
|
+
}
|
|
387
|
+
this.sseReconnectAttempts = 0;
|
|
388
|
+
this._sseConnectionState.next('reconnecting');
|
|
389
|
+
void this._connectSSE();
|
|
390
|
+
}
|
|
284
391
|
/**
|
|
285
392
|
* Connects to the server's SSE endpoint for real-time updates.
|
|
286
393
|
* Automatically manages authentication and reconnection.
|
|
@@ -293,23 +400,34 @@ export class RedseatClient {
|
|
|
293
400
|
autoReconnect: true,
|
|
294
401
|
initialReconnectDelay: 1000,
|
|
295
402
|
maxReconnectDelay: 30000,
|
|
403
|
+
reconnectOnPageVisible: true,
|
|
404
|
+
reconnectVisibleAfterMs: 30000,
|
|
405
|
+
reconnectOnOnline: true,
|
|
406
|
+
reconnectOnFocus: true,
|
|
407
|
+
reconnectOnAuthError: true,
|
|
408
|
+
maxAuthReconnectAttempts: 5,
|
|
296
409
|
...options
|
|
297
410
|
};
|
|
298
411
|
this.sseReconnectAttempts = 0;
|
|
412
|
+
this.sseAuthReconnectAttempts = 0;
|
|
413
|
+
this.sseShouldReconnect = true;
|
|
414
|
+
this.registerSSELifecycleListeners();
|
|
299
415
|
await this._connectSSE();
|
|
300
416
|
}
|
|
301
417
|
/**
|
|
302
418
|
* Disconnects from the SSE endpoint and cleans up resources.
|
|
303
419
|
*/
|
|
304
420
|
disconnectSSE() {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
421
|
+
this.sseShouldReconnect = false;
|
|
422
|
+
this.sseIsConnecting = false;
|
|
423
|
+
this.sseAuthReconnectAttempts = 0;
|
|
424
|
+
this.clearSSEReconnectTimeout();
|
|
425
|
+
this.clearActivityTimeout();
|
|
309
426
|
if (this.sseAbortController) {
|
|
310
427
|
this.sseAbortController.abort();
|
|
311
428
|
this.sseAbortController = undefined;
|
|
312
429
|
}
|
|
430
|
+
this.unregisterSSELifecycleListeners();
|
|
313
431
|
this._sseConnectionState.next('disconnected');
|
|
314
432
|
}
|
|
315
433
|
/**
|
|
@@ -335,6 +453,14 @@ export class RedseatClient {
|
|
|
335
453
|
* Internal method to establish SSE connection
|
|
336
454
|
*/
|
|
337
455
|
async _connectSSE() {
|
|
456
|
+
if (this.disposed || !this.sseShouldReconnect || this.sseIsConnecting) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (this.sseConnectionState === 'connected' && this.sseAbortController) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.clearSSEReconnectTimeout();
|
|
463
|
+
this.sseIsConnecting = true;
|
|
338
464
|
this._sseConnectionState.next('connecting');
|
|
339
465
|
try {
|
|
340
466
|
// Ensure we have a valid token
|
|
@@ -343,28 +469,54 @@ export class RedseatClient {
|
|
|
343
469
|
throw new Error('No authentication token available');
|
|
344
470
|
}
|
|
345
471
|
const url = this.buildSSEUrl();
|
|
346
|
-
|
|
472
|
+
const abortController = new AbortController();
|
|
473
|
+
this.sseAbortController = abortController;
|
|
347
474
|
console.log("SSSEEEE URL", url);
|
|
348
|
-
|
|
475
|
+
let response = await fetch(url, {
|
|
349
476
|
method: 'GET',
|
|
350
477
|
headers: {
|
|
351
478
|
'Authorization': `Bearer ${this.tokenData.token}`,
|
|
352
479
|
'Accept': 'text/event-stream',
|
|
353
480
|
'Cache-Control': 'no-cache'
|
|
354
481
|
},
|
|
355
|
-
signal:
|
|
482
|
+
signal: abortController.signal
|
|
356
483
|
});
|
|
357
484
|
if (!response.ok) {
|
|
358
485
|
if (response.status === 401) {
|
|
359
|
-
|
|
486
|
+
// Try refreshing token and retry ONCE before giving up
|
|
487
|
+
try {
|
|
488
|
+
console.log('SSE 401 - attempting token refresh and retry');
|
|
489
|
+
await this.refreshToken();
|
|
490
|
+
// Retry the connection with the fresh token
|
|
491
|
+
const retryResponse = await fetch(url, {
|
|
492
|
+
method: 'GET',
|
|
493
|
+
headers: {
|
|
494
|
+
'Authorization': `Bearer ${this.tokenData.token}`,
|
|
495
|
+
'Accept': 'text/event-stream',
|
|
496
|
+
'Cache-Control': 'no-cache'
|
|
497
|
+
},
|
|
498
|
+
signal: this.sseAbortController.signal
|
|
499
|
+
});
|
|
500
|
+
if (!retryResponse.ok) {
|
|
501
|
+
throw Object.assign(new Error('Authentication failed after token refresh'), { type: 'auth' });
|
|
502
|
+
}
|
|
503
|
+
// Use the retry response instead
|
|
504
|
+
response = retryResponse;
|
|
505
|
+
}
|
|
506
|
+
catch (retryError) {
|
|
507
|
+
throw Object.assign(new Error('Authentication failed'), { type: 'auth' });
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
throw Object.assign(new Error(`Server returned ${response.status}`), { type: 'server' });
|
|
360
512
|
}
|
|
361
|
-
throw Object.assign(new Error(`Server returned ${response.status}`), { type: 'server' });
|
|
362
513
|
}
|
|
363
514
|
if (!response.body) {
|
|
364
515
|
throw Object.assign(new Error('No response body'), { type: 'server' });
|
|
365
516
|
}
|
|
366
517
|
this._sseConnectionState.next('connected');
|
|
367
518
|
this.sseReconnectAttempts = 0;
|
|
519
|
+
this.sseAuthReconnectAttempts = 0;
|
|
368
520
|
// Process the stream in the background (don't await - it runs forever until disconnected)
|
|
369
521
|
this.processSSEStream(response.body).catch(err => {
|
|
370
522
|
if (err?.name !== 'AbortError') {
|
|
@@ -377,8 +529,12 @@ export class RedseatClient {
|
|
|
377
529
|
// Intentionally disconnected
|
|
378
530
|
return;
|
|
379
531
|
}
|
|
532
|
+
this.sseAbortController = undefined;
|
|
380
533
|
this.handleSSEError(error);
|
|
381
534
|
}
|
|
535
|
+
finally {
|
|
536
|
+
this.sseIsConnecting = false;
|
|
537
|
+
}
|
|
382
538
|
}
|
|
383
539
|
/**
|
|
384
540
|
* Builds the SSE endpoint URL with optional library filters
|
|
@@ -392,6 +548,39 @@ export class RedseatClient {
|
|
|
392
548
|
}
|
|
393
549
|
return url;
|
|
394
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Resets the activity timeout timer.
|
|
553
|
+
* Called when data is received from the SSE stream.
|
|
554
|
+
* If no data is received within the timeout period, triggers reconnection.
|
|
555
|
+
*/
|
|
556
|
+
resetActivityTimeout() {
|
|
557
|
+
if (this.sseActivityTimeout) {
|
|
558
|
+
clearTimeout(this.sseActivityTimeout);
|
|
559
|
+
}
|
|
560
|
+
this.sseActivityTimeout = setTimeout(() => {
|
|
561
|
+
if (this.disposed) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
// No activity within timeout period - connection is likely stale
|
|
565
|
+
console.log('SSE activity timeout - reconnecting');
|
|
566
|
+
this._sseConnectionState.next('disconnected');
|
|
567
|
+
// Abort current connection and schedule reconnect
|
|
568
|
+
if (this.sseAbortController) {
|
|
569
|
+
this.sseAbortController.abort();
|
|
570
|
+
this.sseAbortController = undefined;
|
|
571
|
+
}
|
|
572
|
+
this.scheduleReconnect();
|
|
573
|
+
}, this.SSE_ACTIVITY_TIMEOUT);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Clears the activity timeout timer.
|
|
577
|
+
*/
|
|
578
|
+
clearActivityTimeout() {
|
|
579
|
+
if (this.sseActivityTimeout) {
|
|
580
|
+
clearTimeout(this.sseActivityTimeout);
|
|
581
|
+
this.sseActivityTimeout = undefined;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
395
584
|
/**
|
|
396
585
|
* Processes the SSE stream and emits events
|
|
397
586
|
*/
|
|
@@ -399,15 +588,22 @@ export class RedseatClient {
|
|
|
399
588
|
const reader = body.getReader();
|
|
400
589
|
const decoder = new TextDecoder();
|
|
401
590
|
let buffer = '';
|
|
591
|
+
// Start activity timeout tracking
|
|
592
|
+
this.resetActivityTimeout();
|
|
402
593
|
try {
|
|
403
594
|
while (true) {
|
|
404
595
|
const { done, value } = await reader.read();
|
|
405
596
|
if (done) {
|
|
406
597
|
// Stream ended - server closed connection
|
|
598
|
+
this.clearActivityTimeout();
|
|
407
599
|
this._sseConnectionState.next('disconnected');
|
|
408
|
-
this.
|
|
600
|
+
if (this.shouldAttemptReconnect()) {
|
|
601
|
+
this.scheduleReconnect();
|
|
602
|
+
}
|
|
409
603
|
break;
|
|
410
604
|
}
|
|
605
|
+
// Data received - reset activity timeout
|
|
606
|
+
this.resetActivityTimeout();
|
|
411
607
|
buffer += decoder.decode(value, { stream: true });
|
|
412
608
|
const { events, remainingBuffer } = this.parseSSEBuffer(buffer);
|
|
413
609
|
buffer = remainingBuffer;
|
|
@@ -418,12 +614,14 @@ export class RedseatClient {
|
|
|
418
614
|
}
|
|
419
615
|
}
|
|
420
616
|
catch (error) {
|
|
617
|
+
this.clearActivityTimeout();
|
|
421
618
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
422
619
|
return;
|
|
423
620
|
}
|
|
424
621
|
throw error;
|
|
425
622
|
}
|
|
426
623
|
finally {
|
|
624
|
+
this.sseAbortController = undefined;
|
|
427
625
|
reader.releaseLock();
|
|
428
626
|
}
|
|
429
627
|
}
|
|
@@ -497,6 +695,9 @@ export class RedseatClient {
|
|
|
497
695
|
* Handles SSE errors and triggers reconnection if appropriate
|
|
498
696
|
*/
|
|
499
697
|
handleSSEError(error) {
|
|
698
|
+
if (this.disposed || !this.sseShouldReconnect) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
500
701
|
const sseError = {
|
|
501
702
|
type: 'network',
|
|
502
703
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
@@ -509,40 +710,68 @@ export class RedseatClient {
|
|
|
509
710
|
}
|
|
510
711
|
this._sseConnectionState.next('error');
|
|
511
712
|
this._sseError.next(sseError);
|
|
512
|
-
// Don't reconnect on auth errors
|
|
513
713
|
if (sseError.type === 'auth') {
|
|
714
|
+
if (!(this.sseOptions?.reconnectOnAuthError ?? true)) {
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const maxAuthReconnectAttempts = this.sseOptions?.maxAuthReconnectAttempts ?? 5;
|
|
718
|
+
if (this.sseAuthReconnectAttempts >= maxAuthReconnectAttempts) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
this.sseAuthReconnectAttempts++;
|
|
722
|
+
this.scheduleReconnect();
|
|
514
723
|
return;
|
|
515
724
|
}
|
|
725
|
+
this.sseAuthReconnectAttempts = 0;
|
|
516
726
|
this.scheduleReconnect();
|
|
517
727
|
}
|
|
518
728
|
/**
|
|
519
729
|
* Schedules a reconnection attempt with exponential backoff
|
|
520
730
|
*/
|
|
521
731
|
scheduleReconnect() {
|
|
522
|
-
if (!this.
|
|
732
|
+
if (!this.shouldAttemptReconnect()) {
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
const options = this.sseOptions;
|
|
736
|
+
if (!options) {
|
|
523
737
|
return;
|
|
524
738
|
}
|
|
525
|
-
if (
|
|
526
|
-
this.sseReconnectAttempts >=
|
|
739
|
+
if (options.maxReconnectAttempts !== undefined &&
|
|
740
|
+
this.sseReconnectAttempts >= options.maxReconnectAttempts) {
|
|
527
741
|
return;
|
|
528
742
|
}
|
|
529
|
-
|
|
530
|
-
|
|
743
|
+
if (this.sseReconnectTimeout) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const initialDelay = options.initialReconnectDelay ?? 1000;
|
|
747
|
+
const maxDelay = options.maxReconnectDelay ?? 30000;
|
|
531
748
|
// Exponential backoff with jitter
|
|
532
749
|
const exponentialDelay = initialDelay * Math.pow(2, this.sseReconnectAttempts);
|
|
533
750
|
const jitter = Math.random() * 1000;
|
|
534
751
|
const delay = Math.min(exponentialDelay + jitter, maxDelay);
|
|
535
752
|
this._sseConnectionState.next('reconnecting');
|
|
536
753
|
this.sseReconnectAttempts++;
|
|
537
|
-
this.sseReconnectTimeout = setTimeout(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
catch {
|
|
543
|
-
// Token refresh failed, will try again on next reconnect
|
|
754
|
+
this.sseReconnectTimeout = setTimeout(() => {
|
|
755
|
+
this.sseReconnectTimeout = undefined;
|
|
756
|
+
if (!this.shouldAttemptReconnect()) {
|
|
757
|
+
return;
|
|
544
758
|
}
|
|
545
|
-
|
|
759
|
+
void (async () => {
|
|
760
|
+
try {
|
|
761
|
+
await this.refreshToken();
|
|
762
|
+
}
|
|
763
|
+
catch (error) {
|
|
764
|
+
console.log('SSE reconnect: token refresh failed, scheduling another retry', error);
|
|
765
|
+
this._sseError.next({
|
|
766
|
+
type: 'auth',
|
|
767
|
+
message: `Token refresh failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
768
|
+
timestamp: Date.now()
|
|
769
|
+
});
|
|
770
|
+
this.scheduleReconnect();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
await this._connectSSE();
|
|
774
|
+
})();
|
|
546
775
|
}, delay);
|
|
547
776
|
}
|
|
548
777
|
}
|
package/dist/interfaces.d.ts
CHANGED
|
@@ -130,6 +130,16 @@ export declare enum LibraryRole {
|
|
|
130
130
|
share = "share",
|
|
131
131
|
admin = "admin"
|
|
132
132
|
}
|
|
133
|
+
export interface UserMapping {
|
|
134
|
+
[key: string]: unknown;
|
|
135
|
+
}
|
|
136
|
+
export interface ServerLibrarySettings {
|
|
137
|
+
faceThreshold?: number;
|
|
138
|
+
ignoreGroups?: boolean;
|
|
139
|
+
preductionModel?: string;
|
|
140
|
+
mapProgress?: UserMapping[];
|
|
141
|
+
dataPath?: string;
|
|
142
|
+
}
|
|
133
143
|
export interface ILibrary {
|
|
134
144
|
id?: string;
|
|
135
145
|
name: string;
|
|
@@ -137,6 +147,7 @@ export interface ILibrary {
|
|
|
137
147
|
source?: LibrarySources;
|
|
138
148
|
roles?: LibraryRole[];
|
|
139
149
|
crypt?: boolean;
|
|
150
|
+
settings: ServerLibrarySettings;
|
|
140
151
|
hidden?: boolean;
|
|
141
152
|
status?: string;
|
|
142
153
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export declare class ServerApi {
|
|
|
11
11
|
timeout?: number;
|
|
12
12
|
}): Promise<any>;
|
|
13
13
|
addLibrary(library: Partial<ILibrary>): Promise<ILibrary>;
|
|
14
|
-
|
|
14
|
+
updateLibrary(libraryId: string, library: Partial<ILibrary>): Promise<ILibrary>;
|
|
15
15
|
getPlugins(): Promise<IPlugin[]>;
|
|
16
16
|
getCredentials(): Promise<ICredential[]>;
|
|
17
17
|
saveCredential(credential: ICredential): Promise<ICredential>;
|
package/dist/server.js
CHANGED
|
@@ -18,9 +18,9 @@ export class ServerApi {
|
|
|
18
18
|
const res = await this.client.post('/libraries', library);
|
|
19
19
|
return res.data;
|
|
20
20
|
}
|
|
21
|
-
async
|
|
22
|
-
const res = await this.client.
|
|
23
|
-
return res.data
|
|
21
|
+
async updateLibrary(libraryId, library) {
|
|
22
|
+
const res = await this.client.patch(`/libraries/${libraryId}`, library);
|
|
23
|
+
return res.data;
|
|
24
24
|
}
|
|
25
25
|
async getPlugins() {
|
|
26
26
|
const res = await this.client.get('/plugins');
|
package/dist/sse-types.d.ts
CHANGED
|
@@ -12,6 +12,18 @@ export interface SSEConnectionOptions {
|
|
|
12
12
|
initialReconnectDelay?: number;
|
|
13
13
|
/** Maximum reconnect delay in ms (default: 30000) */
|
|
14
14
|
maxReconnectDelay?: number;
|
|
15
|
+
/** Reconnect when tab becomes visible after being hidden for a while (default: true) */
|
|
16
|
+
reconnectOnPageVisible?: boolean;
|
|
17
|
+
/** Minimum hidden time before reconnecting on visible, in ms (default: 30000) */
|
|
18
|
+
reconnectVisibleAfterMs?: number;
|
|
19
|
+
/** Reconnect when browser comes back online (default: true) */
|
|
20
|
+
reconnectOnOnline?: boolean;
|
|
21
|
+
/** Reconnect on window focus when disconnected/error (default: true) */
|
|
22
|
+
reconnectOnFocus?: boolean;
|
|
23
|
+
/** Retry reconnection on auth (401) errors (default: true) */
|
|
24
|
+
reconnectOnAuthError?: boolean;
|
|
25
|
+
/** Maximum auth-error reconnect attempts before giving up (default: 5) */
|
|
26
|
+
maxAuthReconnectAttempts?: number;
|
|
15
27
|
}
|
|
16
28
|
export interface SSEConnectionError {
|
|
17
29
|
type: 'network' | 'auth' | 'server' | 'parse';
|
package/package.json
CHANGED
package/server.md
CHANGED
|
@@ -6,6 +6,7 @@ The `ServerApi` class provides server-level operations for managing libraries, s
|
|
|
6
6
|
|
|
7
7
|
`ServerApi` handles operations that are not specific to a single library, such as:
|
|
8
8
|
- Listing and creating libraries
|
|
9
|
+
- Updating libraries
|
|
9
10
|
- Getting current user information
|
|
10
11
|
- Managing server settings
|
|
11
12
|
- Listing plugins and credentials
|
|
@@ -66,6 +67,12 @@ Creates a new library.
|
|
|
66
67
|
- `type`: Library type - `'photos'`, `'shows'`, `'movies'`, or `'iptv'` (required)
|
|
67
68
|
- `source`: Optional source type
|
|
68
69
|
- `crypt`: Optional boolean to enable encryption
|
|
70
|
+
- `settings`: Optional library settings object:
|
|
71
|
+
- `faceThreshold?: number`
|
|
72
|
+
- `ignoreGroups?: boolean`
|
|
73
|
+
- `preductionModel?: string` prediction model to use for tagging
|
|
74
|
+
- `mapProgress?: Record<string, any>[]` allow to map view progress from a user to another user
|
|
75
|
+
- `dataPath?: string` custom path to store library running data like thumnails, cache, portraits...
|
|
69
76
|
- `hidden`: Optional boolean to hide library
|
|
70
77
|
|
|
71
78
|
**Returns:** Promise resolving to the created `ILibrary` object with generated `id`
|
|
@@ -75,24 +82,33 @@ Creates a new library.
|
|
|
75
82
|
const newLibrary = await serverApi.addLibrary({
|
|
76
83
|
name: 'My Photos',
|
|
77
84
|
type: 'photos',
|
|
78
|
-
crypt: true // Enable encryption
|
|
85
|
+
crypt: true, // Enable encryption
|
|
86
|
+
settings: {
|
|
87
|
+
faceThreshold: 0.7
|
|
88
|
+
}
|
|
79
89
|
});
|
|
80
90
|
console.log(`Created library with ID: ${newLibrary.id}`);
|
|
81
91
|
```
|
|
82
92
|
|
|
83
|
-
### `
|
|
93
|
+
### `updateLibrary(libraryId: string, library: Partial<ILibrary>): Promise<ILibrary>`
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
Updates an existing library.
|
|
86
96
|
|
|
87
97
|
**Parameters:**
|
|
88
|
-
- `
|
|
98
|
+
- `libraryId`: The library ID to update
|
|
99
|
+
- `library`: Partial library object containing fields to update (for example `name`, `source`, or `settings`)
|
|
89
100
|
|
|
90
|
-
**Returns:** Promise resolving to the
|
|
101
|
+
**Returns:** Promise resolving to the updated `ILibrary` object
|
|
91
102
|
|
|
92
103
|
**Example:**
|
|
93
104
|
```typescript
|
|
94
|
-
const
|
|
95
|
-
|
|
105
|
+
const updatedLibrary = await serverApi.updateLibrary('library-123', {
|
|
106
|
+
name: 'Renamed Library',
|
|
107
|
+
settings: {
|
|
108
|
+
faceThreshold: 0.8
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
console.log(`Updated library: ${updatedLibrary.name}`);
|
|
96
112
|
```
|
|
97
113
|
|
|
98
114
|
### `getPlugins(): Promise<any[]>`
|
|
@@ -509,6 +525,11 @@ const newLibrary = await serverApi.addLibrary({
|
|
|
509
525
|
crypt: true
|
|
510
526
|
});
|
|
511
527
|
|
|
528
|
+
// Update the library settings
|
|
529
|
+
const updatedLibrary = await serverApi.updateLibrary(newLibrary.id!, {
|
|
530
|
+
name: 'Encrypted Family Photos'
|
|
531
|
+
});
|
|
532
|
+
|
|
512
533
|
// Get server settings
|
|
513
534
|
const maxUpload = await serverApi.getSetting('max_upload_size');
|
|
514
535
|
console.log(`Max upload size: ${maxUpload}`);
|
|
@@ -535,4 +556,3 @@ try {
|
|
|
535
556
|
- [RedseatClient Documentation](client.md) - HTTP client used by ServerApi
|
|
536
557
|
- [LibraryApi Documentation](libraries.md) - Library-specific operations
|
|
537
558
|
- [README](README.md) - Package overview
|
|
538
|
-
|