@pikku/fetch 0.12.2 → 0.12.4

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/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## 0.12.4
2
+
3
+ ### Patch Changes
4
+
5
+ - ade6f0b: `subscribeToSSE`: call `onError` when the stream closes cleanly without the caller having called `close()`. Previously a server-side connection drop (or any clean EOF before the terminal event) exited the read loop silently, leaving the caller's `runPhase` stuck at `'running'` indefinitely with no way to recover.
6
+
7
+ ## 0.12.3
8
+
9
+ ### Patch Changes
10
+
11
+ - 409ec80: feat(console): Tests page with live SSE streaming and function test harness
12
+ - `@pikku/addon-console`: add `streamFunctionTests` SSE function that runs the
13
+ cucumber/c8 test harness and streams structured per-scenario events
14
+ (scenario-start, step, scenario-done, done)
15
+ - `@pikku/console`: TestsPage live run view — renders scenario names and step
16
+ status in real time during a test run via SSE; adds `usePikkuSSE` hook and
17
+ `showRunButton` prop
18
+ - `@pikku/fetch`: add `subscribePikkuSSE` helper for typed server-sent event
19
+ streams
20
+ - `@pikku/cli`: wire SSE-returning functions through the console serialiser and
21
+ RPC wrapper so the stream route is included in generated clients
22
+
1
23
  ## 0.12.2
2
24
 
3
25
  ### Patch Changes
@@ -98,6 +98,17 @@ export declare class CorePikkuFetch {
98
98
  * @throws {Response} - Throws the response if the status code is greater than 400.
99
99
  */
100
100
  api(uri: string, method: HTTPMethod, data: any, options?: RequestInit): Promise<any>;
101
+ /**
102
+ * Opens an SSE stream to the given path and calls `handler` for each parsed
103
+ * JSON event. Returns a handle with a `close()` method that aborts the stream.
104
+ *
105
+ * @param path - Server-relative path (e.g. `/function-tests/stream`)
106
+ * @param handler - Called with each decoded JSON event
107
+ * @param onError - Called once if the stream errors (and is not already closed)
108
+ */
109
+ subscribeToSSE<T = unknown>(path: string, handler: (event: T) => void, onError?: (err: unknown) => void): {
110
+ close: () => void;
111
+ };
101
112
  /**
102
113
  * Makes a raw fetch request with the specified URI, method, and data.
103
114
  *
@@ -166,6 +166,81 @@ class CorePikkuFetch {
166
166
  }
167
167
  });
168
168
  }
169
+ /**
170
+ * Opens an SSE stream to the given path and calls `handler` for each parsed
171
+ * JSON event. Returns a handle with a `close()` method that aborts the stream.
172
+ *
173
+ * @param path - Server-relative path (e.g. `/function-tests/stream`)
174
+ * @param handler - Called with each decoded JSON event
175
+ * @param onError - Called once if the stream errors (and is not already closed)
176
+ */
177
+ subscribeToSSE(path, handler, onError) {
178
+ this.verifyServerUrlSet();
179
+ const url = path.startsWith('/')
180
+ ? `${this.options.serverUrl}${path}`
181
+ : `${this.options.serverUrl}/${path}`;
182
+ const controller = new AbortController();
183
+ let closed = false;
184
+ const run = () => __awaiter(this, void 0, void 0, function* () {
185
+ try {
186
+ const response = yield (0, pikku_fetch_js_1.corePikkuFetch)(url, null, {
187
+ method: 'GET',
188
+ mode: this.options.mode,
189
+ credentials: this.options.credentials,
190
+ headers: Object.assign(Object.assign({}, this.getHeaders()), { Accept: 'text/event-stream' }),
191
+ signal: controller.signal,
192
+ });
193
+ if (!response.ok || !response.body) {
194
+ throw new Error(`SSE request failed: ${response.status}`);
195
+ }
196
+ const reader = response.body
197
+ .pipeThrough(new TextDecoderStream())
198
+ .getReader();
199
+ let buffer = '';
200
+ while (!closed) {
201
+ const { done, value } = yield reader.read();
202
+ if (done)
203
+ break;
204
+ buffer += value;
205
+ let sep;
206
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
207
+ const raw = buffer.slice(0, sep);
208
+ buffer = buffer.slice(sep + 2);
209
+ const data = raw
210
+ .split('\n')
211
+ .filter((l) => l.startsWith('data:'))
212
+ .map((l) => l.slice(5).trimStart())
213
+ .join('\n');
214
+ if (!data)
215
+ continue;
216
+ let parsed;
217
+ try {
218
+ parsed = JSON.parse(data);
219
+ }
220
+ catch (_a) {
221
+ /* ignore malformed event */
222
+ continue;
223
+ }
224
+ handler(parsed);
225
+ }
226
+ }
227
+ // Clean EOF before caller called close() = unexpected stream termination.
228
+ if (!closed)
229
+ throw new Error('SSE stream closed unexpectedly');
230
+ }
231
+ catch (err) {
232
+ if (!closed)
233
+ onError === null || onError === void 0 ? void 0 : onError(err);
234
+ }
235
+ });
236
+ run();
237
+ return {
238
+ close: () => {
239
+ closed = true;
240
+ controller.abort();
241
+ },
242
+ };
243
+ }
169
244
  /**
170
245
  * Makes a raw fetch request with the specified URI, method, and data.
171
246
  *
@@ -98,6 +98,17 @@ export declare class CorePikkuFetch {
98
98
  * @throws {Response} - Throws the response if the status code is greater than 400.
99
99
  */
