@redseat/api 0.4.5 → 0.5.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/interfaces.d.ts +13 -2
- package/dist/library.d.ts +29 -4
- package/dist/library.js +60 -6
- package/dist/server.d.ts +4 -1
- package/dist/server.js +7 -2
- package/dist/sse-fetch.d.ts +1 -0
- package/dist/sse-fetch.js +54 -44
- package/package.json +1 -1
package/dist/interfaces.d.ts
CHANGED
|
@@ -202,6 +202,7 @@ export interface ServerLibraryForUpdate {
|
|
|
202
202
|
settings?: ServerLibrarySettings;
|
|
203
203
|
credentials?: string;
|
|
204
204
|
plugin?: string;
|
|
205
|
+
password?: string;
|
|
205
206
|
}
|
|
206
207
|
export interface ILibrary {
|
|
207
208
|
id?: string;
|
|
@@ -216,6 +217,7 @@ export interface ILibrary {
|
|
|
216
217
|
plugin?: string;
|
|
217
218
|
hidden?: boolean;
|
|
218
219
|
status?: string;
|
|
220
|
+
password?: string;
|
|
219
221
|
}
|
|
220
222
|
export interface ITag {
|
|
221
223
|
id: string;
|
|
@@ -451,6 +453,7 @@ export interface ExternalImage {
|
|
|
451
453
|
voteAverage?: number;
|
|
452
454
|
voteCount?: number;
|
|
453
455
|
width?: number;
|
|
456
|
+
matchType?: RsLookupMatchType;
|
|
454
457
|
}
|
|
455
458
|
export interface Relations {
|
|
456
459
|
peopleDetails?: IPerson[];
|
|
@@ -478,6 +481,7 @@ export interface SearchRelations {
|
|
|
478
481
|
export interface SearchStreamResultBase {
|
|
479
482
|
relations?: SearchRelations;
|
|
480
483
|
images?: ExternalImage[];
|
|
484
|
+
matchType?: RsLookupMatchType;
|
|
481
485
|
}
|
|
482
486
|
export type SearchStreamResult<K extends string, T> = SearchStreamResultBase & {
|
|
483
487
|
metadata?: Partial<Record<K, T>>;
|
|
@@ -490,9 +494,15 @@ export type BookSearchStreamResult = SearchStreamResult<'book', IBook>;
|
|
|
490
494
|
export type MovieSearchStreamResult = SearchStreamResult<'movie', IMovie>;
|
|
491
495
|
export type SerieSearchStreamResult = SearchStreamResult<'serie', ISerie>;
|
|
492
496
|
export type LookupSearchStreamResult = SearchStreamResult<'book' | 'movie' | 'serie', IBook | IMovie | ISerie>;
|
|
493
|
-
export type
|
|
497
|
+
export type RsLookupMatchType = 'exactId' | 'exactText';
|
|
498
|
+
export interface SseSearchStreamEvent<T> {
|
|
499
|
+
sourceId: string;
|
|
500
|
+
sourceName: string;
|
|
501
|
+
results: T[];
|
|
502
|
+
nextPageKey?: string | null;
|
|
503
|
+
}
|
|
494
504
|
export interface SearchStreamCallbacks<T> {
|
|
495
|
-
onResults?: (
|
|
505
|
+
onResults?: (event: SseSearchStreamEvent<T>) => void;
|
|
496
506
|
onFinished?: () => void;
|
|
497
507
|
onError?: (error: unknown) => void;
|
|
498
508
|
}
|
|
@@ -927,6 +937,7 @@ export interface RsGroupDownload {
|
|
|
927
937
|
groupMime?: string;
|
|
928
938
|
requests: RsRequest[];
|
|
929
939
|
infos?: MediaForUpdate;
|
|
940
|
+
matchType?: RsLookupMatchType;
|
|
930
941
|
}
|
|
931
942
|
/**
|
|
932
943
|
* Request processing status from plugin-based download/processing.
|
package/dist/library.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Observable } from 'rxjs';
|
|
2
2
|
import type { AxiosResponse } from 'axios';
|
|
3
|
-
import { IFile, ITag, IPerson, ISerie, IMovie, IBook, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, DeletedQuery, RsDeleted, MovieSort, BookSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload, IViewProgress, IWatched, IRsRequestProcessing, BookSearchStreamResult, MovieSearchStreamResult, SerieSearchStreamResult, BookSearchResult, MovieSearchResult, SerieSearchResult, ItemWithRelations, SearchStreamCallbacks, MediaForUpdate, IChannelUpdate } from './interfaces.js';
|
|
3
|
+
import { IFile, ITag, IPerson, ISerie, IMovie, IBook, MediaRequest, IEpisode, ExternalImage, IBackupFile, ILibrary, DeletedQuery, RsDeleted, MovieSort, BookSort, RsSort, SqlOrder, RsRequest, DetectedFaceResult, UnassignFaceResponse, RsGroupDownload, IViewProgress, IWatched, IRsRequestProcessing, BookSearchStreamResult, MovieSearchStreamResult, SerieSearchStreamResult, LookupSearchStreamResult, BookSearchResult, MovieSearchResult, SerieSearchResult, ItemWithRelations, SearchStreamCallbacks, MediaForUpdate, IChannelUpdate } from './interfaces.js';
|
|
4
4
|
import { SSEMediasEvent, SSEUploadProgressEvent, SSEConvertProgressEvent, SSEEpisodesEvent, SSESeriesEvent, SSEMoviesEvent, SSEBooksEvent, SSEPeopleEvent, SSETagsEvent, SSELibraryStatusEvent, SSEMediaRatingEvent, SSEMediaProgressEvent, SSERequestProcessingEvent } from './sse-types.js';
|
|
5
5
|
import { EncryptFileOptions, EncryptedFile } from './encryption.js';
|
|
6
6
|
export interface LibraryHttpClient {
|
|
@@ -78,6 +78,10 @@ export declare class LibraryApi {
|
|
|
78
78
|
setKey(passPhrase: string): Promise<void>;
|
|
79
79
|
private getUrl;
|
|
80
80
|
private openSearchStream;
|
|
81
|
+
searchAllStream(name: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>, options?: {
|
|
82
|
+
source?: string[];
|
|
83
|
+
pageKey?: string;
|
|
84
|
+
}): () => void;
|
|
81
85
|
getTags(query?: {
|
|
82
86
|
name?: string;
|
|
83
87
|
parent?: string;
|
|
@@ -127,6 +131,8 @@ export declare class LibraryApi {
|
|
|
127
131
|
tagMerge(fromId: string, intoId: string): Promise<ITag>;
|
|
128
132
|
tagAddAlt(tagId: string, alt: string): Promise<ITag>;
|
|
129
133
|
tagRemoveAlt(tagId: string, alt: string): Promise<ITag>;
|
|
134
|
+
tagAddOtherId(tagId: string, otherId: string): Promise<ITag>;
|
|
135
|
+
tagRemoveOtherId(tagId: string, otherId: string): Promise<ITag>;
|
|
130
136
|
mediaTransfer(formData: FormData): Promise<void>;
|
|
131
137
|
mediaTransferMany(medias: string[], deleteOriginal: boolean, toLibraryId: string): Promise<void>;
|
|
132
138
|
mediaTransferSingle(mediaId: string, toLibraryId: string, formData: FormData, params?: Map<string, string>): Promise<void>;
|
|
@@ -158,7 +164,10 @@ export declare class LibraryApi {
|
|
|
158
164
|
getMovieProgress(movieId: string): Promise<IViewProgress>;
|
|
159
165
|
setMovieProgress(movieId: string, progress: number): Promise<void>;
|
|
160
166
|
searchMovies(name: string): Promise<MovieSearchResult[]>;
|
|
161
|
-
searchMoviesStream(name: string, callbacks: SearchStreamCallbacks<MovieSearchStreamResult
|
|
167
|
+
searchMoviesStream(name: string, callbacks: SearchStreamCallbacks<MovieSearchStreamResult>, options?: {
|
|
168
|
+
source?: string[];
|
|
169
|
+
pageKey?: string;
|
|
170
|
+
}): () => void;
|
|
162
171
|
movieRename(movieId: string, newName: string): Promise<IMovie>;
|
|
163
172
|
updateMoviePoster(movieId: string, poster: FormData, type: string): Promise<void>;
|
|
164
173
|
updateMovieImageFetch(movieId: string, image: ExternalImage): Promise<void>;
|
|
@@ -178,7 +187,10 @@ export declare class LibraryApi {
|
|
|
178
187
|
removeBook(bookId: string): Promise<void>;
|
|
179
188
|
updateBook(bookId: string, updates: Partial<IBook>): Promise<IBook>;
|
|
180
189
|
searchBooks(name: string): Promise<BookSearchResult[]>;
|
|
181
|
-
searchBooksStream(name: string, callbacks: SearchStreamCallbacks<BookSearchStreamResult
|
|
190
|
+
searchBooksStream(name: string, callbacks: SearchStreamCallbacks<BookSearchStreamResult>, options?: {
|
|
191
|
+
source?: string[];
|
|
192
|
+
pageKey?: string;
|
|
193
|
+
}): () => void;
|
|
182
194
|
bookRename(bookId: string, newName: string): Promise<IBook>;
|
|
183
195
|
getBookImages(bookId: string): Promise<ExternalImage[]>;
|
|
184
196
|
updateBookPoster(bookId: string, poster: FormData): Promise<void>;
|
|
@@ -193,12 +205,17 @@ export declare class LibraryApi {
|
|
|
193
205
|
personRemoveAlt(personId: string, alt: string): Promise<IPerson>;
|
|
194
206
|
personAddSocial(personId: string, social: any): Promise<IPerson>;
|
|
195
207
|
personRemoveSocial(personId: string, social: any): Promise<IPerson>;
|
|
208
|
+
personAddOtherId(personId: string, otherId: string): Promise<IPerson>;
|
|
209
|
+
personRemoveOtherId(personId: string, otherId: string): Promise<IPerson>;
|
|
196
210
|
serieRename(serieId: string, newName: string): Promise<ISerie>;
|
|
197
211
|
serieAddAlt(serieId: string, alt: string): Promise<ISerie>;
|
|
198
212
|
serieRemoveAlt(serieId: string, alt: string): Promise<ISerie>;
|
|
199
213
|
updatePersonPortrait(personId: string, portrait: FormData): Promise<void>;
|
|
200
214
|
searchSeries(name: string): Promise<SerieSearchResult[]>;
|
|
201
|
-
searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult
|
|
215
|
+
searchSeriesStream(name: string, callbacks: SearchStreamCallbacks<SerieSearchStreamResult>, options?: {
|
|
216
|
+
source?: string[];
|
|
217
|
+
pageKey?: string;
|
|
218
|
+
}): () => void;
|
|
202
219
|
setEpisodeWatched(serieId: string, season: number, number: number, date: number): Promise<void>;
|
|
203
220
|
getEpisodeWatched(serieId: string, season: number, episode: number): Promise<IWatched>;
|
|
204
221
|
getEpisodeProgress(serieId: string, season: number, episode: number): Promise<IViewProgress>;
|
|
@@ -287,6 +304,14 @@ export declare class LibraryApi {
|
|
|
287
304
|
* @returns Raw axios response with stream data - use response.data for the stream
|
|
288
305
|
*/
|
|
289
306
|
processRequestStream(request: RsRequest): Promise<AxiosResponse>;
|
|
307
|
+
/**
|
|
308
|
+
* Process a request and return the response as a Blob.
|
|
309
|
+
* Use this in environments where ReadableStream is not supported (e.g. React Native).
|
|
310
|
+
* Same endpoint as processRequestStream but with responseType 'blob'.
|
|
311
|
+
* @param request - The request to process and retrieve as a blob
|
|
312
|
+
* @returns The response blob (e.g. image data)
|
|
313
|
+
*/
|
|
314
|
+
processRequestBlob(request: RsRequest): Promise<Blob>;
|
|
290
315
|
/**
|
|
291
316
|
* Add a request to the processing queue.
|
|
292
317
|
* @param request - The request to add for processing
|
package/dist/library.js
CHANGED
|
@@ -124,6 +124,14 @@ export class LibraryApi {
|
|
|
124
124
|
}
|
|
125
125
|
}, finish, (error) => callbacks.onError?.(error));
|
|
126
126
|
}
|
|
127
|
+
searchAllStream(name, callbacks, options) {
|
|
128
|
+
const params = { name };
|
|
129
|
+
if (options?.source?.length)
|
|
130
|
+
params.source = options.source.join(',');
|
|
131
|
+
if (options?.pageKey)
|
|
132
|
+
params.pageKey = options.pageKey;
|
|
133
|
+
return this.openSearchStream(this.getUrl('/searchstream'), params, callbacks);
|
|
134
|
+
}
|
|
127
135
|
async getTags(query) {
|
|
128
136
|
const params = {};
|
|
129
137
|
if (query) {
|
|
@@ -295,6 +303,14 @@ export class LibraryApi {
|
|
|
295
303
|
const res = await this.client.patch(this.getUrl(`/tags/${tagId}`), { removeAlts: [alt] });
|
|
296
304
|
return res.data;
|
|
297
305
|
}
|
|
306
|
+
async tagAddOtherId(tagId, otherId) {
|
|
307
|
+
const res = await this.client.patch(this.getUrl(`/tags/${tagId}`), { addOtherids: [otherId] });
|
|
308
|
+
return res.data;
|
|
309
|
+
}
|
|
310
|
+
async tagRemoveOtherId(tagId, otherId) {
|
|
311
|
+
const res = await this.client.patch(this.getUrl(`/tags/${tagId}`), { removeOtherids: [otherId] });
|
|
312
|
+
return res.data;
|
|
313
|
+
}
|
|
298
314
|
async mediaTransfer(formData) {
|
|
299
315
|
await this.client.put(this.getUrl('/medias/transfert'), formData);
|
|
300
316
|
}
|
|
@@ -417,8 +433,13 @@ export class LibraryApi {
|
|
|
417
433
|
const res = await this.client.get(this.getUrl(`/movies/search?name=${name}`));
|
|
418
434
|
return res.data;
|
|
419
435
|
}
|
|
420
|
-
searchMoviesStream(name, callbacks) {
|
|
421
|
-
|
|
436
|
+
searchMoviesStream(name, callbacks, options) {
|
|
437
|
+
const params = { name };
|
|
438
|
+
if (options?.source?.length)
|
|
439
|
+
params.source = options.source.join(',');
|
|
440
|
+
if (options?.pageKey)
|
|
441
|
+
params.pageKey = options.pageKey;
|
|
442
|
+
return this.openSearchStream(this.getUrl('/movies/searchstream'), params, callbacks);
|
|
422
443
|
}
|
|
423
444
|
async movieRename(movieId, newName) {
|
|
424
445
|
const res = await this.client.patch(this.getUrl(`/movies/${movieId}`), {
|
|
@@ -485,8 +506,13 @@ export class LibraryApi {
|
|
|
485
506
|
const res = await this.client.get(this.getUrl(`/books/search?name=${name}`));
|
|
486
507
|
return res.data;
|
|
487
508
|
}
|
|
488
|
-
searchBooksStream(name, callbacks) {
|
|
489
|
-
|
|
509
|
+
searchBooksStream(name, callbacks, options) {
|
|
510
|
+
const params = { name };
|
|
511
|
+
if (options?.source?.length)
|
|
512
|
+
params.source = options.source.join(',');
|
|
513
|
+
if (options?.pageKey)
|
|
514
|
+
params.pageKey = options.pageKey;
|
|
515
|
+
return this.openSearchStream(this.getUrl('/books/searchstream'), params, callbacks);
|
|
490
516
|
}
|
|
491
517
|
async bookRename(bookId, newName) {
|
|
492
518
|
const res = await this.client.patch(this.getUrl(`/books/${bookId}`), {
|
|
@@ -552,6 +578,18 @@ export class LibraryApi {
|
|
|
552
578
|
});
|
|
553
579
|
return res.data;
|
|
554
580
|
}
|
|
581
|
+
async personAddOtherId(personId, otherId) {
|
|
582
|
+
const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
|
|
583
|
+
addOtherids: [otherId]
|
|
584
|
+
});
|
|
585
|
+
return res.data;
|
|
586
|
+
}
|
|
587
|
+
async personRemoveOtherId(personId, otherId) {
|
|
588
|
+
const res = await this.client.patch(this.getUrl(`/people/${personId}`), {
|
|
589
|
+
removeOtherids: [otherId]
|
|
590
|
+
});
|
|
591
|
+
return res.data;
|
|
592
|
+
}
|
|
555
593
|
async serieRename(serieId, newName) {
|
|
556
594
|
const res = await this.client.patch(this.getUrl(`/series/${serieId}`), {
|
|
557
595
|
name: newName
|
|
@@ -577,8 +615,13 @@ export class LibraryApi {
|
|
|
577
615
|
const res = await this.client.get(this.getUrl(`/series/search?name=${name}`));
|
|
578
616
|
return res.data;
|
|
579
617
|
}
|
|
580
|
-
searchSeriesStream(name, callbacks) {
|
|
581
|
-
|
|
618
|
+
searchSeriesStream(name, callbacks, options) {
|
|
619
|
+
const params = { name };
|
|
620
|
+
if (options?.source?.length)
|
|
621
|
+
params.source = options.source.join(',');
|
|
622
|
+
if (options?.pageKey)
|
|
623
|
+
params.pageKey = options.pageKey;
|
|
624
|
+
return this.openSearchStream(this.getUrl('/series/searchstream'), params, callbacks);
|
|
582
625
|
}
|
|
583
626
|
async setEpisodeWatched(serieId, season, number, date) {
|
|
584
627
|
await this.client.post(this.getUrl(`/series/${serieId}/seasons/${season}/episodes/${number}/watched`), { date });
|
|
@@ -708,6 +751,17 @@ export class LibraryApi {
|
|
|
708
751
|
async processRequestStream(request) {
|
|
709
752
|
return this.client.post(this.getUrl('/plugins/requests/process/stream'), request, { responseType: 'stream' });
|
|
710
753
|
}
|
|
754
|
+
/**
|
|
755
|
+
* Process a request and return the response as a Blob.
|
|
756
|
+
* Use this in environments where ReadableStream is not supported (e.g. React Native).
|
|
757
|
+
* Same endpoint as processRequestStream but with responseType 'blob'.
|
|
758
|
+
* @param request - The request to process and retrieve as a blob
|
|
759
|
+
* @returns The response blob (e.g. image data)
|
|
760
|
+
*/
|
|
761
|
+
async processRequestBlob(request) {
|
|
762
|
+
const res = await this.client.post(this.getUrl('/plugins/requests/process/stream'), request, { responseType: 'blob' });
|
|
763
|
+
return res.data;
|
|
764
|
+
}
|
|
711
765
|
/**
|
|
712
766
|
* Add a request to the processing queue.
|
|
713
767
|
* @param request - The request to add for processing
|
package/dist/server.d.ts
CHANGED
|
@@ -15,7 +15,10 @@ export declare class ServerApi {
|
|
|
15
15
|
updateLibrary(libraryId: string, library: ServerLibraryForUpdate): Promise<ILibrary>;
|
|
16
16
|
deleteLibrary(libraryId: string, deleteMediaContent?: boolean): Promise<void>;
|
|
17
17
|
getPlugins(): Promise<IPlugin[]>;
|
|
18
|
-
searchLookupStream(query: string, type: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult
|
|
18
|
+
searchLookupStream(query: string, type: string, callbacks: SearchStreamCallbacks<LookupSearchStreamResult>, options?: {
|
|
19
|
+
source?: string[];
|
|
20
|
+
pageKey?: string;
|
|
21
|
+
}): () => void;
|
|
19
22
|
getCredentials(): Promise<ICredential[]>;
|
|
20
23
|
saveCredential(credential: ICredential): Promise<ICredential>;
|
|
21
24
|
updateCredential(credential: ICredential): Promise<ICredential>;
|
package/dist/server.js
CHANGED
|
@@ -73,8 +73,13 @@ export class ServerApi {
|
|
|
73
73
|
const res = await this.client.get('/plugins');
|
|
74
74
|
return res.data;
|
|
75
75
|
}
|
|
76
|
-
searchLookupStream(query, type, callbacks) {
|
|
77
|
-
|
|
76
|
+
searchLookupStream(query, type, callbacks, options) {
|
|
77
|
+
const params = { q: query, type };
|
|
78
|
+
if (options?.source?.length)
|
|
79
|
+
params.source = options.source.join(',');
|
|
80
|
+
if (options?.pageKey)
|
|
81
|
+
params.pageKey = options.pageKey;
|
|
82
|
+
return this.openSearchStream('/plugins/lookup/searchstream', params, callbacks);
|
|
78
83
|
}
|
|
79
84
|
async getCredentials() {
|
|
80
85
|
const res = await this.client.get('/credentials');
|
package/dist/sse-fetch.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface RawSSEEvent {
|
|
|
10
10
|
/**
|
|
11
11
|
* Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
|
|
12
12
|
* Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
|
|
13
|
+
* Handles \r\n, \r, and \n line endings per the SSE specification.
|
|
13
14
|
*/
|
|
14
15
|
export declare function parseSSEBuffer(buffer: string): {
|
|
15
16
|
events: RawSSEEvent[];
|
package/dist/sse-fetch.js
CHANGED
|
@@ -1,58 +1,60 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
|
|
3
3
|
* Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
|
|
4
|
+
* Handles \r\n, \r, and \n line endings per the SSE specification.
|
|
4
5
|
*/
|
|
5
6
|
export function parseSSEBuffer(buffer) {
|
|
6
7
|
const events = [];
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
let
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (
|
|
8
|
+
const normalised = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
9
|
+
// Split into blocks separated by blank lines (\n\n).
|
|
10
|
+
// Only complete blocks (followed by \n\n) are processed;
|
|
11
|
+
// any trailing incomplete block stays in the buffer.
|
|
12
|
+
let lastEventEnd = 0;
|
|
13
|
+
const blockBoundary = '\n\n';
|
|
14
|
+
let searchFrom = 0;
|
|
15
|
+
while (true) {
|
|
16
|
+
const idx = normalised.indexOf(blockBoundary, searchFrom);
|
|
17
|
+
if (idx === -1)
|
|
17
18
|
break;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const block = normalised.slice(lastEventEnd, idx);
|
|
20
|
+
lastEventEnd = idx + blockBoundary.length;
|
|
21
|
+
searchFrom = lastEventEnd;
|
|
22
|
+
let eventType = '';
|
|
23
|
+
let eventData = '';
|
|
24
|
+
let eventId;
|
|
25
|
+
let eventRetry;
|
|
26
|
+
for (const line of block.split('\n')) {
|
|
27
|
+
if (line === '' || line.startsWith(':'))
|
|
28
|
+
continue;
|
|
29
|
+
const colonIdx = line.indexOf(':');
|
|
30
|
+
if (colonIdx === -1)
|
|
31
|
+
continue;
|
|
32
|
+
const field = line.slice(0, colonIdx);
|
|
33
|
+
let value = line.slice(colonIdx + 1);
|
|
34
|
+
if (value.startsWith(' '))
|
|
35
|
+
value = value.slice(1);
|
|
36
|
+
switch (field) {
|
|
37
|
+
case 'event':
|
|
38
|
+
eventType = value;
|
|
39
|
+
break;
|
|
40
|
+
case 'data':
|
|
41
|
+
eventData = eventData ? eventData + '\n' + value : value;
|
|
42
|
+
break;
|
|
43
|
+
case 'id':
|
|
44
|
+
eventId = value;
|
|
45
|
+
break;
|
|
46
|
+
case 'retry':
|
|
47
|
+
eventRetry = parseInt(value, 10);
|
|
48
|
+
break;
|
|
24
49
|
}
|
|
25
|
-
eventType = '';
|
|
26
|
-
eventData = '';
|
|
27
|
-
eventId = undefined;
|
|
28
|
-
eventRetry = undefined;
|
|
29
|
-
continue;
|
|
30
50
|
}
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
const colonIdx = line.indexOf(':');
|
|
34
|
-
if (colonIdx === -1)
|
|
35
|
-
continue;
|
|
36
|
-
const field = line.slice(0, colonIdx);
|
|
37
|
-
let value = line.slice(colonIdx + 1);
|
|
38
|
-
if (value.startsWith(' '))
|
|
39
|
-
value = value.slice(1);
|
|
40
|
-
switch (field) {
|
|
41
|
-
case 'event':
|
|
42
|
-
eventType = value;
|
|
43
|
-
break;
|
|
44
|
-
case 'data':
|
|
45
|
-
eventData = eventData ? eventData + '\n' + value : value;
|
|
46
|
-
break;
|
|
47
|
-
case 'id':
|
|
48
|
-
eventId = value;
|
|
49
|
-
break;
|
|
50
|
-
case 'retry':
|
|
51
|
-
eventRetry = parseInt(value, 10);
|
|
52
|
-
break;
|
|
51
|
+
if (eventType) {
|
|
52
|
+
events.push({ event: eventType, data: eventData, id: eventId, retry: eventRetry });
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// Note: remainingBuffer is from the normalised string. Callers must replace
|
|
56
|
+
// their buffer entirely (not use offset-based splicing on the original input).
|
|
57
|
+
return { events, remainingBuffer: normalised.slice(lastEventEnd) };
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
60
|
* Opens a one-shot fetch-based SSE stream (no reconnection).
|
|
@@ -87,6 +89,14 @@ export function openFetchSSEStream(url, onEvent, onDone, onError) {
|
|
|
87
89
|
while (true) {
|
|
88
90
|
const { done, value } = await reader.read();
|
|
89
91
|
if (done) {
|
|
92
|
+
// Flush any remaining buffered event when the stream closes
|
|
93
|
+
// (server may close without a trailing blank line)
|
|
94
|
+
if (buffer.trim()) {
|
|
95
|
+
const { events: finalEvents } = parseSSEBuffer(buffer + '\n\n');
|
|
96
|
+
for (const event of finalEvents) {
|
|
97
|
+
onEvent(event.event, event.data);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
90
100
|
onDone?.();
|
|
91
101
|
break;
|
|
92
102
|
}
|