@dev-crew-berlin/enter-js-utils 0.95.2 → 0.97.2

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.
@@ -36,6 +36,8 @@ export default class APIBase {
36
36
  onLogout: (reason?: string) => void;
37
37
  });
38
38
  private static fetchResult;
39
+ private buildHeaders;
40
+ private fetchResponse;
39
41
  /**
40
42
  * @category Auth
41
43
  */
@@ -54,6 +56,7 @@ export default class APIBase {
54
56
  expires: Date;
55
57
  }>>;
56
58
  private fetchFromEnter;
59
+ protected stream<T>(endpoint: string, options?: FetchOptions): AsyncGenerator<T>;
57
60
  protected get<Path extends keyof paths>(endpoint: Path, options: FetchOptions): Promise<APIResult<ResponseType<Path, 'get'>>>;
58
61
  protected post<Path extends keyof paths>(endpoint: Path, data: RequestBody<Path, 'post'>, options: FetchOptions): Promise<APIResult<ResponseType<Path, 'post'>>>;
59
62
  protected put<Path extends keyof paths>(endpoint: Path, data: RequestBody<Path, 'put'>, options: FetchOptions): Promise<APIResult<ResponseType<Path, 'put'>>>;
@@ -1,4 +1,5 @@
1
1
  import jws from 'jws';
2
+ import { EventSourceParserStream } from 'eventsource-parser/stream';
2
3
  import { failure, success } from '../lib';