100
100
  api(uri: string, method: HTTPMethod, data: any, options?: RequestInit): Promise<any>;
101
+ /**
102
+ * Opens an SSE stream to the given path and calls `handler` for each parsed
103
+ * JSON event. Returns a handle with a `close()` method that aborts the stream.
104
+ *
105
+ * @param path - Server-relative path (e.g. `/function-tests/stream`)
106
+ * @param handler - Called with each decoded JSON event
107
+ * @param onError - Called once if the stream errors (and is not already closed)
108
+ */
109
+ subscribeToSSE<T = unknown>(path: string, handler: (event: T) => void, onError?: (err: unknown) => void): {
110
+ close: () => void;
111
+ };
101
112
  /**
102
113
  * Makes a raw fetch request with the specified URI, method, and data.
103
114
  *
@@ -144,6 +144,81 @@ export class CorePikkuFetch {
144
144
  return;
145
145
  }
146
146
  }
147
+ /**
148
+ * Opens an SSE stream to the given path and calls `handler` for each parsed
149
+ * JSON event. Returns a handle with a `close()` method that aborts the stream.
150
+ *
151
+ * @param path - Server-relative path (e.g. `/function-tests/stream`)
152
+ * @param handler - Called with each decoded JSON event
153
+ * @param onError - Called once if the stream errors (and is not already closed)
154
+ */
155
+ subscribeToSSE(path, handler, onError) {
156
+ this.verifyServerUrlSet();
157
+ const url = path.startsWith('/')
158
+ ? `${this.options.serverUrl}${path}`
159
+ : `${this.options.serverUrl}/${path}`;
160
+ const controller = new AbortController();
161
+ let closed = false;
162
+ const run = async () => {
163
+ try {
164
+ const response = await corePikkuFetch(url, null, {
165
+ method: 'GET',
166
+ mode: this.options.mode,
167
+ credentials: this.options.credentials,
168
+ headers: { ...this.getHeaders(), Accept: 'text/event-stream' },
169
+ signal: controller.signal,
170
+ });
171
+ if (!response.ok || !response.body) {
172
+ throw new Error(`SSE request failed: ${response.status}`);
173
+ }
174
+ const reader = response.body
175
+ .pipeThrough(new TextDecoderStream())
176
+ .getReader();
177
+ let buffer = '';
178
+ while (!closed) {
179
+ const { done, value } = await reader.read();
180
+ if (done)
181
+ break;
182
+ buffer += value;
183
+ let sep;
184
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
185
+ const raw = buffer.slice(0, sep);
186
+ buffer = buffer.slice(sep + 2);
187
+ const data = raw
188
+ .split('\n')
189
+ .filter((l) => l.startsWith('data:'))
190
+ .map((l) => l.slice(5).trimStart())
191
+ .join('\n');
192
+ if (!data)
193
+ continue;
194
+ let parsed;
195
+ try {
196
+ parsed = JSON.parse(data);
197
+ }
198
+ catch {
199
+ /* ignore malformed event */
200
+ continue;
201
+ }
202
+ handler(parsed);
203
+ }
204
+ }
205
+ // Clean EOF before caller called close() = unexpected stream termination.
206
+ if (!closed)
207
+ throw new Error('SSE stream closed unexpectedly');
208
+ }
209
+ catch (err) {
210
+ if (!closed)
211
+ onError?.(err);
212
+ }
213
+ };
214
+ run();
215
+ return {
216
+ close: () => {
217
+ closed = true;
218
+ controller.abort();
219
+ },
220
+ };
221
+ }
147
222
  /**
148
223
  * Makes a raw fetch request with the specified URI, method, and data.
149
224
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/fetch",
3
- "version": "0.12.2",
3
+ "version": "0.12.4",
4
4
  "author": "yasser.fadl@gmail.com",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -193,6 +193,83 @@ export class CorePikkuFetch {
193
193
  }
194
194
  }
195
195
 
196
+ /**
197
+ * Opens an SSE stream to the given path and calls `handler` for each parsed
198
+ * JSON event. Returns a handle with a `close()` method that aborts the stream.
199
+ *
200
+ * @param path - Server-relative path (e.g. `/function-tests/stream`)
201
+ * @param handler - Called with each decoded JSON event
202
+ * @param onError - Called once if the stream errors (and is not already closed)
203
+ */
204
+ public subscribeToSSE<T = unknown>(
205
+ path: string,
206
+ handler: (event: T) => void,
207
+ onError?: (err: unknown) => void
208
+ ): { close: () => void } {
209
+ this.verifyServerUrlSet()
210
+ const url = path.startsWith('/')
211
+ ? `${this.options.serverUrl}${path}`
212
+ : `${this.options.serverUrl}/${path}`
213
+
214
+ const controller = new AbortController()
215
+ let closed = false
216
+
217
+ const run = async () => {
218
+ try {
219
+ const response = await corePikkuFetch(url, null, {
220
+ method: 'GET',
221
+ mode: this.options.mode,
222
+ credentials: this.options.credentials,
223
+ headers: { ...this.getHeaders(), Accept: 'text/event-stream' },
224
+ signal: controller.signal,
225
+ })
226
+ if (!response.ok || !response.body) {
227
+ throw new Error(`SSE request failed: ${response.status}`)
228
+ }
229
+ const reader = response.body
230
+ .pipeThrough(new TextDecoderStream())
231
+ .getReader()
232
+ let buffer = ''
233
+ while (!closed) {
234
+ const { done, value } = await reader.read()
235
+ if (done) break
236
+ buffer += value
237
+ let sep: number
238
+ while ((sep = buffer.indexOf('\n\n')) !== -1) {
239
+ const raw = buffer.slice(0, sep)
240
+ buffer = buffer.slice(sep + 2)
241
+ const data = raw
242
+ .split('\n')
243
+ .filter((l) => l.startsWith('data:'))
244
+ .map((l) => l.slice(5).trimStart())
245
+ .join('\n')
246
+ if (!data) continue
247
+ let parsed: T
248
+ try {
249
+ parsed = JSON.parse(data) as T
250
+ } catch {
251
+ /* ignore malformed event */
252
+ continue
253
+ }
254
+ handler(parsed)
255
+ }
256
+ }
257
+ // Clean EOF before caller called close() = unexpected stream termination.
258
+ if (!closed) throw new Error('SSE stream closed unexpectedly')
259
+ } catch (err) {
260
+ if (!closed) onError?.(err)
261
+ }
262
+ }
263
+
264
+ run()
265
+ return {
266
+ close: () => {
267
+ closed = true
268
+ controller.abort()
269
+ },
270
+ }
271
+ }
272
+
196
273
  /**
197
274
  * Makes a raw fetch request with the specified URI, method, and data.
198
275
  *