mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Implement ReadableStream.from() method
Add support for ReadableStream.from() which creates ReadableStream instances from iterables and async iterables, following the WHATWG Streams specification. - Supports arrays, strings, Sets, Maps, and custom iterables/async iterables - Returns the same ReadableStream if one is passed - Properly handles error cases with appropriate TypeError messages - Includes comprehensive test coverage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -153,6 +153,7 @@ template<> void JSReadableStreamDOMConstructor::initializeProperties(VM& vm, JSD
|
||||
m_originalName.set(vm, this, nameString);
|
||||
putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum);
|
||||
putDirect(vm, vm.propertyNames->prototype, JSReadableStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete);
|
||||
putDirectBuiltinFunction(vm, &globalObject, Identifier::fromString(vm, "from"_s), readableStreamFromCodeGenerator(vm), static_cast<unsigned>(JSC::PropertyAttribute::DontEnum));
|
||||
}
|
||||
|
||||
template<> FunctionExecutable* JSReadableStreamDOMConstructor::initializeExecutable(VM& vm)
|
||||
@@ -279,6 +280,7 @@ void JSReadableStream::destroy(JSC::JSCell* cell)
|
||||
thisObject->JSReadableStream::~JSReadableStream();
|
||||
}
|
||||
|
||||
|
||||
JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName))
|
||||
{
|
||||
auto& vm = JSC::getVM(lexicalGlobalObject);
|
||||
|
||||
@@ -513,3 +513,103 @@ export function lazyAsyncIterator(this) {
|
||||
$readableStreamDefineLazyIterators(prototype);
|
||||
return prototype[globalThis.Symbol.asyncIterator].$call(this);
|
||||
}
|
||||
|
||||
$linkTimeConstant;
|
||||
export function from(asyncIterable) {
|
||||
if (asyncIterable == null) {
|
||||
throw new TypeError("ReadableStream.from() takes a non-null value");
|
||||
}
|
||||
|
||||
// Check if it's already a ReadableStream
|
||||
if ($isReadableStream(asyncIterable)) {
|
||||
return asyncIterable;
|
||||
}
|
||||
|
||||
// Handle arrays with Array.fromAsync
|
||||
if ($isArray(asyncIterable)) {
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
for (let i = 0; i < asyncIterable.length; i++) {
|
||||
controller.enqueue(asyncIterable[i]);
|
||||
}
|
||||
controller.close();
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle iterables (sync and async)
|
||||
let asyncIteratorMethod = asyncIterable[globalThis.Symbol.asyncIterator];
|
||||
let iteratorMethod = asyncIterable[globalThis.Symbol.iterator];
|
||||
|
||||
if (asyncIteratorMethod != null) {
|
||||
// Async iterable
|
||||
if (typeof asyncIteratorMethod !== "function") {
|
||||
throw new TypeError("ReadableStream.from() argument's @@asyncIterator method must be a function");
|
||||
}
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
const iterator = asyncIteratorMethod.$call(asyncIterable);
|
||||
if (!$isObject(iterator)) {
|
||||
throw new TypeError("ReadableStream.from() argument's @@asyncIterator method must return an object");
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const result = await iterator.next();
|
||||
if (!$isObject(result)) {
|
||||
throw new TypeError("Iterator result must be an object");
|
||||
}
|
||||
|
||||
if (result.done) {
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
controller.enqueue(result.value);
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (iteratorMethod != null) {
|
||||
// Sync iterable
|
||||
if (typeof iteratorMethod !== "function") {
|
||||
throw new TypeError("ReadableStream.from() argument's @@iterator method must be a function");
|
||||
}
|
||||
|
||||
return new ReadableStream({
|
||||
start(controller) {
|
||||
try {
|
||||
const iterator = iteratorMethod.$call(asyncIterable);
|
||||
if (!$isObject(iterator)) {
|
||||
throw new TypeError("ReadableStream.from() argument's @@iterator method must return an object");
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const result = iterator.next();
|
||||
if (!$isObject(result)) {
|
||||
throw new TypeError("Iterator result must be an object");
|
||||
}
|
||||
|
||||
if (result.done) {
|
||||
controller.close();
|
||||
break;
|
||||
}
|
||||
|
||||
controller.enqueue(result.value);
|
||||
}
|
||||
} catch (error) {
|
||||
controller.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new TypeError("ReadableStream.from() argument must be an iterable or async iterable");
|
||||
}
|
||||
}
|
||||
|
||||
280
test/js/web/streams/readable-stream-from.test.ts
Normal file
280
test/js/web/streams/readable-stream-from.test.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
test("ReadableStream.from", () => {
|
||||
expect(typeof ReadableStream.from).toBe("function");
|
||||
expect(ReadableStream.from.length).toBe(1);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with array", async () => {
|
||||
const array = [1, 2, 3, 4, 5];
|
||||
const stream = ReadableStream.from(array);
|
||||
|
||||
expect(stream).toBeInstanceOf(ReadableStream);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const results: number[] = [];
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: isDone } = await reader.read();
|
||||
done = isDone;
|
||||
if (!done) {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
expect(results).toEqual([1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with empty array", async () => {
|
||||
const array: number[] = [];
|
||||
const stream = ReadableStream.from(array);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const { value, done } = await reader.read();
|
||||
|
||||
expect(done).toBe(true);
|
||||
expect(value).toBeUndefined();
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with string (iterable)", async () => {
|
||||
const str = "hello";
|
||||
const stream = ReadableStream.from(str);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const results: string[] = [];
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: isDone } = await reader.read();
|
||||
done = isDone;
|
||||
if (!done) {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
expect(results).toEqual(["h", "e", "l", "l", "o"]);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with Set", async () => {
|
||||
const set = new Set([1, 2, 3]);
|
||||
const stream = ReadableStream.from(set);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const results: number[] = [];
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: isDone } = await reader.read();
|
||||
done = isDone;
|
||||
if (!done) {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
expect(results).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with Map", async () => {
|
||||
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
|
||||
const stream = ReadableStream.from(map);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const results: [string, number][] = [];
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: isDone } = await reader.read();
|
||||
done = isDone;
|
||||
if (!done) {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
expect(results).toEqual([["a", 1], ["b", 2], ["c", 3]]);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with custom iterable", async () => {
|
||||
const customIterable = {
|
||||
*[Symbol.iterator]() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}
|
||||
};
|
||||
|
||||
const stream = ReadableStream.from(customIterable);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const results: number[] = [];
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: isDone } = await reader.read();
|
||||
done = isDone;
|
||||
if (!done) {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
expect(results).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with async iterable", async () => {
|
||||
const asyncIterable = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
}
|
||||
};
|
||||
|
||||
const stream = ReadableStream.from(asyncIterable);
|
||||
|
||||
const reader = stream.getReader();
|
||||
const results: number[] = [];
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: isDone } = await reader.read();
|
||||
done = isDone;
|
||||
if (!done) {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
expect(results).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with existing ReadableStream", async () => {
|
||||
const originalStream = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(1);
|
||||
controller.enqueue(2);
|
||||
controller.enqueue(3);
|
||||
controller.close();
|
||||
}
|
||||
});
|
||||
|
||||
const stream = ReadableStream.from(originalStream);
|
||||
|
||||
// Should return the same stream
|
||||
expect(stream).toBe(originalStream);
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with null should throw", () => {
|
||||
expect(() => ReadableStream.from(null)).toThrow("ReadableStream.from() takes a non-null value");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with undefined should throw", () => {
|
||||
expect(() => ReadableStream.from(undefined)).toThrow("ReadableStream.from() takes a non-null value");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with non-iterable should throw", () => {
|
||||
const nonIterable = {};
|
||||
expect(() => ReadableStream.from(nonIterable)).toThrow("ReadableStream.from() argument must be an iterable or async iterable");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with invalid iterator method should throw", () => {
|
||||
const invalidIterable = {
|
||||
[Symbol.iterator]: "not a function"
|
||||
};
|
||||
|
||||
expect(() => ReadableStream.from(invalidIterable)).toThrow("ReadableStream.from() argument's @@iterator method must be a function");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() with invalid async iterator method should throw", () => {
|
||||
const invalidAsyncIterable = {
|
||||
[Symbol.asyncIterator]: "not a function"
|
||||
};
|
||||
|
||||
expect(() => ReadableStream.from(invalidAsyncIterable)).toThrow("ReadableStream.from() argument's @@asyncIterator method must be a function");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() handles iterator that throws", async () => {
|
||||
const throwingIterable = {
|
||||
*[Symbol.iterator]() {
|
||||
yield 1;
|
||||
throw new Error("Iterator error");
|
||||
}
|
||||
};
|
||||
|
||||
const stream = ReadableStream.from(throwingIterable);
|
||||
const reader = stream.getReader();
|
||||
|
||||
// Should read first value successfully
|
||||
const { value } = await reader.read();
|
||||
expect(value).toBe(1);
|
||||
|
||||
// Should handle error from iterator
|
||||
await expect(reader.read()).rejects.toThrow("Iterator error");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() handles async iterator that throws", async () => {
|
||||
const throwingAsyncIterable = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield 1;
|
||||
throw new Error("Async iterator error");
|
||||
}
|
||||
};
|
||||
|
||||
const stream = ReadableStream.from(throwingAsyncIterable);
|
||||
const reader = stream.getReader();
|
||||
|
||||
// Should read first value successfully
|
||||
const { value } = await reader.read();
|
||||
expect(value).toBe(1);
|
||||
|
||||
// Should handle error from async iterator
|
||||
await expect(reader.read()).rejects.toThrow("Async iterator error");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() works with Array.from() like usage", async () => {
|
||||
// Test that it works similar to how Array.from() works
|
||||
const stream1 = ReadableStream.from("abc");
|
||||
const stream2 = ReadableStream.from([1, 2, 3]);
|
||||
const stream3 = ReadableStream.from(new Set(["x", "y", "z"]));
|
||||
|
||||
const result1 = await new Response(stream1).text();
|
||||
const result2 = await streamToArray(stream2);
|
||||
const result3 = await streamToArray(stream3);
|
||||
|
||||
// For string, each character should be a separate chunk
|
||||
expect(result1).toBe("abc");
|
||||
expect(result2).toEqual([1, 2, 3]);
|
||||
expect(result3).toEqual(["x", "y", "z"]);
|
||||
});
|
||||
|
||||
// Helper function to convert stream to array
|
||||
async function streamToArray(stream: ReadableStream) {
|
||||
const reader = stream.getReader();
|
||||
const results: any[] = [];
|
||||
|
||||
let done = false;
|
||||
while (!done) {
|
||||
const { value, done: isDone } = await reader.read();
|
||||
done = isDone;
|
||||
if (!done) {
|
||||
results.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
test("ReadableStream.from() as static method", () => {
|
||||
// Test that it's actually a static method on the constructor
|
||||
expect(ReadableStream.hasOwnProperty("from")).toBe(true);
|
||||
expect(ReadableStream.from).toBe(ReadableStream.from);
|
||||
expect(typeof ReadableStream.from).toBe("function");
|
||||
});
|
||||
|
||||
test("ReadableStream.from() integration with Response", async () => {
|
||||
// Test integration with other Web APIs that consume ReadableStream
|
||||
const stream = ReadableStream.from(["hello", " ", "world"]);
|
||||
const response = new Response(stream);
|
||||
const text = await response.text();
|
||||
|
||||
expect(text).toBe("hello world");
|
||||
});
|
||||
Reference in New Issue
Block a user