@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 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 } = this.parseSSEBuffer(buffer);
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
- const source = new EventSourceCtor(url);
110
- let closed = false;
111
- const close = () => {
112
- if (closed) {
113
- return;
114
- }
115
- closed = true;
116
- source.removeEventListener('results', onResults);
117
- source.removeEventListener('finished', onFinished);
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
- catch (error) {
127
- callbacks.onError?.(error);
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redseat/api",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "TypeScript API client library for interacting with Redseat servers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",