@redseat/api 0.4.2 → 0.4.3
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/client.d.ts +0 -4
- package/dist/client.js +16 -67
- package/dist/library.js +13 -36
- package/dist/sse-fetch.d.ts +28 -0
- package/dist/sse-fetch.js +112 -0
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -152,10 +152,6 @@ export declare class RedseatClient {
|
|
|
152
152
|
* Processes the SSE stream and emits events
|
|
153
153
|
*/
|
|
154
154
|
private processSSEStream;
|
|
155
|
-
/**
|
|
156
|
-
* Parses the SSE buffer and extracts complete events
|
|
157
|
-
*/
|
|
158
|
-
private parseSSEBuffer;
|
|
159
155
|
/**
|
|
160
156
|
* Handles SSE errors and triggers reconnection if appropriate
|
|
161
157
|
*/
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
2
|
import { BehaviorSubject, Subject, filter, map } from 'rxjs';
|
|
3
3
|
import { fetchServerToken } from './auth.js';
|
|
4
|
+
import { parseSSEBuffer } from './sse-fetch.js';
|
|
4
5
|
export class RedseatClient {
|
|
5
6
|
/**
|
|
6
7
|
* Creates a typed observable for a specific SSE event type.
|
|
@@ -607,8 +608,22 @@ export class RedseatClient {
|
|
|
607
608
|
// Data received - reset activity timeout
|
|
608
609
|
this.resetActivityTimeout();
|
|
609
610
|
buffer += decoder.decode(value, { stream: true });
|
|
610
|
-
const { events, remainingBuffer } =
|
|
611
|
+
const { events: rawEvents, remainingBuffer } = parseSSEBuffer(buffer);
|
|
611
612
|
buffer = remainingBuffer;
|
|
613
|
+
const events = rawEvents.map(raw => {
|
|
614
|
+
let data = raw.data;
|
|
615
|
+
try {
|
|
616
|
+
data = JSON.parse(raw.data);
|
|
617
|
+
}
|
|
618
|
+
catch {
|
|
619
|
+
this._sseError.next({
|
|
620
|
+
type: 'parse',
|
|
621
|
+
message: `Failed to parse SSE data: ${raw.data}`,
|
|
622
|
+
timestamp: Date.now()
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return { event: raw.event, data, id: raw.id, retry: raw.retry };
|
|
626
|
+
});
|
|
612
627
|
for (const event of events) {
|
|
613
628
|
//console.log("event process", JSON.stringify(event))
|
|
614
629
|
this._sseEvents.next(event);
|
|
@@ -627,72 +642,6 @@ export class RedseatClient {
|
|
|
627
642
|
reader.releaseLock();
|
|
628
643
|
}
|
|
629
644
|
}
|
|
630
|
-
/**
|
|
631
|
-
* Parses the SSE buffer and extracts complete events
|
|
632
|
-
*/
|
|
633
|
-
parseSSEBuffer(buffer) {
|
|
634
|
-
const events = [];
|
|
635
|
-
const lines = buffer.split('\n');
|
|
636
|
-
let currentEvent = {};
|
|
637
|
-
let processedUpTo = 0;
|
|
638
|
-
for (let i = 0; i < lines.length; i++) {
|
|
639
|
-
const line = lines[i];
|
|
640
|
-
// Check if this is an incomplete line (last line without newline)
|
|
641
|
-
if (i === lines.length - 1 && !buffer.endsWith('\n')) {
|
|
642
|
-
break;
|
|
643
|
-
}
|
|
644
|
-
processedUpTo += line.length + 1; // +1 for the newline
|
|
645
|
-
if (line === '') {
|
|
646
|
-
// Empty line marks end of event
|
|
647
|
-
if (currentEvent.event && currentEvent.data !== undefined) {
|
|
648
|
-
events.push(currentEvent);
|
|
649
|
-
}
|
|
650
|
-
currentEvent = {};
|
|
651
|
-
continue;
|
|
652
|
-
}
|
|
653
|
-
if (line.startsWith(':')) {
|
|
654
|
-
// Comment, ignore
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
const colonIndex = line.indexOf(':');
|
|
658
|
-
if (colonIndex === -1) {
|
|
659
|
-
continue;
|
|
660
|
-
}
|
|
661
|
-
const field = line.slice(0, colonIndex);
|
|
662
|
-
// Value starts after colon, skip optional space after colon
|
|
663
|
-
let value = line.slice(colonIndex + 1);
|
|
664
|
-
if (value.startsWith(' ')) {
|
|
665
|
-
value = value.slice(1);
|
|
666
|
-
}
|
|
667
|
-
switch (field) {
|
|
668
|
-
case 'event':
|
|
669
|
-
currentEvent.event = value;
|
|
670
|
-
break;
|
|
671
|
-
case 'data':
|
|
672
|
-
try {
|
|
673
|
-
currentEvent.data = JSON.parse(value);
|
|
674
|
-
}
|
|
675
|
-
catch {
|
|
676
|
-
this._sseError.next({
|
|
677
|
-
type: 'parse',
|
|
678
|
-
message: `Failed to parse SSE data: ${value}`,
|
|
679
|
-
timestamp: Date.now()
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
break;
|
|
683
|
-
case 'id':
|
|
684
|
-
currentEvent.id = value;
|
|
685
|
-
break;
|
|
686
|
-
case 'retry':
|
|
687
|
-
currentEvent.retry = parseInt(value, 10);
|
|
688
|
-
break;
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
return {
|
|
692
|
-
events,
|
|
693
|
-
remainingBuffer: buffer.slice(processedUpTo)
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
645
|
/**
|
|
697
646
|
* Handles SSE errors and triggers reconnection if appropriate
|
|
698
647
|
*/
|
package/dist/library.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { filter, EMPTY } from 'rxjs';
|
|
2
2
|
import { deriveKey, encryptText as encryptTextUtil, decryptText as decryptTextUtil, encryptBuffer, decryptBuffer, encryptFile as encryptFileUtil, decryptFile as decryptFileUtil, decryptFileThumb, encryptFilename as encryptFilenameUtil, getRandomIV as getRandomIVUtil } from './encryption.js';
|
|
3
3
|
import { uint8ArrayFromBase64 } from './crypto.js';
|
|
4
|
+
import { openFetchSSEStream } from './sse-fetch.js';
|
|
4
5
|
export class LibraryApi {
|
|
5
6
|
constructor(client, libraryId, library) {
|
|
6
7
|
this.client = client;
|
|
@@ -97,48 +98,24 @@ export class LibraryApi {
|
|
|
97
98
|
return `/libraries/${this.libraryId}${path}`;
|
|
98
99
|
}
|
|
99
100
|
openSearchStream(path, params, callbacks) {
|
|
100
|
-
const EventSourceCtor = globalThis.EventSource;
|
|
101
|
-
if (!EventSourceCtor) {
|
|
102
|
-
callbacks.onError?.(new Error('EventSource is not available in this runtime.'));
|
|
103
|
-
return () => undefined;
|
|
104
|
-
}
|
|
105
101
|
const url = this.client.getFullUrl(path, {
|
|
106
102
|
...params,
|
|
107
103
|
token: this.client.getAuthToken()
|
|
108
104
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
source.onerror = null;
|
|
119
|
-
source.close();
|
|
120
|
-
};
|
|
121
|
-
const onResults = (event) => {
|
|
122
|
-
try {
|
|
123
|
-
const payload = JSON.parse(event.data);
|
|
124
|
-
callbacks.onResults?.(payload);
|
|
105
|
+
return openFetchSSEStream(url, (eventType, data) => {
|
|
106
|
+
if (eventType === 'results') {
|
|
107
|
+
try {
|
|
108
|
+
const payload = JSON.parse(data);
|
|
109
|
+
callbacks.onResults?.(payload);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
callbacks.onError?.(error);
|
|
113
|
+
}
|
|
125
114
|
}
|
|
126
|
-
|
|
127
|
-
callbacks.
|
|
128
|
-
close();
|
|
115
|
+
else if (eventType === 'finished') {
|
|
116
|
+
callbacks.onFinished?.();
|
|
129
117
|
}
|
|
130
|
-
};
|
|
131
|
-
const onFinished = () => {
|
|
132
|
-
callbacks.onFinished?.();
|
|
133
|
-
close();
|
|
134
|
-
};
|
|
135
|
-
source.addEventListener('results', onResults);
|
|
136
|
-
source.addEventListener('finished', onFinished);
|
|
137
|
-
source.onerror = (error) => {
|
|
138
|
-
callbacks.onError?.(error);
|
|
139
|
-
close();
|
|
140
|
-
};
|
|
141
|
-
return close;
|
|
118
|
+
}, () => callbacks.onFinished?.(), (error) => callbacks.onError?.(error));
|
|
142
119
|
}
|
|
143
120
|
async getTags(query) {
|
|
144
121
|
const params = {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw SSE event as received from the server (data is unparsed string).
|
|
3
|
+
*/
|
|
4
|
+
export interface RawSSEEvent {
|
|
5
|
+
event: string;
|
|
6
|
+
data: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
retry?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
|
|
12
|
+
* Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseSSEBuffer(buffer: string): {
|
|
15
|
+
events: RawSSEEvent[];
|
|
16
|
+
remainingBuffer: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Opens a one-shot fetch-based SSE stream (no reconnection).
|
|
20
|
+
* Suitable for search streams where a token is embedded in the URL.
|
|
21
|
+
*
|
|
22
|
+
* @param url - Full URL including auth token as query param
|
|
23
|
+
* @param onEvent - Called for each complete event with (eventType, rawData)
|
|
24
|
+
* @param onDone - Called when the stream ends normally
|
|
25
|
+
* @param onError - Called on network or parse errors
|
|
26
|
+
* @returns A cancel function that aborts the stream
|
|
27
|
+
*/
|
|
28
|
+
export declare function openFetchSSEStream(url: string, onEvent: (eventType: string, data: string) => void, onDone?: () => void, onError?: (error: unknown) => void): () => void;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a raw SSE buffer into discrete events, returning any incomplete trailing data.
|
|
3
|
+
* Follows the SSE spec: events are separated by blank lines, fields are `field: value`.
|
|
4
|
+
*/
|
|
5
|
+
export function parseSSEBuffer(buffer) {
|
|
6
|
+
const events = [];
|
|
7
|
+
const lines = buffer.split('\n');
|
|
8
|
+
let processedUpTo = 0;
|
|
9
|
+
let eventType = '';
|
|
10
|
+
let eventData = '';
|
|
11
|
+
let eventId;
|
|
12
|
+
let eventRetry;
|
|
13
|
+
for (let i = 0; i < lines.length; i++) {
|
|
14
|
+
const line = lines[i];
|
|
15
|
+
// Incomplete last line — keep in buffer
|
|
16
|
+
if (i === lines.length - 1 && !buffer.endsWith('\n')) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
processedUpTo += line.length + 1; // +1 for '\n'
|
|
20
|
+
if (line === '') {
|
|
21
|
+
// Blank line = end of event
|
|
22
|
+
if (eventType) {
|
|
23
|
+
events.push({ event: eventType, data: eventData, id: eventId, retry: eventRetry });
|
|
24
|
+
}
|
|
25
|
+
eventType = '';
|
|
26
|
+
eventData = '';
|
|
27
|
+
eventId = undefined;
|
|
28
|
+
eventRetry = undefined;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (line.startsWith(':'))
|
|
32
|
+
continue; // SSE comment
|
|
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 = value;
|
|
46
|
+
break;
|
|
47
|
+
case 'id':
|
|
48
|
+
eventId = value;
|
|
49
|
+
break;
|
|
50
|
+
case 'retry':
|
|
51
|
+
eventRetry = parseInt(value, 10);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { events, remainingBuffer: buffer.slice(processedUpTo) };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Opens a one-shot fetch-based SSE stream (no reconnection).
|
|
59
|
+
* Suitable for search streams where a token is embedded in the URL.
|
|
60
|
+
*
|
|
61
|
+
* @param url - Full URL including auth token as query param
|
|
62
|
+
* @param onEvent - Called for each complete event with (eventType, rawData)
|
|
63
|
+
* @param onDone - Called when the stream ends normally
|
|
64
|
+
* @param onError - Called on network or parse errors
|
|
65
|
+
* @returns A cancel function that aborts the stream
|
|
66
|
+
*/
|
|
67
|
+
export function openFetchSSEStream(url, onEvent, onDone, onError) {
|
|
68
|
+
let aborted = false;
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const run = async () => {
|
|
71
|
+
try {
|
|
72
|
+
const response = await fetch(url, {
|
|
73
|
+
signal: controller.signal,
|
|
74
|
+
headers: { Accept: 'text/event-stream' }
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
onError?.(new Error(`HTTP error: ${response.status}`));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const reader = response.body?.getReader();
|
|
81
|
+
if (!reader) {
|
|
82
|
+
onError?.(new Error('No response body'));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const decoder = new TextDecoder();
|
|
86
|
+
let buffer = '';
|
|
87
|
+
while (true) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
if (done) {
|
|
90
|
+
onDone?.();
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
buffer += decoder.decode(value, { stream: true });
|
|
94
|
+
const { events, remainingBuffer } = parseSSEBuffer(buffer);
|
|
95
|
+
buffer = remainingBuffer;
|
|
96
|
+
for (const event of events) {
|
|
97
|
+
onEvent(event.event, event.data);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!aborted) {
|
|
103
|
+
onError?.(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
run();
|
|
108
|
+
return () => {
|
|
109
|
+
aborted = true;
|
|
110
|
+
controller.abort();
|
|
111
|
+
};
|
|
112
|
+
}
|