@webqit/fetch-plus 0.1.31 → 0.1.32

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/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "homepage": "https://fetch-plus.netlify.app/",
13
13
  "icon": "https://webqit.io/icon.svg",
14
- "version": "0.1.31",
14
+ "version": "0.1.32",
15
15
  "license": "MIT",
16
16
  "repository": {
17
17
  "type": "git",
@@ -1,7 +1,7 @@
1
1
  import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
2
2
  import { Observer, ListenerRegistry, Descriptor } from '@webqit/observer';
3
3
  import { BroadcastChannelPlus, WebSocketPort, MessagePortPlus } from '@webqit/port-plus';
4
- import { isTypeStream, _meta, _wq } from './messageParserMixin.js';
4
+ import { isTypeStream, _meta, _wq, isAsyncIterable, isGenerator } from './messageParserMixin.js';
5
5
  import { ResponsePlus } from './ResponsePlus.js';
6
6
 
7
7
  export class LiveResponse extends EventTarget {
@@ -461,10 +461,13 @@ export class LiveResponse extends EventTarget {
461
461
  throw new Error(`frameClosure is not supported for responses.`);
462
462
  }
463
463
  frame.donePromise = execReplaceWithResponse(frame, body, frameOptions);
464
- } else if (isGenerator(body)) {
464
+ } else if (isAsyncIterable(body) || isGenerator(body)) {
465
465
  if (frameClosure) {
466
466
  throw new Error(`frameClosure is not supported for generators.`);
467
467
  }
468
+ if (!isGenerator(body)) {
469
+ body = body[Symbol.asyncIterator]();
470
+ }
468
471
  frame.donePromise = execReplaceWithGenerator(frame, body, frameOptions);
469
472
  } else if (body instanceof LiveProgramHandleX) {
470
473
  if (frameClosure) {
@@ -575,12 +578,6 @@ export class LiveResponse extends EventTarget {
575
578
  }
576
579
  }
577
580
 
578
- export const isGenerator = (obj) => {
579
- return typeof obj?.next === 'function' &&
580
- typeof obj?.throw === 'function' &&
581
- typeof obj?.return === 'function';
582
- };
583
-
584
581
  export class ReplaceEvent extends Event {
585
582
 
586
583
  [Symbol.toStringTag] = 'ReplaceEvent';
@@ -24,6 +24,14 @@ export function messageParserMixin(superClass) {
24
24
 
25
25
  // Process body
26
26
  let body = httpMessageInit.body;
27
+
28
+ if (isAsyncIterable(body) || isGenerator(body)) {
29
+ body = asyncIterableToStream(body);
30
+ const type = 'ReadableStream';
31
+ headers['content-type'] ??= 'application/octet-stream';
32
+ return { body, headers: new Headers(headers), $type: type };
33
+ }
34
+
27
35
  let type = [null, undefined].includes(body) ? null : dataType(body);
28
36
 
29
37
  // Binary bodies
@@ -203,6 +211,8 @@ export function dataType(value) {
203
211
  return null;
204
212
  }
205
213
 
214
+ // --------------
215
+
206
216
  export function isTypeReadable(obj) {
207
217
  return (
208
218
  obj !== null &&
@@ -217,3 +227,142 @@ export function isTypeStream(obj) {
217
227
  return obj instanceof ReadableStream
218
228
  || isTypeReadable(obj);
219
229
  }
230
+
231
+ // --------------
232
+
233
+ export const isGenerator = (obj) => {
234
+ return typeof obj?.next === 'function'
235
+ //&& typeof obj?.throw === 'function'
236
+ //&& typeof obj?.return === 'function';
237
+ };
238
+
239
+ export function isAsyncIterable(obj) {
240
+ return (
241
+ obj !== null &&
242
+ typeof obj === 'object' &&
243
+ typeof obj[Symbol.asyncIterator] === 'function'
244
+ );
245
+ }
246
+
247
+ export function asyncIterableToStream(iterable) {
248
+ if (!isAsyncIterable(iterable) && !isGenerator(body)) {
249
+ throw new TypeError('Body must be an async iterable.');
250
+ }
251
+
252
+ const iterator = isGenerator(iterable) ? iterable : iterable[Symbol.asyncIterator]();
253
+ const encoder = new TextEncoder();
254
+ let finished = false;
255
+
256
+ const closeIterator = async (reason) => {
257
+ if (finished) return;
258
+ finished = true;
259
+
260
+ if (typeof iterator.return === 'function') {
261
+ try {
262
+ await iterator.return(reason);
263
+ } catch { }
264
+ }
265
+ };
266
+
267
+ const encodeChunk = (value) => {
268
+ if (value == null) return null;
269
+
270
+ // Binary passthrough
271
+ if (value instanceof Uint8Array) {
272
+ return value;
273
+ }
274
+
275
+ // Text passthrough
276
+ if (typeof value === 'string') {
277
+ return encoder.encode(value);
278
+ }
279
+
280
+ // JSON (objects, arrays, numbers, booleans)
281
+ if (
282
+ typeof value === 'object' ||
283
+ typeof value === 'number' ||
284
+ typeof value === 'boolean'
285
+ ) {
286
+ return encoder.encode(JSON.stringify(value) + '\n');
287
+ }
288
+
289
+ throw new TypeError(
290
+ `Unsupported chunk type in async iterable: ${typeof value}`
291
+ );
292
+ };
293
+
294
+ return new ReadableStream({
295
+ async pull(controller) {
296
+ try {
297
+ const { value, done } = await iterator.next();
298
+
299
+ if (done) {
300
+ await closeIterator();
301
+ controller.close();
302
+ return;
303
+ }
304
+
305
+ const chunk = encodeChunk(value);
306
+ if (chunk) controller.enqueue(chunk);
307
+
308
+ } catch (err) {
309
+ await closeIterator();
310
+ controller.error(err);
311
+ }
312
+ },
313
+
314
+ async cancel(reason) {
315
+ await closeIterator(reason);
316
+ }
317
+ });
318
+ }
319
+
320
+ function streamToAsyncIterable(stream, { parse = null } = {}) {
321
+ const reader = stream.getReader();
322
+ const decoder = new TextDecoder();
323
+
324
+ let finished = false;
325
+ let buffer = '';
326
+
327
+ const close = async () => {
328
+ if (finished) return;
329
+ finished = true;
330
+ try {
331
+ await reader.cancel();
332
+ } catch { }
333
+ reader.releaseLock();
334
+ };
335
+
336
+ return {
337
+ async *[Symbol.asyncIterator]() {
338
+ try {
339
+ while (true) {
340
+ const { value, done } = await reader.read();
341
+ if (done) break;
342
+
343
+ if (parse === 'ndjson') {
344
+ buffer += decoder.decode(value, { stream: true });
345
+
346
+ let lines = buffer.split('\n');
347
+ buffer = lines.pop(); // incomplete fragment
348
+
349
+ for (const line of lines) {
350
+ if (line.trim()) yield JSON.parse(line);
351
+ }
352
+
353
+ continue;
354
+ }
355
+
356
+ yield value;
357
+ }
358
+
359
+ if (parse === 'ndjson' && buffer.trim()) {
360
+ yield JSON.parse(buffer);
361
+ }
362
+
363
+ } finally {
364
+ await close();
365
+ }
366
+ }
367
+ };
368
+ }