1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

feat(fetch): accept async iterables for body (#26882)

Reland of #24623, but with a fix for `String` objects.

Co-authored-by: crowlkats <crowlkats@toaxl.com>
This commit is contained in:
Luca Casonato 2024-11-15 15:54:28 +01:00 committed by GitHub
parent 3f26310728
commit b8cf259924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 251 additions and 51 deletions

View file

@ -15,6 +15,7 @@ import { core, primordials } from "ext:core/mod.js";
const { const {
isAnyArrayBuffer, isAnyArrayBuffer,
isArrayBuffer, isArrayBuffer,
isStringObject,
} = core; } = core;
const { const {
ArrayBufferIsView, ArrayBufferIsView,
@ -466,6 +467,8 @@ function extractBody(object) {
if (object.locked || isReadableStreamDisturbed(object)) { if (object.locked || isReadableStreamDisturbed(object)) {
throw new TypeError("ReadableStream is locked or disturbed"); throw new TypeError("ReadableStream is locked or disturbed");
} }
} else if (object[webidl.AsyncIterable] === webidl.AsyncIterable) {
stream = ReadableStream.from(object.open());
} }
if (typeof source === "string") { if (typeof source === "string") {
// WARNING: this deviates from spec (expects length to be set) // WARNING: this deviates from spec (expects length to be set)
@ -483,6 +486,9 @@ function extractBody(object) {
return { body, contentType }; return { body, contentType };
} }
webidl.converters["async iterable<Uint8Array>"] = webidl
.createAsyncIterableConverter(webidl.converters.Uint8Array);
webidl.converters["BodyInit_DOMString"] = (V, prefix, context, opts) => { webidl.converters["BodyInit_DOMString"] = (V, prefix, context, opts) => {
// Union for (ReadableStream or Blob or ArrayBufferView or ArrayBuffer or FormData or URLSearchParams or USVString) // Union for (ReadableStream or Blob or ArrayBufferView or ArrayBuffer or FormData or URLSearchParams or USVString)
if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, V)) { if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, V)) {
@ -501,6 +507,14 @@ webidl.converters["BodyInit_DOMString"] = (V, prefix, context, opts) => {
if (ArrayBufferIsView(V)) { if (ArrayBufferIsView(V)) {
return webidl.converters["ArrayBufferView"](V, prefix, context, opts); return webidl.converters["ArrayBufferView"](V, prefix, context, opts);
} }
if (webidl.isAsyncIterable(V) && !isStringObject(V)) {
return webidl.converters["async iterable<Uint8Array>"](
V,
prefix,
context,
opts,
);
}
} }
// BodyInit conversion is passed to extractBody(), which calls core.encode(). // BodyInit conversion is passed to extractBody(), which calls core.encode().
// core.encode() will UTF-8 encode strings with replacement, being equivalent to the USV normalization. // core.encode() will UTF-8 encode strings with replacement, being equivalent to the USV normalization.

View file

@ -163,6 +163,8 @@ type BodyInit =
| FormData | FormData
| URLSearchParams | URLSearchParams
| ReadableStream<Uint8Array> | ReadableStream<Uint8Array>
| Iterable<Uint8Array>
| AsyncIterable<Uint8Array>
| string; | string;
/** @category Fetch */ /** @category Fetch */
type RequestDestination = type RequestDestination =

View file

@ -70,7 +70,6 @@ const {
String, String,
Symbol, Symbol,
SymbolAsyncIterator, SymbolAsyncIterator,
SymbolIterator,
SymbolFor, SymbolFor,
TypeError, TypeError,
TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetBuffer,
@ -5084,34 +5083,6 @@ function initializeCountSizeFunction(globalObject) {
WeakMapPrototypeSet(countSizeFunctionWeakMap, globalObject, size); WeakMapPrototypeSet(countSizeFunctionWeakMap, globalObject, size);
} }
// Ref: https://tc39.es/ecma262/#sec-getiterator
function getAsyncOrSyncIterator(obj) {
let iterator;
if (obj[SymbolAsyncIterator] != null) {
iterator = obj[SymbolAsyncIterator]();
if (!isObject(iterator)) {
throw new TypeError(
"[Symbol.asyncIterator] returned a non-object value",
);
}
} else if (obj[SymbolIterator] != null) {
iterator = obj[SymbolIterator]();
if (!isObject(iterator)) {
throw new TypeError("[Symbol.iterator] returned a non-object value");
}
} else {
throw new TypeError("No iterator found");
}
if (typeof iterator.next !== "function") {
throw new TypeError("iterator.next is not a function");
}
return iterator;
}
function isObject(x) {
return (typeof x === "object" && x != null) || typeof x === "function";
}
const _resourceBacking = Symbol("[[resourceBacking]]"); const _resourceBacking = Symbol("[[resourceBacking]]");
// This distinction exists to prevent unrefable streams being used in // This distinction exists to prevent unrefable streams being used in
// regular fast streams that are unaware of refability // regular fast streams that are unaware of refability
@ -5197,21 +5168,22 @@ class ReadableStream {
} }
static from(asyncIterable) { static from(asyncIterable) {
const prefix = "Failed to execute 'ReadableStream.from'";
webidl.requiredArguments( webidl.requiredArguments(
arguments.length, arguments.length,
1, 1,
"Failed to execute 'ReadableStream.from'", prefix,
); );
asyncIterable = webidl.converters.any(asyncIterable); asyncIterable = webidl.converters["async iterable<any>"](
asyncIterable,
const iterator = getAsyncOrSyncIterator(asyncIterable); prefix,
"Argument 1",
);
const iter = asyncIterable.open();
const stream = createReadableStream(noop, async () => { const stream = createReadableStream(noop, async () => {
// deno-lint-ignore prefer-primordials // deno-lint-ignore prefer-primordials
const res = await iterator.next(); const res = await iter.next();
if (!isObject(res)) {
throw new TypeError("iterator.next value is not an object");
}
if (res.done) { if (res.done) {
readableStreamDefaultControllerClose(stream[_controller]); readableStreamDefaultControllerClose(stream[_controller]);
} else { } else {
@ -5221,17 +5193,8 @@ class ReadableStream {
); );
} }
}, async (reason) => { }, async (reason) => {
if (iterator.return == null) {
return undefined;
} else {
// deno-lint-ignore prefer-primordials // deno-lint-ignore prefer-primordials
const res = await iterator.return(reason); await iter.return(reason);
if (!isObject(res)) {
throw new TypeError("iterator.return value is not an object");
} else {
return undefined;
}
}
}, 0); }, 0);
return stream; return stream;
} }
@ -6892,6 +6855,10 @@ webidl.converters.StreamPipeOptions = webidl
{ key: "signal", converter: webidl.converters.AbortSignal }, { key: "signal", converter: webidl.converters.AbortSignal },
]); ]);
webidl.converters["async iterable<any>"] = webidl.createAsyncIterableConverter(
webidl.converters.any,
);
internals.resourceForReadableStream = resourceForReadableStream; internals.resourceForReadableStream = resourceForReadableStream;
export { export {

View file

@ -26,6 +26,7 @@ const {
Float32Array, Float32Array,
Float64Array, Float64Array,
FunctionPrototypeBind, FunctionPrototypeBind,
FunctionPrototypeCall,
Int16Array, Int16Array,
Int32Array, Int32Array,
Int8Array, Int8Array,
@ -77,6 +78,7 @@ const {
StringPrototypeToWellFormed, StringPrototypeToWellFormed,
Symbol, Symbol,
SymbolIterator, SymbolIterator,
SymbolAsyncIterator,
SymbolToStringTag, SymbolToStringTag,
TypedArrayPrototypeGetBuffer, TypedArrayPrototypeGetBuffer,
TypedArrayPrototypeGetSymbolToStringTag, TypedArrayPrototypeGetSymbolToStringTag,
@ -920,6 +922,127 @@ function createSequenceConverter(converter) {
}; };
} }
function isAsyncIterable(obj) {
if (obj[SymbolAsyncIterator] === undefined) {
if (obj[SymbolIterator] === undefined) {
return false;
}
}
return true;
}
const AsyncIterable = Symbol("[[asyncIterable]]");
function createAsyncIterableConverter(converter) {
return function (
V,
prefix = undefined,
context = undefined,
opts = { __proto__: null },
) {
if (type(V) !== "Object") {
throw makeException(
TypeError,
"can not be converted to async iterable.",
prefix,
context,
);
}
let isAsync = true;
let method = V[SymbolAsyncIterator];
if (method === undefined) {
method = V[SymbolIterator];
if (method === undefined) {
throw makeException(
TypeError,
"is not iterable.",
prefix,
context,
);
}
isAsync = false;
}
return {
value: V,
[AsyncIterable]: AsyncIterable,
open(context) {
const iter = FunctionPrototypeCall(method, V);
if (type(iter) !== "Object") {
throw new TypeError(
`${context} could not be iterated because iterator method did not return object, but ${
type(iter)
}.`,
);
}
let asyncIterator = iter;
if (!isAsync) {
asyncIterator = {
// deno-lint-ignore require-await
async next() {
// deno-lint-ignore prefer-primordials
return iter.next();
},
};
}
return {
async next() {
// deno-lint-ignore prefer-primordials
const iterResult = await asyncIterator.next();
if (type(iterResult) !== "Object") {
throw TypeError(
`${context} failed to iterate next value because the next() method did not return an object, but ${
type(iterResult)
}.`,
);
}
if (iterResult.done) {
return { done: true };
}
const iterValue = converter(
iterResult.value,
`${context} failed to iterate next value`,
`The value returned from the next() method`,
opts,
);
return { done: false, value: iterValue };
},
async return(reason) {
if (asyncIterator.return === undefined) {
return undefined;
}
// deno-lint-ignore prefer-primordials
const returnPromiseResult = await asyncIterator.return(reason);
if (type(returnPromiseResult) !== "Object") {
throw TypeError(
`${context} failed to close iterator because the return() method did not return an object, but ${
type(returnPromiseResult)
}.`,
);
}
return undefined;
},
[SymbolAsyncIterator]() {
return this;
},
};
},
};
};
}
function createRecordConverter(keyConverter, valueConverter) { function createRecordConverter(keyConverter, valueConverter) {
return (V, prefix, context, opts) => { return (V, prefix, context, opts) => {
if (type(V) !== "Object") { if (type(V) !== "Object") {
@ -1302,9 +1425,11 @@ function setlike(obj, objPrototype, readonly) {
export { export {
assertBranded, assertBranded,
AsyncIterable,
brand, brand,
configureInterface, configureInterface,
converters, converters,
createAsyncIterableConverter,
createBranded, createBranded,
createDictionaryConverter, createDictionaryConverter,
createEnumConverter, createEnumConverter,
@ -1315,6 +1440,7 @@ export {
createSequenceConverter, createSequenceConverter,
illegalConstructor, illegalConstructor,
invokeCallbackFunction, invokeCallbackFunction,
isAsyncIterable,
makeException, makeException,
mixinPairIterable, mixinPairIterable,
requiredArguments, requiredArguments,

View file

@ -438,6 +438,27 @@ declare module "ext:deno_webidl/00_webidl.js" {
opts?: any, opts?: any,
) => T[]; ) => T[];
/**
* Create a converter that converts an async iterable of the inner type.
*/
function createAsyncIterableConverter<V, T>(
converter: (
v: V,
prefix?: string,
context?: string,
opts?: any,
) => T,
): (
v: any,
prefix?: string,
context?: string,
opts?: any,
) => ConvertedAsyncIterable<V, T>;
interface ConvertedAsyncIterable<V, T> extends AsyncIterableIterator<T> {
value: V;
}
/** /**
* Create a converter that converts a Promise of the inner type. * Create a converter that converts a Promise of the inner type.
*/ */
@ -559,4 +580,9 @@ declare module "ext:deno_webidl/00_webidl.js" {
| "Symbol" | "Symbol"
| "BigInt" | "BigInt"
| "Object"; | "Object";
/**
* Check whether a value is an async iterable.
*/
function isAsyncIterable(v: any): boolean;
} }

View file

@ -72,6 +72,7 @@ util::unit_test_factory!(
dgram_test, dgram_test,
domain_test, domain_test,
fs_test, fs_test,
fetch_test,
http_test, http_test,
http2_test, http2_test,
inspector_test, inspector_test,

View file

@ -2119,3 +2119,30 @@ Deno.test(
await server; await server;
}, },
); );
Deno.test("fetch async iterable", async () => {
const iterable = (async function* () {
yield new Uint8Array([1, 2, 3, 4, 5]);
yield new Uint8Array([6, 7, 8, 9, 10]);
})();
const res = new Response(iterable);
const actual = await res.bytes();
const expected = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assertEquals(actual, expected);
});
Deno.test("fetch iterable", async () => {
const iterable = (function* () {
yield new Uint8Array([1, 2, 3, 4, 5]);
yield new Uint8Array([6, 7, 8, 9, 10]);
})();
const res = new Response(iterable);
const actual = await res.bytes();
const expected = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
assertEquals(actual, expected);
});
Deno.test("fetch string object", async () => {
const res = new Response(Object("hello"));
assertEquals(await res.text(), "hello");
});

View file

@ -1,5 +1,10 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals, assertRejects, fail } from "./test_util.ts"; import {
assertEquals,
assertRejects,
assertThrows,
fail,
} from "./test_util.ts";
const { const {
core, core,
@ -533,3 +538,11 @@ Deno.test(async function decompressionStreamInvalidGzipStillReported() {
"corrupt gzip stream does not have a matching checksum", "corrupt gzip stream does not have a matching checksum",
); );
}); });
Deno.test(function readableStreamFromWithStringThrows() {
assertThrows(
() => ReadableStream.from("string"),
TypeError,
"Failed to execute 'ReadableStream.from': Argument 1 can not be converted to async iterable.",
);
});

View file

@ -0,0 +1,18 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "@std/assert";
import { createReadStream } from "node:fs";
Deno.test("fetch node stream", async () => {
const file = createReadStream("tests/testdata/assets/fixture.json");
const response = await fetch("http://localhost:4545/echo_server", {
method: "POST",
body: file,
});
assertEquals(
await response.text(),
await Deno.readTextFile("tests/testdata/assets/fixture.json"),
);
});

View file

@ -3257,8 +3257,14 @@
"owning-type-message-port.any.worker.html": false, "owning-type-message-port.any.worker.html": false,
"owning-type.any.html": false, "owning-type.any.html": false,
"owning-type.any.worker.html": false, "owning-type.any.worker.html": false,
"from.any.html": true, "from.any.html": [
"from.any.worker.html": true "ReadableStream.from ignores a null @@asyncIterator",
"ReadableStream.from accepts a string"
],
"from.any.worker.html": [
"ReadableStream.from ignores a null @@asyncIterator",
"ReadableStream.from accepts a string"
]
}, },
"transform-streams": { "transform-streams": {
"backpressure.any.html": true, "backpressure.any.html": true,