3
4
  export default class APIBase {
4
5
  constructor(args) {
@@ -16,6 +17,53 @@ export default class APIBase {
16
17
  return failure(e);
17
18
  }
18
19
  }
20
+ buildHeaders(extraHeaders = {}) {
21
+ return {
22
+ Authorization: `bearer ${this.credentials.accessToken}`,
23
+ 'Content-Type': 'application/json',
24
+ 'X-Enter-Device-Name': this.deviceName,
25
+ 'X-Enter-Requester-Id': this.requesterId,
26
+ ...extraHeaders
27
+ };
28
+ }
29
+ async fetchResponse(endpoint, init) {
30
+ const res = await APIBase.fetchResult(`${this.credentials.url}${endpoint}`, init);
31
+ if (!res.success) {
32
+ return failure([{
33
+ type: 'network-error',
34
+ message: res.error.message,
35
+ endpoint
36
+ }]);
37
+ }
38
+ if (res.data.status === 401) {
39
+ if (this.isLoggedIn) this.logout();
40
+ return failure([{
41
+ type: 'http-error',
42
+ statusCode: 401,
43
+ endpoint,
44
+ message: res.data.statusText
45
+ }]);
46
+ }
47
+ if (res.data.status >= 300) {
48
+ try {
49
+ const error = await res.data.json();
50
+ return failure([{
51
+ type: 'http-error',
52
+ statusCode: res.data.status,
53
+ message: error.error ?? res.data.statusText,
54
+ endpoint
55
+ }]);
56
+ } catch {
57
+ return failure([{
58
+ type: 'http-error',
59
+ statusCode: res.data.status,
60
+ message: `invalid response from backend: ${res.data.status} (${res.data.statusText}) for: ${endpoint}`,
61
+ endpoint
62
+ }]);
63
+ }
64
+ }
65
+ return success(res.data);
66
+ }
19
67
 
20
68
  /**
21
69
  * @category Auth
@@ -74,58 +122,52 @@ export default class APIBase {
74
122
  }
75
123
  async fetchFromEnter(endpoint, method, data, options) {
76
124
  const body = data !== null ? JSON.stringify(data) : null;
77
- const headers = {
78
- Authorization: `bearer ${this.credentials.accessToken}`,
79
- 'Content-Type': 'application/json',
80
- 'X-Enter-Device-Name': this.deviceName,
81
- 'X-Enter-Requester-Id': this.requesterId
82
- };
83
- const res = await APIBase.fetchResult(`${this.credentials.url}${endpoint}`, {
125
+ const res = await this.fetchResponse(endpoint, {
84
126
  method: method.toUpperCase(),
85
- headers,
127
+ headers: this.buildHeaders(),
86
128
  body,
87
129
  ...options
88
130
  });
89
- if (!res.success) {
90
- return failure([{
91
- type: 'network-error',
92
- message: res.error.message,
93
- endpoint
94
- }]);
95
- }
96
- if (res.data.status === 401) {
97
- if (this.isLoggedIn) this.logout();
98
- return failure([{
99
- type: 'http-error',
100
- statusCode: 401,
101
- endpoint,
102
- message: res.data.statusText
103
- }]);
104
- }
105
- if (res.data.status >= 300) {
106
- try {
107
- const error = await res.data.json();
108
- return failure([{
109
- type: 'http-error',
110
- statusCode: res.data.status,
111
- message: error.error ?? res.data.statusText,
112
- endpoint
113
- }]);
114
- } catch (e) {
115
- return failure([{
116
- type: 'http-error',
117
- statusCode: res.data.status,
118
- message: `invalid response from backend: ${res.data.status} (${res.data.statusText}) for: ${endpoint}`,
119
- endpoint
120
- }]);
121
- }
122
- }
131
+ if (!res.success) return res;
123
132
 
124
133
  // HTTP status 204 means no content so we can't parse it as json
125
134
  if (res.data.status === 204) return success(null);
126
135
  const json = await res.data.json();
127
136
  return success(json);
128
137
  }
138
+ async *stream(endpoint, options = {}) {
139
+ const res = await this.fetchResponse(endpoint, {
140
+ method: 'GET',
141
+ headers: this.buildHeaders({
142
+ Accept: 'text/event-stream'
143
+ }),
144
+ ...options
145
+ });
146
+ if (!res.success) {
147
+ throw new Error(res.error[0]?.message ?? 'Unknown error');
148
+ }
149
+ if (!res.data.body) {
150
+ throw new Error('Response body is null');
151
+ }
152
+ const reader = res.data.body.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream()).getReader();
153
+ try {
154
+ while (true) {
155
+ const {
156
+ done,
157
+ value: event
158
+ } = await reader.read();
159
+ if (done) break;
160
+ if (event.data === '[DONE]') return;
161
+ try {
162
+ yield JSON.parse(event.data);
163
+ } catch {
164
+ console.warn('[SSE] Failed to parse event data:', event.data);
165
+ }
166
+ }
167
+ } finally {
168
+ reader.releaseLock();
169
+ }
170
+ }
129
171
  async get(endpoint, options) {
130
172
  const rawResponse = await this.fetchFromEnter(endpoint, 'get', undefined, options);
131
173
  if (!rawResponse.success) return rawResponse;
@@ -161,5 +161,20 @@ declare class API extends APIBase {
161
161
  * @category Events
162
162
  */
163
163
  createEvents(args: Event[], options?: FetchOptions): Promise<APIResult<null>>;
164
+ /**
165
+ * Subscribe to the SSE event stream. Yields events as they arrive.
166
+ * Use `for await` to consume events, and `break` or `return` to unsubscribe.
167
+ *
168
+ * @example
169
+ * for await (const event of api.subscribeToEvents({ instanceName: 'my-instance' })) {
170
+ * console.log(event)
171
+ * }
172
+ *
173
+ * @category Events
174
+ */
175
+ subscribeToEvents(args: {
176
+ instanceName: string;
177
+ eventType?: string[];
178
+ }, options?: FetchOptions): AsyncGenerator<Event>;
164
179
  }
165
180
  export default API;
@@ -221,5 +221,23 @@ class API extends APIBase {
221
221
  async createEvents(args, options = {}) {
222
222
  return this.post(`/events`, args, options);
223
223
  }
224
+
225
+ /**
226
+ * Subscribe to the SSE event stream. Yields events as they arrive.
227
+ * Use `for await` to consume events, and `break` or `return` to unsubscribe.
228
+ *
229
+ * @example
230
+ * for await (const event of api.subscribeToEvents({ instanceName: 'my-instance' })) {
231
+ * console.log(event)
232
+ * }
233
+ *
234
+ * @category Events
235
+ */
236
+ async *subscribeToEvents(args, options = {}) {
237
+ const query = new URLSearchParams();
238
+ query.append('instance', args.instanceName);
239
+ args.eventType?.forEach(t => query.append('event_type', t));
240
+ yield* this.stream(`/events/stream?${query.toString()}`, options);
241
+ }
224
242
  }
225
243
  export default API;