@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
package/src/core-pikku-fetch.ts
CHANGED
|
@@ -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
|
*
|