diff --git a/.gitignore b/.gitignore index 489f42e901b7..ef34a5a0cc66 100644 --- a/.gitignore +++ b/.gitignore @@ -198,3 +198,7 @@ src/runtime/bake/generated.ts # them on disk; keep ignored so they don't show as untracked). src/jsc/bindings/GeneratedJS2Native.zig src/jsc/bindings/GeneratedBindings.zig + +# Web Streams rewrite working notes (design docs, spec transcription, review logs). +# Kept locally for the ongoing work; not part of the source tree. +/specs/ diff --git a/bench/snippets/webstreams-consumers.mjs b/bench/snippets/webstreams-consumers.mjs new file mode 100644 index 000000000000..a552ad4c3d43 --- /dev/null +++ b/bench/snippets/webstreams-consumers.mjs @@ -0,0 +1,100 @@ +// Consumer x chunk-shape matrix for JS-sourced ReadableStreams (MB/s, best of RUNS). +// Every shape totals ~8 MiB so numbers are comparable across rows. +const RUNS = 5; +const MB = 1024 * 1024; +const TOTAL = 8 * MB; + +const binary = size => { + const chunk = new Uint8Array(size).fill(120); + const count = TOTAL / size; + return () => { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i++ < count) c.enqueue(chunk); + else c.close(); + }, + }); + }; +}; +const text = size => { + const chunk = "x".repeat(size); + const count = TOTAL / size; + return () => { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i++ < count) c.enqueue(chunk); + else c.close(); + }, + }); + }; +}; +const mixed = size => { + const textChunk = "y".repeat(size); + const binaryChunk = new Uint8Array(size).fill(121); + const count = TOTAL / size; + return () => { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i < count) (c.enqueue(i % 2 ? textChunk : binaryChunk), i++); + else c.close(); + }, + }); + }; +}; + +const shapes = { + "binary 64KiB x128": binary(64 * 1024), + "binary 1KiB x8192": binary(1024), + "text 64KiB x128": text(64 * 1024), + "text 1KiB x8192": text(1024), + "mixed text/bytes 64KiB x128": mixed(64 * 1024), + "one 8MiB chunk": (() => { + const chunk = new Uint8Array(TOTAL).fill(122); + return () => + new ReadableStream({ + start(c) { + c.enqueue(chunk); + c.close(); + }, + }); + })(), +}; + +const consumers = { + "toText": s => Bun.readableStreamToText(s), + "toArrayBuffer": s => Bun.readableStreamToArrayBuffer(s), + "toBytes": s => Bun.readableStreamToBytes(s), + "toArray": s => Bun.readableStreamToArray(s), + "toBlob": async s => (await Bun.readableStreamToBlob(s)).size, + "Response.text": s => new Response(s).text(), + "Response.arrayBuffer": s => new Response(s).arrayBuffer(), + "for await": async s => { + let n = 0; + for await (const c of s) n += c.length; + return n; + }, +}; + +const table = {}; +for (const [shapeName, make] of Object.entries(shapes)) { + const row = (table[shapeName] = {}); + for (const [consumerName, consume] of Object.entries(consumers)) { + await consume(make()); // warmup + validity + let best = Infinity; + for (let i = 0; i < RUNS; i++) { + const t0 = performance.now(); + await consume(make()); + best = Math.min(best, performance.now() - t0); + } + row[consumerName] = Math.round(TOTAL / MB / (best / 1000)); + } +} +const consumerNames = Object.keys(consumers); +const version = typeof Bun !== "undefined" ? `bun ${Bun.revision.slice(0, 9)}` : `node ${process.version}`; +console.log(`# webstreams consumers (MB/s) — ${version} — ${TOTAL / MB} MiB per pass, best of ${RUNS}`); +console.log(["shape".padEnd(28), ...consumerNames.map(n => n.padStart(14))].join("")); +for (const [shapeName, row] of Object.entries(table)) + console.log([shapeName.padEnd(28), ...consumerNames.map(n => String(row[n]).padStart(14))].join("")); diff --git a/bench/snippets/webstreams-memory.mjs b/bench/snippets/webstreams-memory.mjs new file mode 100644 index 000000000000..57af6c8657a0 --- /dev/null +++ b/bench/snippets/webstreams-memory.mjs @@ -0,0 +1,122 @@ +// Web Streams memory: (1) retained RSS per live instance, (2) peak/settled RSS for +// streaming workloads. Run with any JS runtime; extra heap counts print under Bun. +const N = 100_000; +const gc = globalThis.Bun?.gc ?? globalThis.gc; +if (!gc) throw new Error("This benchmark needs a GC hook: run with Bun, or node --expose-gc."); +const rss = () => process.memoryUsage.rss(); +const MB = 1024 * 1024; +const fmt = n => (n / MB).toFixed(1).padStart(8) + " MB"; + +console.log(`# per-instance retained RSS (n=${N} live instances)`); +const keep = []; +function perInstance(label, make) { + gc(true); + const before = rss(); + const held = new Array(N); + for (let i = 0; i < N; i++) held[i] = make(); + gc(true); + const perObject = (rss() - before) / N; + console.log(`${label.padEnd(40)} ${perObject.toFixed(0).padStart(6)} bytes/instance`); + keep.push(held); +} +perInstance("new ReadableStream({pull(){}})", () => new ReadableStream({ pull() {} })); +perInstance("new ReadableStream() + getReader()", () => new ReadableStream({ pull() {} }).getReader()); +perInstance("new WritableStream({write(){}})", () => new WritableStream({ write() {} })); +perInstance("new TransformStream()", () => new TransformStream()); +keep.length = 0; +gc(true); + +console.log(`\n# workload RSS (peak over baseline during the run, settled after gc)`); +const CHUNK = new Uint8Array(64 * 1024).fill(120); +async function workload(label, fn) { + gc(true); + const before = rss(); + let peak = before; + const timer = setInterval(() => { + peak = Math.max(peak, rss()); + }, 5); + // Whatever `fn` returns is kept alive until after the settled measurement, so + // "N live objects" workloads measure retention rather than post-return garbage. + const keepAlive = await fn(); + clearInterval(timer); + peak = Math.max(peak, rss()); + gc(true); + const settled = rss(); + console.log(`${label.padEnd(46)} peak ${fmt(peak - before)} settled ${fmt(settled - before)}`); + return keepAlive; +} +const source = n => { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i++ < n) c.enqueue(CHUNK); + else c.close(); + }, + }); +}; +await workload("pipeTo 512 MiB (64 KiB chunks)", async () => { + let n = 0; + await source(8192).pipeTo( + new WritableStream({ + write(c) { + n += c.length; + }, + }), + ); +}); +await workload("for await 512 MiB", async () => { + let n = 0; + for await (const c of source(8192)) n += c.length; +}); +await workload("Response(stream 256 MiB).arrayBuffer()", async () => { + (await new Response(source(4096)).arrayBuffer()).byteLength; +}); +await workload("Response(stream 256 MiB of text).text()", async () => { + let i = 0; + const text = "x".repeat(64 * 1024); + const rs = new ReadableStream({ + pull(c) { + if (i++ < 4096) c.enqueue(text); + else c.close(); + }, + }); + (await new Response(rs).text()).length; +}); +{ + const held = await workload("10k live TransformStream chains (held)", async () => { + const chains = new Array(10_000); + for (let i = 0; i < chains.length; i++) { + const ts = new TransformStream(); + chains[i] = [ts, ts.readable.getReader(), ts.writable.getWriter()]; + } + gc(true); + return chains; + }); + held.length = 0; +} +await workload("2k concurrent pipeThrough pipes (1 MiB each)", async () => { + const pipes = []; + for (let i = 0; i < 2000; i++) { + let k = 0; + const rs = new ReadableStream({ + pull(c) { + if (k++ < 16) c.enqueue(CHUNK); + else c.close(); + }, + }); + pipes.push(rs.pipeThrough(new TransformStream()).pipeTo(new WritableStream({ write() {} }))); + } + await Promise.all(pipes); +}); + +if (typeof Bun !== "undefined") { + gc(true); + const { heapStats } = await import("bun:jsc"); + const counts = heapStats().objectTypeCounts; + const interesting = Object.entries(counts) + .filter(([k]) => /Stream|Reader|Writer|Controller|Request|Promise|Function/i.test(k)) + .sort((a, b) => b[1] - a[1]) + .slice(0, 16); + console.log("\n# heapStats().objectTypeCounts after the workloads (top stream-related):"); + for (const [k, v] of interesting) console.log(` ${k}: ${v}`); +} diff --git a/bench/snippets/webstreams-tee.mjs b/bench/snippets/webstreams-tee.mjs new file mode 100644 index 000000000000..7d522cf661ce --- /dev/null +++ b/bench/snippets/webstreams-tee.mjs @@ -0,0 +1,107 @@ +// tee()/clone() throughput and memory: plain ReadableStream.tee, fetch Response.clone, +// and Bun.serve Request.clone. Reports MB/s over the total bytes moved plus peak/settled +// RSS growth for each scenario (best-of-RUNS for time; max for memory). +const RUNS = 3; +const MB = 1024 * 1024; +const CHUNK = new Uint8Array(64 * 1024).fill(120); +const gc = globalThis.Bun?.gc ?? globalThis.gc; +if (!gc) throw new Error("This benchmark needs a GC hook: run with Bun, or node --expose-gc."); +const rss = () => process.memoryUsage.rss(); +const fmt = n => (n / MB).toFixed(1).padStart(7) + " MB"; + +const source = totalBytes => { + const count = Math.ceil(totalBytes / CHUNK.length); + let i = 0; + return new ReadableStream({ + pull(c) { + if (i++ < count) c.enqueue(CHUNK); + else c.close(); + }, + }); +}; +const drain = async rs => { + const r = rs.getReader(); + let n = 0; + while (true) { + const { done, value } = await r.read(); + if (done) return n; + n += value.length; + } +}; + +async function bench(label, totalBytes, fn) { + await fn(); // warmup + let best = Infinity; + let peak = 0; + for (let i = 0; i < RUNS; i++) { + gc(true); + const before = rss(); + let localPeak = before; + const timer = setInterval(() => { + localPeak = Math.max(localPeak, rss()); + }, 5); + const t0 = performance.now(); + await fn(); + const elapsed = performance.now() - t0; + clearInterval(timer); + localPeak = Math.max(localPeak, rss()); + best = Math.min(best, elapsed); + peak = Math.max(peak, localPeak - before); + } + const mbps = totalBytes / MB / (best / 1000); + console.log(`${label.padEnd(46)} ${mbps.toFixed(0).padStart(7)} MB/s peak RSS +${fmt(peak)}`); +} + +const TOTAL = 128 * MB; +await bench("tee(): both branches drained concurrently", TOTAL * 2, async () => { + const [a, b] = source(TOTAL).tee(); + await Promise.all([drain(a), drain(b)]); +}); +await bench("tee(): branch B read only after A finishes", TOTAL * 2, async () => { + const [a, b] = source(TOTAL).tee(); + await drain(a); + await drain(b); +}); +await bench("tee(): read A, cancel B", TOTAL, async () => { + const [a, b] = source(TOTAL).tee(); + const done = drain(a); + await b.cancel(); + await done; +}); + +if (typeof Bun !== "undefined") { + const BODY_BYTES = 64 * MB; + await using server = Bun.serve({ + port: 0, + async fetch(req) { + const url = new URL(req.url); + if (url.pathname === "/stream") return new Response(source(BODY_BYTES)); + if (url.pathname === "/clone-echo") { + // Request.clone(): consume the body twice server-side. + const clone = req.clone(); + const [a, b] = await Promise.all([req.arrayBuffer(), clone.arrayBuffer()]); + return new Response(String(a.byteLength + b.byteLength)); + } + return new Response("nope", { status: 404 }); + }, + }); + const base = `http://localhost:${server.port}`; + + await bench("fetch(stream).clone(): read both bodies", BODY_BYTES * 2, async () => { + const response = await fetch(`${base}/stream`); + const clone = response.clone(); + await Promise.all([response.arrayBuffer(), clone.arrayBuffer()]); + }); + await bench("fetch(stream).clone(): read one, cancel clone", BODY_BYTES, async () => { + const response = await fetch(`${base}/stream`); + const clone = response.clone(); + const read = response.arrayBuffer(); + await clone.body.cancel(); + await read; + }); + const upload = new Uint8Array(32 * MB).fill(7); + await bench("Bun.serve: req.clone(), read both bodies", upload.length * 2, async () => { + const res = await fetch(`${base}/clone-echo`, { method: "POST", body: upload }); + if ((await res.text()) !== String(upload.length * 2)) throw new Error("bad echo"); + }); +} diff --git a/bench/snippets/webstreams-throughput.mjs b/bench/snippets/webstreams-throughput.mjs new file mode 100644 index 000000000000..561d248d8826 --- /dev/null +++ b/bench/snippets/webstreams-throughput.mjs @@ -0,0 +1,202 @@ +// Web Streams throughput: 64 KiB chunks, 32 MiB per pass, best of RUNS passes, +// timed end-to-end so numbers are directly comparable across runtimes. +// +// Two source families: +// - "shared chunk" scenarios enqueue the SAME Uint8Array object every time. +// Default streams pass chunks by reference (no engine copies them), so these +// rows measure per-chunk machinery overhead only; they are reported as +// chunks/sec (with ns/chunk), NOT MB/s, because no payload bytes move. +// - "fresh buffers" scenarios allocate and fill a new chunk per enqueue (what a +// socket or file source produces), so their MB/s is bounded by real memory +// work and is meaningful as throughput. +// Consumer scenarios (arrayBuffer/text/readableStreamTo*) always materialize +// their output, so they report MB/s. +const CHUNK = 64 * 1024; +const CHUNKS = 512; // 32 MiB +const RUNS = 5; +const BYTES = CHUNK * CHUNKS; +const chunk = new Uint8Array(CHUNK).fill(120); +const textChunk = "x".repeat(CHUNK); + +const byteSource = () => { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i++ < CHUNKS) c.enqueue(chunk); + else c.close(); + }, + }); +}; +// A fresh, written-to buffer per chunk: the shape real byte sources (sockets, +// files) produce. Bounded by allocation + memory-touch bandwidth. +const freshSource = () => { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i++ < CHUNKS) c.enqueue(new Uint8Array(CHUNK).fill(i & 0xff)); + else c.close(); + }, + }); +}; +const textSource = () => { + let i = 0; + return new ReadableStream({ + pull(c) { + if (i++ < CHUNKS) c.enqueue(textChunk); + else c.close(); + }, + }); +}; +const byobSource = () => { + let i = 0; + return new ReadableStream({ + type: "bytes", + autoAllocateChunkSize: CHUNK, + pull(c) { + if (i++ < CHUNKS) { + const v = c.byobRequest.view; + c.byobRequest.respond(v.byteLength); + } else { + c.close(); + c.byobRequest?.respond(0); + } + }, + }); +}; + +const drain = async rs => { + const r = rs.getReader(); + let n = 0; + while (true) { + const { done, value } = await r.read(); + if (done) return n; + n += value.length; + } +}; + +// Scenario names listed here are reference-passing ("shared chunk") and are +// reported as chunks/sec instead of MB/s. +const SHARED_CHUNK_SCENARIOS = new Set([ + "reader.read() loop (shared chunk)", + "for await (shared chunk)", + "pipeTo(WritableStream) (shared chunk)", + "pipeThrough(TransformStream) (shared chunk)", + "tee + drain both (shared chunk)", +]); + +const scenarios = { + "reader.read() loop (shared chunk)": () => drain(byteSource()), + "reader.read() loop (fresh buffers)": () => drain(freshSource()), + "for await (shared chunk)": async () => { + let n = 0; + for await (const c of byteSource()) n += c.length; + return n; + }, + "for await (fresh buffers)": async () => { + let n = 0; + for await (const c of freshSource()) n += c.length; + return n; + }, + "pipeTo(WritableStream) (shared chunk)": async () => { + let n = 0; + await byteSource().pipeTo( + new WritableStream({ + write(c) { + n += c.length; + }, + }), + ); + return n; + }, + "pipeTo(WritableStream) (fresh buffers)": async () => { + let n = 0; + await freshSource().pipeTo( + new WritableStream({ + write(c) { + n += c.length; + }, + }), + ); + return n; + }, + "pipeThrough(TransformStream) (shared chunk)": () => drain(byteSource().pipeThrough(new TransformStream())), + "pipeThrough(TransformStream) (fresh buffers)": () => drain(freshSource().pipeThrough(new TransformStream())), + "tee + drain both (shared chunk)": async () => { + const [a, b] = byteSource().tee(); + const [x] = await Promise.all([drain(a), drain(b)]); + return x; + }, + "tee + drain both (fresh buffers)": async () => { + const [a, b] = freshSource().tee(); + const [x] = await Promise.all([drain(a), drain(b)]); + return x; + }, + "new Response(stream).arrayBuffer()": async () => (await new Response(byteSource()).arrayBuffer()).byteLength, + "byte source (byobRequest) default reader": () => drain(byobSource()), + "byte source (byobRequest) BYOB reader": async () => { + const r = byobSource().getReader({ mode: "byob" }); + let n = 0; + let view = new Uint8Array(CHUNK); + while (true) { + const { done, value } = await r.read(view); + if (done) return n; + n += value.byteLength; + view = new Uint8Array(value.buffer); + } + }, +}; + +if (typeof Bun !== "undefined") { + // Response bodies of string chunks are a Bun extension (the spec requires Uint8Array + // chunks; Node and Deno reject them), so this scenario only runs on Bun. + scenarios["text chunks -> Response.text()"] = async () => (await new Response(textSource()).text()).length; + scenarios["direct stream -> readableStreamToBytes"] = async () => { + const rs = new ReadableStream({ + type: "direct", + pull(c) { + for (let i = 0; i < CHUNKS; i++) c.write(chunk); + c.end(); + }, + }); + return (await Bun.readableStreamToBytes(rs)).byteLength; + }; + scenarios["Bun.readableStreamToBytes(stream)"] = async () => + (await Bun.readableStreamToBytes(byteSource())).byteLength; +} + +const version = + typeof Bun !== "undefined" + ? `bun ${Bun.revision.slice(0, 9)}` + : typeof Deno !== "undefined" + ? `deno ${Deno.version.deno}` + : `node ${process.version}`; +console.log( + `# webstreams throughput — ${version} — ${CHUNKS} x ${CHUNK / 1024} KiB = ${BYTES / 1024 / 1024} MiB per pass, best of ${RUNS}`, +); +// `--scenario=` runs one scenario in isolation (e.g. under `/usr/bin/time -v` +// so the process's peak RSS measures exactly one scenario). +const only = (globalThis.process?.argv ?? []).find(a => a.startsWith("--scenario="))?.slice("--scenario=".length); +for (const [name, fn] of Object.entries(scenarios)) { + if (only && name !== only) continue; + // Collect between scenarios so no scenario pays the previous one's GC debt. + globalThis.Bun?.gc(true); + // warmup + if ((await fn()) !== BYTES) throw new Error(`${name}: wrong byte count`); + let best = Infinity; + for (let i = 0; i < RUNS; i++) { + const t0 = performance.now(); + await fn(); + best = Math.min(best, performance.now() - t0); + } + if (SHARED_CHUNK_SCENARIOS.has(name)) { + // Reference-passing: no payload bytes move, so MB/s would be misleading. + const chunksPerSec = CHUNKS / (best / 1000); + const nsPerChunk = (best * 1e6) / CHUNKS; + console.log( + `${name.padEnd(46)} ${(chunksPerSec / 1e6).toFixed(2).padStart(6)} M chunks/s (${nsPerChunk.toFixed(0)} ns/chunk, ${best.toFixed(1)} ms)`, + ); + } else { + const mbps = BYTES / 1024 / 1024 / (best / 1000); + console.log(`${name.padEnd(46)} ${mbps.toFixed(0).padStart(6)} MB/s (${best.toFixed(1)} ms)`); + } +} diff --git a/bench/snippets/webstreams-transform.mjs b/bench/snippets/webstreams-transform.mjs new file mode 100644 index 000000000000..e3756230e658 --- /dev/null +++ b/bench/snippets/webstreams-transform.mjs @@ -0,0 +1,90 @@ +// TextEncoderStream / TextDecoderStream / CompressionStream / DecompressionStream throughput. +// 64 KiB chunks; MB/s of payload through the transform (decoded / uncompressed bytes). +// Portable across Bun, Node, and Deno; each scenario reports the best of RUNS passes. +const CHUNK = 64 * 1024; +const CHUNKS = 256; // 16 MiB per pass +const RUNS = 5; +const BYTES = CHUNK * CHUNKS; + +// UTF-8 with multi-byte content sprinkled in so decoding is not pure-ASCII. +const textChunk = (() => { + const s = "hello world 🌊 stream ✨ ".repeat(3000); + return new TextEncoder().encode(s).slice(0, CHUNK); +})(); +// JSON-ish compressible payload. +const compressibleChunk = new TextEncoder() + .encode(JSON.stringify({ messages: Array.from({ length: 500 }, (_, i) => ({ id: i, role: "user", body: "the quick brown fox jumps over the lazy dog" })) })) + .slice(0, CHUNK); + +const source = chunk => + new ReadableStream({ + pull(c) { + if (this.i === undefined) this.i = 0; + if (this.i++ < CHUNKS) c.enqueue(chunk); + else c.close(); + }, + }); + +async function drainBytes(rs) { + const reader = rs.getReader(); + let n = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) return n; + n += typeof value === "string" ? value.length : value.byteLength; + } +} + +let compressed; +{ + // Pre-compress one pass of input for the decompression scenario. + const parts = []; + const rs = source(compressibleChunk).pipeThrough(new CompressionStream("gzip")); + const reader = rs.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) break; + parts.push(value); + } + let total = 0; + for (const p of parts) total += p.byteLength; + compressed = new Uint8Array(total); + let off = 0; + for (const p of parts) { + compressed.set(p, off); + off += p.byteLength; + } +} +const compressedSource = () => + new ReadableStream({ + start(c) { + // 64 KiB slices of the gzip stream. + for (let i = 0; i < compressed.byteLength; i += CHUNK) c.enqueue(compressed.subarray(i, Math.min(i + CHUNK, compressed.byteLength))); + c.close(); + }, + }); + +// A 64 K-character string chunk (mostly ASCII with multi-byte content mixed in). +const stringChunk = ("hello world \u{1F30A} stream \u2728 " + "x".repeat(40)).repeat(1200).slice(0, CHUNK); + +const scenarios = { + "TextEncoderStream": () => drainBytes(source(stringChunk).pipeThrough(new TextEncoderStream())), + "TextDecoderStream": () => drainBytes(source(textChunk).pipeThrough(new TextDecoderStream())), + "CompressionStream (gzip)": () => drainBytes(source(compressibleChunk).pipeThrough(new CompressionStream("gzip"))), + "DecompressionStream (gzip)": () => drainBytes(compressedSource().pipeThrough(new DecompressionStream("gzip"))), +}; + +const only = (globalThis.process?.argv ?? []).find(a => a.startsWith("--scenario="))?.slice("--scenario=".length); +for (const [name, fn] of Object.entries(scenarios)) { + if (only && name !== only) continue; + await fn(); // warmup + let best = Infinity; + for (let i = 0; i < RUNS; i++) { + const t0 = performance.now(); + await fn(); + best = Math.min(best, performance.now() - t0); + } + // Throughput in terms of the uncompressed/decoded payload the transform handled. + const mbps = BYTES / 1024 / 1024 / (best / 1000); + console.log(`${name.padEnd(30)} ${mbps.toFixed(0).padStart(6)} MB/s (${best.toFixed(1)} ms)`); +} diff --git a/bench/snippets/webstreams.mjs b/bench/snippets/webstreams.mjs new file mode 100644 index 000000000000..c06eff0e2f4b --- /dev/null +++ b/bench/snippets/webstreams.mjs @@ -0,0 +1,113 @@ +import { bench, run } from "../runner.mjs"; + +const CHUNK = "x".repeat(1024); +const CHUNKS = 100; + +function sourceOf(n) { + let i = 0; + return { + pull(c) { + if (i++ < n) c.enqueue(CHUNK); + else c.close(); + }, + }; +} + +bench("new ReadableStream()", () => { + return new ReadableStream(sourceOf(0)); +}); + +bench("new TransformStream()", () => { + return new TransformStream(); +}); + +bench("new WritableStream()", () => { + return new WritableStream({ write() {} }); +}); + +bench(`getReader().read() x ${CHUNKS}`, async () => { + const reader = new ReadableStream(sourceOf(CHUNKS)).getReader(); + while (!(await reader.read()).done); +}); + +bench(`for await x ${CHUNKS}`, async () => { + let n = 0; + for await (const chunk of new ReadableStream(sourceOf(CHUNKS))) n += chunk.length; + return n; +}); + +bench(`pipeTo x ${CHUNKS}`, async () => { + let n = 0; + await new ReadableStream(sourceOf(CHUNKS)).pipeTo( + new WritableStream({ + write(c) { + n += c.length; + }, + }), + ); + return n; +}); + +bench(`pipeThrough(TransformStream) + drain x ${CHUNKS}`, async () => { + const rs = new ReadableStream(sourceOf(CHUNKS)).pipeThrough(new TransformStream()); + const reader = rs.getReader(); + while (!(await reader.read()).done); +}); + +bench(`tee + drain both x ${CHUNKS}`, async () => { + const [a, b] = new ReadableStream(sourceOf(CHUNKS)).tee(); + const drain = async s => { + const r = s.getReader(); + while (!(await r.read()).done); + }; + await Promise.all([drain(a), drain(b)]); +}); + +bench(`new Response(stream).text() x ${CHUNKS}`, async () => { + return (await new Response(new ReadableStream(sourceOf(CHUNKS))).text()).length; +}); + +bench(`writer.write() x ${CHUNKS}`, async () => { + const ws = new WritableStream({ write() {} }); + const writer = ws.getWriter(); + for (let i = 0; i < CHUNKS; i++) await writer.write(CHUNK); + await writer.close(); +}); + +bench(`byte stream BYOB read x ${CHUNKS}`, async () => { + let i = 0; + const rs = new ReadableStream({ + type: "bytes", + autoAllocateChunkSize: 1024, + pull(c) { + if (i++ < CHUNKS) { + const view = c.byobRequest.view; + new Uint8Array(view.buffer, view.byteOffset, view.byteLength).fill(7); + c.byobRequest.respond(view.byteLength); + } else { + c.close(); + // An outstanding BYOB request must be released after close(). + c.byobRequest?.respond(0); + } + }, + }); + const reader = rs.getReader({ mode: "byob" }); + let n = 0; + while (true) { + const { done, value } = await reader.read(new Uint8Array(1024)); + if (done) break; + n += value.byteLength; + } + return n; +}); + +if (typeof Bun !== "undefined") { + bench(`Bun.readableStreamToText x ${CHUNKS}`, async () => { + return (await Bun.readableStreamToText(new ReadableStream(sourceOf(CHUNKS)))).length; + }); + bench(`Bun.readableStreamToArray x ${CHUNKS}`, async () => { + return (await Bun.readableStreamToArray(new ReadableStream(sourceOf(CHUNKS)))).length; + }); +} + +await run(); diff --git a/scripts/build/unified.ts b/scripts/build/unified.ts index 509bf4b8ae4b..f6c848be3018 100644 --- a/scripts/build/unified.ts +++ b/scripts/build/unified.ts @@ -147,6 +147,18 @@ const noUnify: readonly string[] = [ "src/jsc/bindings/image_wic_shim.cpp", ]; +/** + * Directories whose every .cpp compiles standalone (repo-root-relative, + * posix-style, no trailing slash). Same semantics as `noUnify`, per-directory. + * The streams entry exists because its sibling TUs repeat file-local static + * helper names; it can be lifted once those are deduplicated. + */ +const noUnifyDirs: readonly string[] = [ + // One WHATWG spec algorithm group per TU, each with file-local static + // helpers written assuming TU isolation; a unified bundle collides them. + "src/jsc/bindings/webcore/streams", +]; + /** * How many .cpp files per bundle. WebKit defaults to 8. * @@ -211,11 +223,11 @@ export function generateUnifiedSources(cfg: Config, cxxSources: readonly string[ } // slash(): noUnify keys and the dir tag below are posix-style. const rel = slash(relative(cfg.cwd, abs)); - if (skip.has(rel)) { + const dir = dirname(rel); + if (skip.has(rel) || noUnifyDirs.includes(dir)) { standalone.push(abs); continue; } - const dir = dirname(rel); let arr = byDir.get(dir); if (arr === undefined) byDir.set(dir, (arr = [])); arr.push(abs); diff --git a/scripts/glob-sources.ts b/scripts/glob-sources.ts index aaca9a81249d..9848a8c000f8 100644 --- a/scripts/glob-sources.ts +++ b/scripts/glob-sources.ts @@ -90,6 +90,7 @@ const patterns = { "src/jsc/modules/*.cpp", "src/jsc/bindings/*.cpp", "src/jsc/bindings/webcore/*.cpp", + "src/jsc/bindings/webcore/streams/*.cpp", "src/jsc/bindings/sqlite/*.cpp", "src/jsc/bindings/webcrypto/*.cpp", "src/jsc/bindings/webcrypto/*/*.cpp", diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index 808fd6239441..dea2a643525c 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -208,7 +208,6 @@ extern "C" bool JSSink_isSink(JSC::JSGlobalObject*, JSC::EncodedJSValue); namespace WebCore { using namespace JSC; -JSC_DECLARE_HOST_FUNCTION(functionStartDirectStream); `; const bottom = ` @@ -276,7 +275,6 @@ async function implementation() { // #include #include -#include "JSReadableStream.h" #include "BunClientData.h" #include #include @@ -287,67 +285,9 @@ using namespace JSC; ${classes.map(name => `extern "C" size_t ${name}__memoryCost(void* sinkPtr);`).join("\n")} ${classes.map(name => `extern "C" void ${name}__controllerDetached(void* sinkPtr, JSC::EncodedJSValue controllerValue);`).join("\n")} - -JSC_DEFINE_HOST_FUNCTION(functionStartDirectStream, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame *callFrame)) -{ - - auto& vm = lexicalGlobalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - Zig::GlobalObject* globalObject = reinterpret_cast(lexicalGlobalObject); - - JSC::JSValue readableStream = callFrame->argument(0); - JSC::JSValue onPull = callFrame->argument(1); - JSC::JSValue onClose = callFrame->argument(2); - JSC::JSValue asyncContext = callFrame->argument(3); - - if (!readableStream.isObject()) { - scope.throwException(globalObject, JSC::createTypeError(globalObject, "Expected ReadableStream"_s)); - return JSC::JSValue::encode(JSC::jsUndefined()); - } - - if (!onPull.isObject() || !onPull.isCallable()) { - onPull = JSC::jsUndefined(); - } else if (!asyncContext.isUndefined()) { - onPull = AsyncContextFrame::create(globalObject, onPull, asyncContext); - } - - if (!onClose.isObject() || !onClose.isCallable()) { - onClose = JSC::jsUndefined(); - } else if (!asyncContext.isUndefined()) { - onClose = AsyncContextFrame::create(globalObject, onClose, asyncContext); - } - `; var templ = head; - var isFirst = true; - for (let name of classes) { - const { className, controller, prototypeName, controllerPrototypeName, constructor } = names(name); - - templ += ` - - ${isFirst ? "" : "else"} if (WebCore::${controller}* ${name}Controller = dynamicDowncast(callFrame->thisValue())) { - if (${name}Controller->wrapped() == nullptr) { - scope.throwException(globalObject, JSC::createTypeError(globalObject, "Cannot start stream with closed controller"_s)); - return JSC::JSValue::encode(JSC::jsUndefined()); - } - - ${name}Controller->start(globalObject, readableStream, onPull, onClose); - } -`; - isFirst = false; - } - - templ += ` - else { - scope.throwException(globalObject, JSC::createTypeError(globalObject, "Unknown direct controller. This is a bug in Bun."_s)); - return JSC::JSValue::encode(JSC::jsUndefined()); - } - - RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::jsUndefined())); -} -`; - for (let name of classes) { const { className, diff --git a/src/js/CLAUDE.md b/src/js/CLAUDE.md index eb0f11627685..e4f321251b95 100644 --- a/src/js/CLAUDE.md +++ b/src/js/CLAUDE.md @@ -30,17 +30,10 @@ export default { ## Writing Builtin Functions ```typescript -export function initializeReadableStream( - this: ReadableStream, - underlyingSource, - strategy, -) { - if (!$isObject(underlyingSource)) { - throw new TypeError( - "ReadableStream constructor takes an object as first argument", - ); - } - $putByIdDirectPrivate(this, "state", $streamReadable); +// Fifo.ts +export function createFIFO(): Dequeue { + const Dequeue = require("internal/fifo"); + return new Dequeue(); } ``` @@ -48,7 +41,7 @@ C++ access: ```cpp object->putDirectBuiltinFunction(vm, globalObject, identifier, - readableStreamInitializeReadableStreamCodeGenerator(vm), 0); + fifoCreateFIFOCodeGenerator(vm), 0); ``` ## $ Globals and Special Syntax diff --git a/src/js/README.md b/src/js/README.md index f01fbd32d216..bf58933a1b80 100644 --- a/src/js/README.md +++ b/src/js/README.md @@ -76,9 +76,9 @@ object->putDirectBuiltinFunction( vm, globalObject, identifier, - // ReadableStream.ts, `function readableStreamToJSON()` + // Fifo.ts, `function createFIFO()` // This returns a FunctionExecutable* (extends JSCell*, but not JSFunction*). - readableStreamReadableStreamToJSONCodeGenerator(vm), + fifoCreateFIFOCodeGenerator(vm), JSC::PropertyAttribute::DontDelete | 0 ); ``` diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 7b86c3557ed8..fecc365fecb1 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -60,45 +60,11 @@ declare var $sloppy; /** Place this directly above a function declaration (like a decorator) to always inline the function */ declare var $alwaysInline; -declare function $extractHighWaterMarkFromQueuingStrategyInit(obj: any): any; /** * Overrides ** */ -class ReadableStreamDefaultController extends _ReadableStreamDefaultController { - constructor( - stream: unknown, - underlyingSource: unknown, - size: unknown, - highWaterMark: unknown, - $isReadableStream: typeof $isReadableStream, - ); - - $controlledReadableStream: ReadableStream; - $underlyingSource: UnderlyingSource; - $queue: any; - $started: number; - $closeRequested: boolean; - $pullAgain: boolean; - $pulling: boolean; - $strategy: any; - - $pullAlgorithm(): void; - $pull: typeof ReadableStreamDefaultController.prototype.pull; - $cancel: typeof ReadableStreamDefaultController.prototype.cancel; - $cancelAlgorithm: (reason?: any) => void; - $close: typeof ReadableStreamDefaultController.prototype.close; - $enqueue: typeof ReadableStreamDefaultController.prototype.enqueue; - $error: typeof ReadableStreamDefaultController.prototype.error; -} - -interface ReadableStream extends _ReadableStream { - $highWaterMark: number; - $bunNativePtr: undefined | TODO; - $asyncContext?: {}; - $disturbed: boolean; - $state: $streamClosed | $streamErrored | $streamReadable | $streamWritable | $streamClosedAndErrored; -} +interface ReadableStream extends _ReadableStream {} declare var ReadableStream: { prototype: ReadableStream; @@ -346,38 +312,20 @@ declare const $asyncContext: InternalFieldObject<[ReadonlyArray | undefined // We define our intrinsics in ./BunBuiltinNames.h. Some of those are globals. declare var $_events: TODO; -declare function $abortAlgorithm(): TODO; -declare function $abortSteps(): TODO; declare function $addAbortAlgorithmToSignal(signal: AbortSignal, algorithm: () => void): TODO; -declare function $assignToStream(): TODO; -declare function $assignStreamIntoResumableSink(): TODO; -declare function $associatedReadableByteStreamController(): TODO; declare function $autoAllocateChunkSize(): TODO; -declare function $backpressure(): TODO; -declare function $backpressureChangePromise(): TODO; declare function $basename(): TODO; declare function $body(): TODO; declare function $bunNativePtr(): TODO; declare function $bunNativeType(): TODO; declare function $byobRequest(): TODO; declare function $cancel(): TODO; -declare function $cancelAlgorithm(): TODO; declare function $cloneArrayBuffer(a, b, c): TODO; declare function $close(): TODO; -declare function $closeAlgorithm(): TODO; -declare function $closeRequest(): TODO; -declare function $closeRequested(): TODO; -declare function $closedPromise(): TODO; -declare function $closedPromiseCapability(): TODO; declare function $code(): TODO; -declare function $controlledReadableStream(): TODO; declare function $controller(): TODO; -declare function $createEmptyReadableStream(): TODO; -declare function $createErroredReadableStream(reason: unknown): TODO; declare function $createFIFO(): TODO; -declare function $createNativeReadableStream(): TODO; declare function $createUninitializedArrayBuffer(size: number): ArrayBuffer; -declare function $createWritableStreamFromInternal(...args: any[]): TODO; declare function $data(): TODO; declare function $dataView(): TODO; declare function $decode(): TODO; @@ -386,12 +334,10 @@ declare function $disturbed(): TODO; declare function $encoding(): TODO; declare function $end(): TODO; declare function $errno(): TODO; -declare function $errorSteps(): TODO; declare function $extname(): TODO; declare function $fatal(): TODO; declare function $filePath(): TODO; declare function $filter(): TODO; -declare function $flushAlgorithm(): TODO; declare function $format(): TODO; declare function $fulfillModuleSync(key: string): void; declare function $esmNamespaceForCjs(key: string): any | undefined; @@ -399,7 +345,6 @@ declare function $esmRegistryDelete(key: string): boolean; declare function $esmRegistryEvaluatedKeys(): string[]; declare function $esmLoadSync(key: string): any; declare function $get(): TODO; -declare function $getInternalWritableStream(writable: WritableStream): TODO; declare function $handleEvent(): TODO; declare function $headers(): TODO; declare function $highWaterMark(): TODO; @@ -407,14 +352,10 @@ declare function $host(): TODO; declare function $hostname(): TODO; declare function $ignoreBOM(): TODO; declare function $importer(): TODO; -declare function $inFlightCloseRequest(): TODO; -declare function $inFlightWriteRequest(): TODO; declare function $internalRequire(id: string, parent: JSCommonJSModule): TODO; -declare function $internalWritable(): TODO; declare function $isAbortSignal(signal: unknown): signal is AbortSignal; declare function $isAbsolute(): TODO; declare function $join(): TODO; -declare const $lazyStreamPrototypeMap: Map; declare function $loadModule(): TODO; declare function $main(): TODO; declare function $makeDOMException(): TODO; @@ -422,26 +363,13 @@ declare function $makeGetterTypeError(className: string, prop: string): Error; declare function $map(): TODO; declare function $method(): TODO; declare function $normalize(): TODO; -declare function $ownerReadableStream(): TODO; declare function $parse(): TODO; declare function $path(): TODO; -declare function $pendingAbortRequest(): TODO; -declare function $pendingPullIntos(): TODO; declare function $port(): TODO; declare function $post(): TODO; declare function $pull(): TODO; -declare function $pullAgain(): TODO; -declare function $pullAlgorithm(): TODO; -declare function $pulling(): TODO; -declare function $queue(): TODO; declare function $read(): TODO; -declare function $readIntoRequests(): TODO; -declare function $readRequests(): TODO; declare function $readable(): TODO; -declare function $readableByteStreamControllerGetDesiredSize(...args: any): TODO; -declare function $readableStreamController(): TODO; -declare function $reader(): TODO; -declare function $readyPromise(): TODO; declare function $removeAbortAlgorithmFromSignal(signal: AbortSignal, algorithmIdentifier: number): TODO; declare function $redirect(): TODO; declare function $relative(): TODO; @@ -461,43 +389,26 @@ declare function $resume(): TODO; declare function $search(): TODO; declare function $searchParams(): TODO; declare function $self(): TODO; -declare function $sink(): TODO; declare function $size(): TODO; declare function $start(): TODO; -declare function $startAlgorithm(): TODO; -declare function $startDirectStream(): TODO; declare function $started(): TODO; declare function $state(): TODO; declare function $status(): TODO; -declare function $storedError(): TODO; -declare function $strategy(): TODO; -declare function $strategyHWM(): TODO; -declare function $strategySizeAlgorithm(): TODO; declare function $stream(): TODO; declare function $streamClosed(): TODO; -declare function $streamClosing(): TODO; declare function $streamErrored(): TODO; declare function $streamReadable(): TODO; -declare function $streamWaiting(): TODO; declare function $streamWritable(): TODO; declare function $structuredCloneForStream(): TODO; declare function $syscall(): TODO; declare function $textDecoderStreamDecoder(): TODO; -declare function $textDecoderStreamTransform(): TODO; declare function $textEncoderStreamEncoder(): TODO; -declare function $textEncoderStreamTransform(): TODO; declare function $toNamespacedPath(): TODO; -declare function $transformAlgorithm(): TODO; -declare function $underlyingByteSource(): TODO; -declare function $underlyingSink(): TODO; -declare function $underlyingSource(): TODO; declare function $url(): TODO; declare function $view(): TODO; declare function $whenSignalAborted(signal: AbortSignal, cb: (reason: any) => void): TODO; declare function $writable(): TODO; declare function $write(): TODO; -declare function $writeAlgorithm(): TODO; -declare function $writeRequests(): TODO; declare function $writer(): TODO; declare function $written(): TODO; @@ -550,19 +461,15 @@ declare class OutOfMemoryError { constructor(); } +// Provided by the C++ Web Streams implementation. declare class ReadableByteStreamController { - constructor( - stream: unknown, - underlyingSource: unknown, - strategy: unknown, - $isReadableStream: typeof $isReadableStream, - ); + private constructor(); } declare class ReadableStreamBYOBRequest { - constructor(stream: unknown, view: unknown, $isReadableStream: typeof $isReadableStream); + private constructor(); } declare class ReadableStreamBYOBReader { - constructor(stream: unknown); + constructor(stream: ReadableStream); } // Inlining our enum types diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 9bc51bdea898..496ca899a102 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -22,6 +22,7 @@ using namespace JSC; // Keep this list sorted. #define BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME(macro) \ + macro($$typeof) \ macro(AbortSignal) \ macro(Buffer) \ macro(Loader) \ @@ -38,47 +39,39 @@ using namespace JSC; macro(WritableStream) \ macro(WritableStreamDefaultController) \ macro(WritableStreamDefaultWriter) \ + macro(_debugInfo) \ + macro(_debugStack) \ + macro(_debugTask) \ macro(_events) \ - macro(abortAlgorithm) \ - macro(abortSteps) \ + macro(_owner) \ + macro(_store) \ + macro(abort) \ macro(addAbortAlgorithmToSignal) \ - macro(assignToStream) \ - macro(associatedReadableByteStreamController) \ + macro(arrayBuffer) \ + macro(asUint8Array) \ macro(atimeMs) \ macro(attributes) \ macro(autoAllocateChunkSize) \ - macro(backpressure) \ - macro(backpressureChangePromise) \ macro(basename) \ macro(birthtimeMs) \ + macro(blob) \ macro(body) \ macro(bunNativePtr) \ macro(bunNativeType) \ macro(byobRequest) \ + macro(bytes) \ macro(cancel) \ - macro(cancelAlgorithm) \ - macro(checks) \ macro(checkBufferRead) \ + macro(checks) \ macro(cloneArrayBuffer) \ macro(close) \ - macro(closeAlgorithm) \ - macro(closeRequest) \ - macro(closeRequested) \ - macro(closedPromise) \ - macro(closedPromiseCapability) \ macro(cmd) \ macro(code) \ - macro(controlledReadableStream) \ macro(controller) \ macro(createCommonJSModule) \ - macro(createEmptyReadableStream) \ - macro(createErroredReadableStream) \ macro(createFIFO) \ macro(createInternalModuleById) \ - macro(createNativeReadableStream) \ macro(createUninitializedArrayBuffer) \ - macro(createUsedReadableStream) \ - macro(createWritableStreamFromInternal) \ macro(ctimeMs) \ macro(data) \ macro(dataView) \ @@ -87,10 +80,15 @@ using namespace JSC; macro(dirname) \ macro(disturbed) \ macro(domain) \ + macro(drain) \ + macro(encode) \ macro(encoding) \ macro(end) \ macro(errno) \ - macro(errorSteps) \ + macro(esmLoadSync) \ + macro(esmNamespaceForCjs) \ + macro(esmRegistryDelete) \ + macro(esmRegistryEvaluatedKeys) \ macro(evaluateCommonJSModule) \ macro(evictIsolationSourceProviderCache) \ macro(expires) \ @@ -100,14 +98,9 @@ using namespace JSC; macro(fatal) \ macro(fd) \ macro(filename) \ - macro(flushAlgorithm) \ + macro(flush) \ macro(format) \ macro(fulfillModuleSync) \ - macro(esmNamespaceForCjs) \ - macro(esmRegistryDelete) \ - macro(esmRegistryEvaluatedKeys) \ - macro(esmLoadSync) \ - macro(getInternalWritableStream) \ macro(handleEvent) \ macro(headers) \ macro(highWaterMark) \ @@ -117,17 +110,15 @@ using namespace JSC; macro(httpOnly) \ macro(ignoreBOM) \ macro(importer) \ - macro(inFlightCloseRequest) \ - macro(inFlightWriteRequest) \ macro(inherits) \ macro(internalModuleRegistry) \ macro(internalRequire) \ - macro(internalWritable) \ macro(isAbortSignal) \ macro(isAbsolute) \ macro(join) \ + macro(json) \ + macro(key) \ macro(lazy) \ - macro(lazyStreamPrototypeMap) \ macro(lineText) \ macro(loadEsmIntoCjs) \ macro(main) \ @@ -136,42 +127,38 @@ using namespace JSC; macro(makeErrorWithCode) \ macro(makeGetterTypeError) \ macro(maxAge) \ - macro(method) \ macro(metafileJson) \ + macro(method) \ + macro(min) \ macro(mockedFunction) \ macro(mode) \ macro(mtimeMs) \ macro(napiDlopenHandle) \ macro(napiWrappedContents) \ macro(normalize) \ + macro(onClose) \ + macro(onDrain) \ macro(originalColumn) \ macro(originalLine) \ macro(overridableRequire) \ - macro(ownerReadableStream) \ macro(parse) \ macro(partitioned) \ macro(path) \ macro(paths) \ macro(peekPromiseSettledValue) \ macro(peekPromiseStatus) \ - macro(pendingAbortRequest) \ - macro(pendingPullIntos) \ macro(pokePromiseAsHandled) \ macro(port) \ macro(post) \ + macro(preventAbort) \ + macro(preventCancel) \ + macro(preventClose) \ macro(processBindingConstants) \ + macro(props) \ macro(pull) \ - macro(pullAgain) \ - macro(pullAlgorithm) \ - macro(pulling) \ - macro(queue) \ macro(read) \ - macro(readIntoRequests) \ - macro(readRequests) \ macro(readable) \ - macro(readableStreamController) \ - macro(reader) \ - macro(readyPromise) \ + macro(readableType) \ macro(redirect) \ macro(relative) \ macro(removeAbortAlgorithmFromSignal) \ @@ -184,55 +171,37 @@ using namespace JSC; macro(sameSite) \ macro(secure) \ macro(self) \ + macro(setHandlers) \ macro(signal) \ - macro(sink) \ macro(size) \ macro(specifier) \ macro(start) \ - macro(startAlgorithm) \ - macro(startDirectStream) \ macro(started) \ macro(state) \ macro(status) \ macro(statusText) \ - macro(storedError) \ - macro(strategy) \ - macro(strategyHWM) \ - macro(strategySizeAlgorithm) \ macro(stream) \ macro(structuredCloneForStream) \ macro(syscall) \ + macro(text) \ macro(textDecoder) \ macro(textDecoderStreamDecoder) \ - macro(textDecoderStreamTransform) \ macro(textEncoderStreamEncoder) \ - macro(textEncoderStreamTransform) \ macro(toClass) \ macro(toNamespacedPath) \ - macro(transformAlgorithm) \ - macro(underlyingByteSource) \ - macro(underlyingSink) \ - macro(underlyingSource) \ + macro(transform) \ + macro(type) \ + macro(updateRef) \ macro(url) \ + macro(validated) \ macro(view) \ macro(vmErrorDecorated) \ macro(warning) \ macro(writable) \ + macro(writableType) \ macro(write) \ - macro(writeAlgorithm) \ - macro(writeRequests) \ macro(writer) \ macro(written) \ - macro($$typeof) \ - macro(type) \ - macro(key) \ - macro(props) \ - macro(validated) \ - macro(_store) \ - macro(_owner) \ - macro(_debugInfo) \ - macro(_debugStack) \ - macro(_debugTask) \ BUN_ADDITIONAL_BUILTIN_NAMES(macro) // --- END of BUN_COMMON_PRIVATE_IDENTIFIERS_EACH_PROPERTY_NAME --- diff --git a/src/js/builtins/ByteLengthQueuingStrategy.ts b/src/js/builtins/ByteLengthQueuingStrategy.ts deleted file mode 100644 index fc3f3d99802a..000000000000 --- a/src/js/builtins/ByteLengthQueuingStrategy.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * Copyright (C) 2015 Igalia S.L. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -$getter; -export function highWaterMark(this: any) { - const highWaterMark = $getByIdDirectPrivate(this, "highWaterMark"); - if (highWaterMark === undefined) - throw new TypeError("ByteLengthQueuingStrategy.highWaterMark getter called on incompatible |this| value."); - - return highWaterMark; -} - -export function size(chunk) { - return chunk.byteLength; -} - -export function initializeByteLengthQueuingStrategy(this: any, parameters: any) { - $putByIdDirectPrivate(this, "highWaterMark", $extractHighWaterMarkFromQueuingStrategyInit(parameters)); -} diff --git a/src/js/builtins/CountQueuingStrategy.ts b/src/js/builtins/CountQueuingStrategy.ts deleted file mode 100644 index a72dca1ca509..000000000000 --- a/src/js/builtins/CountQueuingStrategy.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -$getter; -export function highWaterMark(this: any) { - const highWaterMark = $getByIdDirectPrivate(this, "highWaterMark"); - - if (highWaterMark === undefined) - throw new TypeError("CountQueuingStrategy.highWaterMark getter called on incompatible |this| value."); - - return highWaterMark; -} - -export function size() { - return 1; -} - -export function initializeCountQueuingStrategy(this: any, parameters: any) { - $putByIdDirectPrivate(this, "highWaterMark", $extractHighWaterMarkFromQueuingStrategyInit(parameters)); -} diff --git a/src/js/builtins/Fifo.ts b/src/js/builtins/Fifo.ts new file mode 100644 index 000000000000..6daca72929bc --- /dev/null +++ b/src/js/builtins/Fifo.ts @@ -0,0 +1,8 @@ +// @internal + +import type Dequeue from "internal/fifo"; +$linkTimeConstant; +export function createFIFO(): Dequeue { + const Dequeue = require("internal/fifo"); + return new Dequeue(); +} diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index 1716fe343d6b..5206f8d7f10f 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -122,7 +122,6 @@ export function getStdinStream( var reader: ReadableStreamDefaultReader | undefined; - var shouldDisown = false; let needsInternalReadRefresh = false; // if true, while the stream is own()ed it will not let forceUnref = false; @@ -136,7 +135,6 @@ export function getStdinStream( source.updateRef(forceUnref ? false : true); source?.setFlowing?.(true); - shouldDisown = false; if (needsInternalReadRefresh) { needsInternalReadRefresh = false; internalRead(stream); @@ -148,22 +146,13 @@ export function getStdinStream( source?.setFlowing?.(false); if (reader) { - try { - reader.releaseLock(); - reader = undefined; - $debug("released reader"); - } catch (e: any) { - $debug("reader lock cannot be released, waiting"); - $assert(e.message === "There are still pending read requests, cannot release the lock"); - - // Releasing the lock is not possible as there are active reads - // we will instead pretend we are unref'd, and release the lock once the reads are finished. - shouldDisown = true; - source?.updateRef?.(false); - } - } else if (source) { - source.updateRef(false); + // releaseLock() rejects any in-flight internalRead() with a TypeError; that + // rejection is handled there by observing that `reader` was cleared here. + reader.releaseLock(); + reader = undefined; + $debug("released reader"); } + source?.updateRef?.(false); } const ReadStream = isTTY ? require("node:tty").ReadStream : require("node:fs").ReadStream; @@ -232,14 +221,16 @@ export function getStdinStream( async function internalRead(stream) { $debug("internalRead();"); + // The reader this read belongs to. releaseLock() rejects the in-flight read(); by the + // time that rejection lands, own() may already have acquired a NEW reader, so the catch + // must key on this acquisition rather than on the current `reader`. + const readerForThisRead = reader; try { - $assert(reader); - const { value } = await reader.read(); + $assert(readerForThisRead); + const { value } = await readerForThisRead.read(); if (value) { stream.push(value); - - if (shouldDisown) disown(); } else { // EOF. Nothing is left to read, so release the native reader before // push(null) runs user 'readable' listeners; the process must be able @@ -252,10 +243,10 @@ export function getStdinStream( stream.push(null); } } catch (err) { - if (err?.code === "ERR_STREAM_RELEASE_LOCK") { - // The stream was unref()ed. It may be ref()ed again in the future, - // or maybe it has already been ref()ed again and we just need to - // restart the internalRead() function. triggerRead() will figure that out. + if (readerForThisRead !== reader) { + // disown() released this read's reader while it was in flight (stdin may have been + // re-owned since), so the read rejected because the stream was unref()ed, not + // because it failed. triggerRead() re-arms if/when it is ref()ed again. triggerRead.$call(stream, undefined); return; } @@ -266,7 +257,7 @@ export function getStdinStream( function triggerRead(_size) { $debug("_read();", reader); - if (reader && !shouldDisown) { + if (reader) { internalRead(this); } else { // The stream has not been ref()ed yet. If it is ever ref()ed, diff --git a/src/js/builtins/ReadableByteStreamController.ts b/src/js/builtins/ReadableByteStreamController.ts deleted file mode 100644 index e6cb46ef433a..000000000000 --- a/src/js/builtins/ReadableByteStreamController.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2016 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeReadableByteStreamController(this, stream, underlyingByteSource, highWaterMark) { - if (arguments.length !== 4 && arguments[3] !== $isReadableStream) - throw new TypeError("ReadableByteStreamController constructor should not be called directly"); - - return $privateInitializeReadableByteStreamController.$call(this, stream, underlyingByteSource, highWaterMark); -} - -export function enqueue(this: ReadableByteStreamController, chunk: ArrayBufferView) { - if (!$isReadableByteStreamController(this)) throw $ERR_INVALID_THIS("ReadableByteStreamController"); - - if ($getByIdDirectPrivate(this, "closeRequested")) throw $ERR_INVALID_STATE_TypeError("Controller is already closed"); - - if ($getByIdDirectPrivate($getByIdDirectPrivate(this, "controlledReadableStream"), "state") !== $streamReadable) - throw $ERR_INVALID_STATE_TypeError("Controller is already closed"); - - if (!$isObject(chunk) || !ArrayBuffer.$isView(chunk)) - throw $ERR_INVALID_ARG_TYPE("buffer", "Buffer, TypedArray, or DataView", chunk); - - return $readableByteStreamControllerEnqueue(this, chunk); -} - -export function error(this: ReadableByteStreamController, error: any) { - if (!$isReadableByteStreamController(this)) throw $ERR_INVALID_THIS("ReadableByteStreamController"); - - if ($getByIdDirectPrivate($getByIdDirectPrivate(this, "controlledReadableStream"), "state") !== $streamReadable) - throw $ERR_INVALID_STATE_TypeError("Controller is already closed"); - - $readableByteStreamControllerError(this, error); -} - -export function close(this: ReadableByteStreamController) { - if (!$isReadableByteStreamController(this)) throw $ERR_INVALID_THIS("ReadableByteStreamController"); - - if ($getByIdDirectPrivate(this, "closeRequested")) throw new TypeError("Close has already been requested"); - - if ($getByIdDirectPrivate($getByIdDirectPrivate(this, "controlledReadableStream"), "state") !== $streamReadable) - throw $ERR_INVALID_STATE_TypeError("Controller is already closed"); - - $readableByteStreamControllerClose(this); -} - -$getter; -export function byobRequest(this) { - if (!$isReadableByteStreamController(this)) throw $makeGetterTypeError("ReadableByteStreamController", "byobRequest"); - - var request = $getByIdDirectPrivate(this, "byobRequest"); - if (request === undefined) { - var pending = $getByIdDirectPrivate(this, "pendingPullIntos"); - const firstDescriptor = pending.peek(); - if (firstDescriptor) { - const view = new Uint8Array( - firstDescriptor.buffer, - firstDescriptor.byteOffset + firstDescriptor.bytesFilled, - firstDescriptor.byteLength - firstDescriptor.bytesFilled, - ); - $putByIdDirectPrivate(this, "byobRequest", new ReadableStreamBYOBRequest(this, view, $isReadableStream)); - } - } - - return $getByIdDirectPrivate(this, "byobRequest"); -} - -$getter; -export function desiredSize(this) { - if (!$isReadableByteStreamController(this)) throw $makeGetterTypeError("ReadableByteStreamController", "desiredSize"); - - return $readableByteStreamControllerGetDesiredSize(this); -} diff --git a/src/js/builtins/ReadableByteStreamInternals.ts b/src/js/builtins/ReadableByteStreamInternals.ts deleted file mode 100644 index afb553c462be..000000000000 --- a/src/js/builtins/ReadableByteStreamInternals.ts +++ /dev/null @@ -1,731 +0,0 @@ -/// -/** - * ## References - * - [ReadableStream - `ReadableByteStreamController`](https://streams.spec.whatwg.org/#rbs-controller-class) - */ -/* - * Copyright (C) 2016 Canon Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -// @internal - -export function privateInitializeReadableByteStreamController(this, stream, underlyingByteSource, highWaterMark) { - if (!$isReadableStream(stream)) throw new TypeError("ReadableByteStreamController needs a ReadableStream"); - - // readableStreamController is initialized with null value. - if ($getByIdDirectPrivate(stream, "readableStreamController") !== null) - throw new TypeError("ReadableStream already has a controller"); - - $putByIdDirectPrivate(this, "controlledReadableStream", stream); - $putByIdDirectPrivate(this, "underlyingByteSource", underlyingByteSource); - $putByIdDirectPrivate(this, "pullAgain", false); - $putByIdDirectPrivate(this, "pulling", false); - $readableByteStreamControllerClearPendingPullIntos(this); - $putByIdDirectPrivate(this, "queue", $newQueue()); - $putByIdDirectPrivate(this, "started", 0); - $putByIdDirectPrivate(this, "closeRequested", false); - - let hwm = $toNumber(highWaterMark); - if (hwm !== hwm || hwm < 0) throw new RangeError("highWaterMark value is negative or not a number"); - $putByIdDirectPrivate(this, "strategyHWM", hwm); - - let autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize; - if (autoAllocateChunkSize !== undefined) { - autoAllocateChunkSize = $toNumber(autoAllocateChunkSize); - if (autoAllocateChunkSize <= 0 || autoAllocateChunkSize === Infinity || autoAllocateChunkSize === -Infinity) - throw new RangeError("autoAllocateChunkSize value is negative or equal to positive or negative infinity"); - } - $putByIdDirectPrivate(this, "autoAllocateChunkSize", autoAllocateChunkSize); - $putByIdDirectPrivate(this, "pendingPullIntos", $createFIFO()); - - const controller = this; - $promiseInvokeOrNoopNoCatch($getByIdDirectPrivate(controller, "underlyingByteSource"), "start", [controller]).$then( - () => { - $putByIdDirectPrivate(controller, "started", 1); - $assert(!$getByIdDirectPrivate(controller, "pulling")); - $assert(!$getByIdDirectPrivate(controller, "pullAgain")); - $readableByteStreamControllerCallPullIfNeeded(controller); - }, - error => { - if ($getByIdDirectPrivate(stream, "state") === $streamReadable) - $readableByteStreamControllerError(controller, error); - }, - ); - - $putByIdDirectPrivate(this, "cancel", $readableByteStreamControllerCancel); - $putByIdDirectPrivate(this, "pull", $readableByteStreamControllerPull); - - return this; -} - -export function readableStreamByteStreamControllerStart(this, controller) { - $putByIdDirectPrivate(controller, "start", undefined); -} - -export function privateInitializeReadableStreamBYOBRequest(this, controller, view) { - $putByIdDirectPrivate(this, "associatedReadableByteStreamController", controller); - $putByIdDirectPrivate(this, "view", view); -} - -export function isReadableByteStreamController(controller) { - // Same test mechanism as in isReadableStreamDefaultController (ReadableStreamInternals.js). - // See corresponding function for explanations. - return $isObject(controller) && !!$getByIdDirectPrivate(controller, "underlyingByteSource"); -} - -export function isReadableStreamBYOBRequest(byobRequest) { - // Same test mechanism as in isReadableStreamDefaultController (ReadableStreamInternals.js). - // See corresponding function for explanations. - return ( - $isObject(byobRequest) && $getByIdDirectPrivate(byobRequest, "associatedReadableByteStreamController") !== undefined - ); -} - -export function isReadableStreamBYOBReader(reader) { - // Spec tells to return true only if reader has a readIntoRequests internal slot. - // However, since it is a private slot, it cannot be checked using hasOwnProperty(). - // Since readIntoRequests is initialized with an empty array, the following test is ok. - return $isObject(reader) && !!$getByIdDirectPrivate(reader, "readIntoRequests"); -} - -export function readableByteStreamControllerCancel(controller, reason) { - var pendingPullIntos = $getByIdDirectPrivate(controller, "pendingPullIntos"); - var first: PullIntoDescriptor | undefined = pendingPullIntos.peek(); - if (first) first.bytesFilled = 0; - - $putByIdDirectPrivate(controller, "queue", $newQueue()); - return $promiseInvokeOrNoop($getByIdDirectPrivate(controller, "underlyingByteSource"), "cancel", [reason]); -} - -export function readableByteStreamControllerError(controller, e) { - $assert( - $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable, - ); - $readableByteStreamControllerClearPendingPullIntos(controller); - $putByIdDirectPrivate(controller, "queue", $newQueue()); - $readableStreamError($getByIdDirectPrivate(controller, "controlledReadableStream"), e); -} - -export function readableByteStreamControllerClose(controller) { - $assert(!$getByIdDirectPrivate(controller, "closeRequested")); - $assert( - $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable, - ); - - if ($getByIdDirectPrivate(controller, "queue").size > 0) { - $putByIdDirectPrivate(controller, "closeRequested", true); - return; - } - - var first: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos")?.peek(); - if (first) { - if (first.bytesFilled > 0) { - const e = $makeTypeError("Close requested while there remain pending bytes"); - $readableByteStreamControllerError(controller, e); - throw e; - } - } - - $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); -} - -export function readableByteStreamControllerClearPendingPullIntos(controller) { - $readableByteStreamControllerInvalidateBYOBRequest(controller); - var existing: Dequeue = $getByIdDirectPrivate(controller, "pendingPullIntos"); - if (existing !== undefined) { - existing.clear(); - } else { - $putByIdDirectPrivate(controller, "pendingPullIntos", $createFIFO()); - } -} - -export function readableByteStreamControllerGetDesiredSize(controller) { - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - const state = $getByIdDirectPrivate(stream, "state"); - - if (state === $streamErrored) return null; - if (state === $streamClosed) return 0; - - return $getByIdDirectPrivate(controller, "strategyHWM") - $getByIdDirectPrivate(controller, "queue").size; -} - -export function readableStreamHasBYOBReader(stream) { - const reader = $getByIdDirectPrivate(stream, "reader"); - return reader !== undefined && $isReadableStreamBYOBReader(reader); -} - -export function readableStreamHasDefaultReader(stream) { - const reader = $getByIdDirectPrivate(stream, "reader"); - return reader !== undefined && $isReadableStreamDefaultReader(reader); -} - -export function readableByteStreamControllerHandleQueueDrain(controller) { - $assert( - $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === $streamReadable, - ); - if (!$getByIdDirectPrivate(controller, "queue").size && $getByIdDirectPrivate(controller, "closeRequested")) - $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); - else $readableByteStreamControllerCallPullIfNeeded(controller); -} - -export function readableByteStreamControllerPull(controller) { - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - $assert($readableStreamHasDefaultReader(stream)); - if ($getByIdDirectPrivate(controller, "queue").content?.isNotEmpty()) { - const entry = $getByIdDirectPrivate(controller, "queue").content.shift(); - $getByIdDirectPrivate(controller, "queue").size -= entry.byteLength; - $readableByteStreamControllerHandleQueueDrain(controller); - let view; - try { - view = new Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength); - } catch (error) { - return Promise.$reject(error); - } - return $createFulfilledPromise({ value: view, done: false }); - } - - if ($getByIdDirectPrivate(controller, "autoAllocateChunkSize") !== undefined) { - let buffer; - try { - buffer = new ArrayBuffer($getByIdDirectPrivate(controller, "autoAllocateChunkSize")); - } catch (error) { - return Promise.$reject(error); - } - const pullIntoDescriptor: PullIntoDescriptor = { - buffer, - byteOffset: 0, - byteLength: $getByIdDirectPrivate(controller, "autoAllocateChunkSize"), - bytesFilled: 0, - elementSize: 1, - ctor: Uint8Array, - readerType: "default", - }; - $getByIdDirectPrivate(controller, "pendingPullIntos").push(pullIntoDescriptor); - } - - const promise = $readableStreamAddReadRequest(stream); - $readableByteStreamControllerCallPullIfNeeded(controller); - return promise; -} - -export function readableByteStreamControllerShouldCallPull(controller) { - $assert(controller); - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - if (!stream) { - return false; - } - - if ($getByIdDirectPrivate(stream, "state") !== $streamReadable) return false; - if ($getByIdDirectPrivate(controller, "closeRequested")) return false; - if (!($getByIdDirectPrivate(controller, "started") > 0)) return false; - const reader = $getByIdDirectPrivate(stream, "reader"); - - if (reader && ($getByIdDirectPrivate(reader, "readRequests")?.isNotEmpty() || !!reader.$bunNativePtr)) return true; - if ( - $readableStreamHasBYOBReader(stream) && - $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readIntoRequests")?.isNotEmpty() - ) - return true; - if ($readableByteStreamControllerGetDesiredSize(controller) > 0) return true; - return false; -} - -export function readableByteStreamControllerCallPullIfNeeded(controller) { - $assert(controller); - if (!$readableByteStreamControllerShouldCallPull(controller)) return; - - if ($getByIdDirectPrivate(controller, "pulling")) { - $putByIdDirectPrivate(controller, "pullAgain", true); - return; - } - - $assert(!$getByIdDirectPrivate(controller, "pullAgain")); - $putByIdDirectPrivate(controller, "pulling", true); - $promiseInvokeOrNoop($getByIdDirectPrivate(controller, "underlyingByteSource"), "pull", [controller]).$then( - () => { - $putByIdDirectPrivate(controller, "pulling", false); - if ($getByIdDirectPrivate(controller, "pullAgain")) { - $putByIdDirectPrivate(controller, "pullAgain", false); - $readableByteStreamControllerCallPullIfNeeded(controller); - } - }, - error => { - if ( - $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === - $streamReadable - ) - $readableByteStreamControllerError(controller, error); - }, - ); -} - -export function transferBufferToCurrentRealm(buffer) { - // FIXME: Determine what should be done here exactly (what is already existing in current - // codebase and what has to be added). According to spec, Transfer operation should be - // performed in order to transfer buffer to current realm. For the moment, simply return - // received buffer. - return buffer; -} - -export function readableStreamReaderKind(reader) { - if (!!$getByIdDirectPrivate(reader, "readRequests")) return reader.$bunNativePtr ? 3 : 1; - - if (!!$getByIdDirectPrivate(reader, "readIntoRequests")) return 2; - - return 0; -} - -export function readableByteStreamControllerEnqueue(controller, chunk) { - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - $assert(!$getByIdDirectPrivate(controller, "closeRequested")); - $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); - - switch ( - $getByIdDirectPrivate(stream, "reader") ? $readableStreamReaderKind($getByIdDirectPrivate(stream, "reader")) : 0 - ) { - /* default reader */ - case 1: { - if (!$getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty()) - $readableByteStreamControllerEnqueueChunk( - controller, - $transferBufferToCurrentRealm(chunk.buffer), - chunk.byteOffset, - chunk.byteLength, - ); - else { - $assert(!$getByIdDirectPrivate(controller, "queue").content.size()); - const transferredView = - chunk.constructor === Uint8Array ? chunk : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); - $readableStreamFulfillReadRequest(stream, transferredView, false); - } - break; - } - - /* BYOB */ - case 2: { - $readableByteStreamControllerEnqueueChunk( - controller, - $transferBufferToCurrentRealm(chunk.buffer), - chunk.byteOffset, - chunk.byteLength, - ); - $readableByteStreamControllerProcessPullDescriptors(controller); - break; - } - - /* NativeReader */ - case 3: { - // reader.$enqueueNative($getByIdDirectPrivate(reader, "bunNativePtr"), chunk); - - break; - } - - default: { - $assert(!$isReadableStreamLocked(stream)); - $readableByteStreamControllerEnqueueChunk( - controller, - $transferBufferToCurrentRealm(chunk.buffer), - chunk.byteOffset, - chunk.byteLength, - ); - break; - } - } -} - -// Spec name: readableByteStreamControllerEnqueueChunkToQueue. -export function readableByteStreamControllerEnqueueChunk(controller, buffer, byteOffset, byteLength) { - $getByIdDirectPrivate(controller, "queue").content.push({ - buffer: buffer, - byteOffset: byteOffset, - byteLength: byteLength, - }); - $getByIdDirectPrivate(controller, "queue").size += byteLength; -} - -export function readableByteStreamControllerRespondWithNewView(controller, view) { - $assert($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()); - - let firstDescriptor: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); - - if (firstDescriptor!.byteOffset + firstDescriptor!.bytesFilled !== view.byteOffset) - throw new RangeError("Invalid value for view.byteOffset"); - - if (firstDescriptor!.byteLength < view.byteLength) throw $ERR_INVALID_ARG_VALUE("view", view); - - firstDescriptor!.buffer = view.buffer; - $readableByteStreamControllerRespondInternal(controller, view.byteLength); -} - -export function readableByteStreamControllerRespond(controller, bytesWritten) { - bytesWritten = $toNumber(bytesWritten); - - if (bytesWritten !== bytesWritten || bytesWritten === Infinity || bytesWritten < 0) - throw new RangeError("bytesWritten has an incorrect value"); - - $assert($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()); - - $readableByteStreamControllerRespondInternal(controller, bytesWritten); -} - -export function readableByteStreamControllerRespondInternal(controller, bytesWritten) { - let firstDescriptor: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); - let stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - - if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { - $readableByteStreamControllerRespondInClosedState(controller, firstDescriptor); - } else { - $assert($getByIdDirectPrivate(stream, "state") === $streamReadable); - $readableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor); - } -} - -export function readableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor) { - if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength) - throw new RangeError("bytesWritten value is too great"); - - $assert( - $getByIdDirectPrivate(controller, "pendingPullIntos").isEmpty() || - $getByIdDirectPrivate(controller, "pendingPullIntos").peek() === pullIntoDescriptor, - ); - $readableByteStreamControllerInvalidateBYOBRequest(controller); - pullIntoDescriptor.bytesFilled += bytesWritten; - - if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize) return; - - $readableByteStreamControllerShiftPendingDescriptor(controller); - const remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize; - - if (remainderSize > 0) { - const end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; - const remainder = $cloneArrayBuffer(pullIntoDescriptor.buffer, end - remainderSize, remainderSize); - $readableByteStreamControllerEnqueueChunk(controller, remainder, 0, remainder.byteLength); - } - - pullIntoDescriptor.buffer = $transferBufferToCurrentRealm(pullIntoDescriptor.buffer); - pullIntoDescriptor.bytesFilled -= remainderSize; - $readableByteStreamControllerCommitDescriptor( - $getByIdDirectPrivate(controller, "controlledReadableStream"), - pullIntoDescriptor, - ); - $readableByteStreamControllerProcessPullDescriptors(controller); -} - -export function readableByteStreamControllerRespondInClosedState(controller, firstDescriptor) { - firstDescriptor.buffer = $transferBufferToCurrentRealm(firstDescriptor.buffer); - $assert(firstDescriptor.bytesFilled === 0); - - if ($readableStreamHasBYOBReader($getByIdDirectPrivate(controller, "controlledReadableStream"))) { - while ( - $getByIdDirectPrivate( - $getByIdDirectPrivate($getByIdDirectPrivate(controller, "controlledReadableStream"), "reader"), - "readIntoRequests", - )?.isNotEmpty() - ) { - let pullIntoDescriptor = $readableByteStreamControllerShiftPendingDescriptor(controller); - $readableByteStreamControllerCommitDescriptor( - $getByIdDirectPrivate(controller, "controlledReadableStream"), - pullIntoDescriptor, - ); - } - } -} - -// Spec name: readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue (shortened for readability). -export function readableByteStreamControllerProcessPullDescriptors(controller) { - $assert(!$getByIdDirectPrivate(controller, "closeRequested")); - while ($getByIdDirectPrivate(controller, "pendingPullIntos").isNotEmpty()) { - if ($getByIdDirectPrivate(controller, "queue").size === 0) return; - let pullIntoDescriptor: PullIntoDescriptor = $getByIdDirectPrivate(controller, "pendingPullIntos").peek(); - if ($readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)) { - $readableByteStreamControllerShiftPendingDescriptor(controller); - $readableByteStreamControllerCommitDescriptor( - $getByIdDirectPrivate(controller, "controlledReadableStream"), - pullIntoDescriptor, - ); - } - } -} - -// Spec name: readableByteStreamControllerFillPullIntoDescriptorFromQueue (shortened for readability). -export function readableByteStreamControllerFillDescriptorFromQueue( - controller, - pullIntoDescriptor: PullIntoDescriptor, -) { - const currentAlignedBytes = - pullIntoDescriptor.bytesFilled - (pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize); - const maxBytesToCopy = - $getByIdDirectPrivate(controller, "queue").size < pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled - ? $getByIdDirectPrivate(controller, "queue").size - : pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled; - const maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy; - const maxAlignedBytes = maxBytesFilled - (maxBytesFilled % pullIntoDescriptor.elementSize); - let totalBytesToCopyRemaining = maxBytesToCopy; - let ready = false; - - if (maxAlignedBytes > currentAlignedBytes) { - totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled; - ready = true; - } - - while (totalBytesToCopyRemaining > 0) { - let headOfQueue = $getByIdDirectPrivate(controller, "queue").content.peek(); - const bytesToCopy = - totalBytesToCopyRemaining < headOfQueue.byteLength ? totalBytesToCopyRemaining : headOfQueue.byteLength; - // Copy appropriate part of pullIntoDescriptor.buffer to headOfQueue.buffer. - // Remark: this implementation is not completely aligned on the definition of CopyDataBlockBytes - // operation of ECMAScript (the case of Shared Data Block is not considered here, but it doesn't seem to be an issue). - const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled; - // FIXME: As indicated in comments of bug 172717, access to set is not safe. However, using prototype.$set.$call does - // not work ($set is undefined). A safe way to do that is needed. - new Uint8Array(pullIntoDescriptor.buffer).set( - new Uint8Array(headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy), - destStart, - ); - - if (headOfQueue.byteLength === bytesToCopy) $getByIdDirectPrivate(controller, "queue").content.shift(); - else { - headOfQueue.byteOffset += bytesToCopy; - headOfQueue.byteLength -= bytesToCopy; - } - - $getByIdDirectPrivate(controller, "queue").size -= bytesToCopy; - $assert( - $getByIdDirectPrivate(controller, "pendingPullIntos").isEmpty() || - $getByIdDirectPrivate(controller, "pendingPullIntos").peek() === pullIntoDescriptor, - ); - $readableByteStreamControllerInvalidateBYOBRequest(controller); - pullIntoDescriptor.bytesFilled += bytesToCopy; - totalBytesToCopyRemaining -= bytesToCopy; - } - - if (!ready) { - $assert($getByIdDirectPrivate(controller, "queue").size === 0); - $assert(pullIntoDescriptor.bytesFilled > 0); - $assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize); - } - - return ready; -} - -// Spec name: readableByteStreamControllerShiftPendingPullInto (renamed for consistency). -export function readableByteStreamControllerShiftPendingDescriptor(controller): PullIntoDescriptor | undefined { - let descriptor: PullIntoDescriptor | undefined = $getByIdDirectPrivate(controller, "pendingPullIntos").shift(); - $readableByteStreamControllerInvalidateBYOBRequest(controller); - return descriptor; -} - -export function readableByteStreamControllerInvalidateBYOBRequest(controller) { - if ($getByIdDirectPrivate(controller, "byobRequest") === undefined) return; - const byobRequest = $getByIdDirectPrivate(controller, "byobRequest"); - $putByIdDirectPrivate(byobRequest, "associatedReadableByteStreamController", null); - $putByIdDirectPrivate(byobRequest, "view", undefined); - $putByIdDirectPrivate(controller, "byobRequest", undefined); -} - -// Spec name: readableByteStreamControllerCommitPullIntoDescriptor (shortened for readability). -export function readableByteStreamControllerCommitDescriptor(stream, pullIntoDescriptor) { - $assert($getByIdDirectPrivate(stream, "state") !== $streamErrored); - let done = false; - if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { - $assert(!pullIntoDescriptor.bytesFilled); - done = true; - } - let filledView = $readableByteStreamControllerConvertDescriptor(pullIntoDescriptor); - if (pullIntoDescriptor.readerType === "default") $readableStreamFulfillReadRequest(stream, filledView, done); - else { - $assert(pullIntoDescriptor.readerType === "byob"); - $readableStreamFulfillReadIntoRequest(stream, filledView, done); - } -} - -// Spec name: readableByteStreamControllerConvertPullIntoDescriptor (shortened for readability). -export function readableByteStreamControllerConvertDescriptor(pullIntoDescriptor) { - $assert(pullIntoDescriptor.bytesFilled <= pullIntoDescriptor.byteLength); - $assert(pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize === 0); - - return new pullIntoDescriptor.ctor( - pullIntoDescriptor.buffer, - pullIntoDescriptor.byteOffset, - pullIntoDescriptor.bytesFilled / pullIntoDescriptor.elementSize, - ); -} - -export function readableStreamFulfillReadIntoRequest(stream, chunk, done) { - const readIntoRequest = $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readIntoRequests").shift(); - $fulfillPromise(readIntoRequest, { value: chunk, done: done }); -} - -export function readableStreamBYOBReaderRead(reader, view) { - const stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); - $assert(!!stream); - - $putByIdDirectPrivate(stream, "disturbed", true); - if ($getByIdDirectPrivate(stream, "state") === $streamErrored) - return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); - - return $readableByteStreamControllerPullInto($getByIdDirectPrivate(stream, "readableStreamController"), view); -} - -export function readableByteStreamControllerPullInto(controller, view) { - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - let elementSize = 1; - // Spec describes that in the case where view is a TypedArray, elementSize - // should be set to the size of an element (e.g. 2 for UInt16Array). For - // DataView, BYTES_PER_ELEMENT is undefined, contrary to the same property - // for TypedArrays. - // FIXME: Getting BYTES_PER_ELEMENT like this is not safe (property is read-only - // but can be modified if the prototype is redefined). A safe way of getting - // it would be to determine which type of ArrayBufferView view is an instance - // of based on typed arrays private variables. However, this is not possible due - // to bug 167697, which prevents access to typed arrays through their private - // names unless public name has already been met before. - const bytesPerElement = view.BYTES_PER_ELEMENT; - if (bytesPerElement !== undefined) elementSize = bytesPerElement; - - // FIXME: Getting constructor like this is not safe. A safe way of getting - // it would be to determine which type of ArrayBufferView view is an instance - // of, and to assign appropriate constructor based on this (e.g. ctor = - // $Uint8Array). However, this is not possible due to bug 167697, which - // prevents access to typed arrays through their private names unless public - // name has already been met before. - const ctor = view.constructor; - - const pullIntoDescriptor: PullIntoDescriptor = { - buffer: view.buffer, - byteOffset: view.byteOffset, - byteLength: view.byteLength, - bytesFilled: 0, - elementSize, - ctor, - readerType: "byob", - }; - - var pending = $getByIdDirectPrivate(controller, "pendingPullIntos"); - if (pending?.isNotEmpty()) { - pullIntoDescriptor.buffer = $transferBufferToCurrentRealm(pullIntoDescriptor.buffer); - pending.push(pullIntoDescriptor); - return $readableStreamAddReadIntoRequest(stream); - } - - if ($getByIdDirectPrivate(stream, "state") === $streamClosed) { - const emptyView = new ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0); - return $createFulfilledPromise({ value: emptyView, done: true }); - } - - if ($getByIdDirectPrivate(controller, "queue").size > 0) { - if ($readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)) { - const filledView = $readableByteStreamControllerConvertDescriptor(pullIntoDescriptor); - $readableByteStreamControllerHandleQueueDrain(controller); - return $createFulfilledPromise({ value: filledView, done: false }); - } - if ($getByIdDirectPrivate(controller, "closeRequested")) { - const e = $makeTypeError("Closing stream has been requested"); - $readableByteStreamControllerError(controller, e); - return Promise.$reject(e); - } - } - - pullIntoDescriptor.buffer = $transferBufferToCurrentRealm(pullIntoDescriptor.buffer); - $getByIdDirectPrivate(controller, "pendingPullIntos").push(pullIntoDescriptor); - const promise = $readableStreamAddReadIntoRequest(stream); - $readableByteStreamControllerCallPullIfNeeded(controller); - return promise; -} - -export function readableStreamAddReadIntoRequest(stream) { - $assert($isReadableStreamBYOBReader($getByIdDirectPrivate(stream, "reader"))); - $assert( - $getByIdDirectPrivate(stream, "state") === $streamReadable || - $getByIdDirectPrivate(stream, "state") === $streamClosed, - ); - - const readRequest = $newPromise(); - $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readIntoRequests").push(readRequest); - - return readRequest; -} - -/** - * ## References - * - [Spec](https://streams.spec.whatwg.org/#pull-into-descriptor) - */ -interface PullIntoDescriptor { - /** - * An {@link ArrayBuffer} - */ - buffer: ArrayBuffer; - - /** - * A nonnegative integer byte offset into the {@link buffer} where the - * underlying byte source will start writing - */ - byteOffset: number; - /** - * A positive integer number of bytes which can be written into the - * {@link buffer} - */ - byteLength: number; - /** - * A nonnegative integer number of bytes that have been written into the - * {@link buffer} so far - */ - bytesFilled: number; - - /** - * A positive integer representing the number of bytes that can be written - * into the {@link buffer} at a time, using views of the type described by the - * view constructor - */ - elementSize: number; - /** - * `view constructor` - * - * A {@link NodeJS.TypedArray typed array constructor} or - * {@link NodeJS.DataView `%DataView%`}, which will be used for constructing a - * view with which to write into the {@link buffer} - * - * ## References - * - [`TypedArray` Constructors](https://tc39.es/ecma262/#table-49) - */ - ctor: ArrayBufferViewConstructor; - /** - * Either "default" or "byob", indicating what type of readable stream reader - * initiated this request, or "none" if the initiating reader was released - */ - readerType: "default" | "byob" | "none"; -} - -type TypedArrayConstructor = - | Uint8ArrayConstructor - | Uint8ClampedArrayConstructor - | Uint16ArrayConstructor - | Uint32ArrayConstructor - | Int8ArrayConstructor - | Int16ArrayConstructor - | Int32ArrayConstructor - | BigUint64ArrayConstructor - | BigInt64ArrayConstructor - | Float32ArrayConstructor - | Float64ArrayConstructor; -type ArrayBufferViewConstructor = TypedArrayConstructor | DataViewConstructor; diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts deleted file mode 100644 index 7de64feca7ea..000000000000 --- a/src/js/builtins/ReadableStream.ts +++ /dev/null @@ -1,526 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * Copyright (C) 2015 Igalia. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeReadableStream( - this: ReadableStream, - underlyingSource: UnderlyingSource, - strategy: QueuingStrategy, -) { - if (underlyingSource === undefined) underlyingSource = { $bunNativePtr: undefined, $lazy: false } as UnderlyingSource; - if (strategy === undefined) strategy = {}; - - if (!$isObject(underlyingSource)) throw new TypeError("ReadableStream constructor takes an object as first argument"); - - if (strategy !== undefined && !$isObject(strategy)) - throw new TypeError("ReadableStream constructor takes an object as second argument, if any"); - - $putByIdDirectPrivate(this, "state", $streamReadable); - - $putByIdDirectPrivate(this, "reader", undefined); - - $putByIdDirectPrivate(this, "storedError", undefined); - - this.$disturbed = false; - - // Initialized with null value to enable distinction with undefined case. - $putByIdDirectPrivate(this, "readableStreamController", null); - this.$bunNativePtr = $getByIdDirectPrivate(underlyingSource, "bunNativePtr") ?? undefined; - - $putByIdDirectPrivate(this, "asyncContext", $getInternalField($asyncContext, 0)); - - const isDirect = underlyingSource.type === "direct"; - // direct streams are always lazy - const isUnderlyingSourceLazy = !!underlyingSource.$lazy; - const isLazy = isDirect || isUnderlyingSourceLazy; - let pullFn; - - // FIXME: We should introduce https://streams.spec.whatwg.org/#create-readable-stream. - // For now, we emulate this with underlyingSource with private properties. - if (!isLazy && (pullFn = $getByIdDirectPrivate(underlyingSource, "pull")) !== undefined) { - const size = $getByIdDirectPrivate(strategy, "size"); - const highWaterMark = $getByIdDirectPrivate(strategy, "highWaterMark"); - $putByIdDirectPrivate(this, "highWaterMark", highWaterMark); - $putByIdDirectPrivate(this, "underlyingSource", undefined); - $setupReadableStreamDefaultController( - this, - underlyingSource, - size, - highWaterMark !== undefined ? highWaterMark : 1, - $getByIdDirectPrivate(underlyingSource, "start"), - pullFn, - $getByIdDirectPrivate(underlyingSource, "cancel"), - ); - - return this; - } - if (isDirect) { - $putByIdDirectPrivate(this, "underlyingSource", underlyingSource); - $putByIdDirectPrivate(this, "highWaterMark", $getByIdDirectPrivate(strategy, "highWaterMark")); - $putByIdDirectPrivate(this, "start", () => $createReadableStreamController(this, underlyingSource, strategy)); - } else if (isLazy) { - const autoAllocateChunkSize = underlyingSource.autoAllocateChunkSize; - $putByIdDirectPrivate(this, "highWaterMark", undefined); - $putByIdDirectPrivate(this, "underlyingSource", undefined); - $putByIdDirectPrivate( - this, - "highWaterMark", - autoAllocateChunkSize || $getByIdDirectPrivate(strategy, "highWaterMark"), - ); - - $putByIdDirectPrivate(this, "start", () => { - const instance = $lazyLoadStream(this, autoAllocateChunkSize); - if (instance) { - $createReadableStreamController(this, instance, strategy); - } - }); - } else { - $putByIdDirectPrivate(this, "underlyingSource", undefined); - $putByIdDirectPrivate(this, "highWaterMark", $getByIdDirectPrivate(strategy, "highWaterMark")); - $putByIdDirectPrivate(this, "start", undefined); - $createReadableStreamController(this, underlyingSource, strategy); - } - - return this; -} - -$linkTimeConstant; -export function readableStreamToArray(stream: ReadableStream): Promise { - if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - // this is a direct stream - var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); - if (underlyingSource != null) { - return $readableStreamToArrayDirect(stream, underlyingSource); - } - if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - return $readableStreamIntoArray(stream); -} - -$linkTimeConstant; -export function readableStreamToText(stream: ReadableStream): Promise { - if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - // this is a direct stream - var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); - if (underlyingSource != null) { - return $readableStreamToTextDirect(stream, underlyingSource); - } - if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - - const result = $tryUseReadableStreamBufferedFastPath(stream, "text"); - - if (result) { - return result; - } - - return $readableStreamIntoText(stream); -} - -$linkTimeConstant; -export function readableStreamToArrayBuffer(stream: ReadableStream): Promise | ArrayBuffer { - if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - // this is a direct stream - var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); - if (underlyingSource != null) { - return $readableStreamToArrayBufferDirect(stream, underlyingSource, false); - } - if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - - let result = $tryUseReadableStreamBufferedFastPath(stream, "arrayBuffer"); - - if (result) { - return result; - } - - result = Bun.readableStreamToArray(stream); - - function toArrayBuffer(result: unknown[]) { - switch (result.length) { - case 0: { - return new ArrayBuffer(0); - } - case 1: { - const view = result[0]; - if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) { - return view; - } - - if (ArrayBuffer.isView(view)) { - const buffer = view.buffer; - const byteOffset = view.byteOffset; - const byteLength = view.byteLength; - if (byteOffset === 0 && byteLength === buffer.byteLength) { - return buffer; - } - - return buffer.slice(byteOffset, byteOffset + byteLength); - } - - if (typeof view === "string") { - return new TextEncoder().encode(view); - } - } - default: { - let anyStrings = false; - for (const chunk of result) { - if (typeof chunk === "string") { - anyStrings = true; - break; - } - } - - if (!anyStrings) { - return Bun.concatArrayBuffers(result, false); - } - - const sink = new Bun.ArrayBufferSink(); - sink.start(); - - for (const chunk of result) { - sink.write(chunk); - } - - return sink.end() as Uint8Array; - } - } - } - - if ($isPromise(result)) { - if ($isPromiseFulfilled(result)) { - result = $peekPromiseSettledValue(result); - } else { - // Pending, or already rejected (the stream was errored): the returned - // promise must settle the same way. - return result.then(toArrayBuffer); - } - } - return $createFulfilledPromise(toArrayBuffer(result)); -} - -$linkTimeConstant; -export function readableStreamToBytes(stream: ReadableStream): Promise | Uint8Array { - if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - // this is a direct stream - var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); - - if (underlyingSource != null) { - return $readableStreamToArrayBufferDirect(stream, underlyingSource, true); - } - if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - - let result = $tryUseReadableStreamBufferedFastPath(stream, "bytes"); - - if (result) { - return result; - } - - result = Bun.readableStreamToArray(stream); - - function toBytes(result: unknown[]) { - switch (result.length) { - case 0: { - return new Uint8Array(0); - } - case 1: { - const view = result[0]; - if (view instanceof Uint8Array) { - return view; - } - - if (ArrayBuffer.isView(view)) { - return new Uint8Array(view.buffer, view.byteOffset, view.byteLength); - } - - if (view instanceof ArrayBuffer || view instanceof SharedArrayBuffer) { - return new Uint8Array(view); - } - - if (typeof view === "string") { - return new TextEncoder().encode(view); - } - } - default: { - let anyStrings = false; - for (const chunk of result) { - if (typeof chunk === "string") { - anyStrings = true; - break; - } - } - - if (!anyStrings) { - return Bun.concatArrayBuffers(result, true); - } - - const sink = new Bun.ArrayBufferSink(); - sink.start({ asUint8Array: true }); - - for (const chunk of result) { - sink.write(chunk); - } - - return sink.end() as Uint8Array; - } - } - } - - if ($isPromise(result)) { - if ($isPromiseFulfilled(result)) { - result = $peekPromiseSettledValue(result); - } else { - // Pending, or already rejected (the stream was errored): the returned - // promise must settle the same way. - return result.then(toBytes); - } - } - - return $createFulfilledPromise(toBytes(result)); -} - -$linkTimeConstant; -export function readableStreamToFormData( - stream: ReadableStream, - contentType: string | ArrayBuffer | ArrayBufferView, -): Promise { - if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - return Bun.readableStreamToBlob(stream).then(blob => { - return FormData.from(blob, contentType); - }); -} - -$linkTimeConstant; -export function readableStreamToJSON(stream: ReadableStream): unknown { - if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - let result = $tryUseReadableStreamBufferedFastPath(stream, "json"); - if (result) { - return result; - } - - let text = Bun.readableStreamToText(stream); - const peeked = Bun.peek(text); - if (peeked !== text) { - try { - return $createFulfilledPromise(globalThis.JSON.parse(peeked)); - } catch (e) { - return Promise.$reject(e); - } - } - - return text.then(globalThis.JSON.parse); -} - -$linkTimeConstant; -export function readableStreamToBlob(stream: ReadableStream): Promise { - if (!$isReadableStream(stream)) throw $ERR_INVALID_ARG_TYPE("stream", "ReadableStream", typeof stream); - if ($isReadableStreamLocked(stream)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - - return ( - $tryUseReadableStreamBufferedFastPath(stream, "blob") || - Promise.$resolve(Bun.readableStreamToArray(stream)).then(array => new Blob(array)) - ); -} - -$linkTimeConstant; -export function createEmptyReadableStream() { - var stream = new ReadableStream({ - pull() {}, - } as any); - $readableStreamClose(stream); - return stream; -} - -$linkTimeConstant; -export function createUsedReadableStream() { - var stream = new ReadableStream({ - pull() {}, - } as any); - stream.getReader(); - return stream; -} - -$linkTimeConstant; -export function createErroredReadableStream(reason) { - var stream = new ReadableStream({ - pull() {}, - } as any); - $readableStreamError(stream, reason); - return stream; -} - -$linkTimeConstant; -export function createNativeReadableStream(nativePtr, autoAllocateChunkSize) { - $assert(nativePtr, "nativePtr must be a valid pointer"); - return new ReadableStream({ - $lazy: true, - $bunNativePtr: nativePtr, - autoAllocateChunkSize: autoAllocateChunkSize, - }); -} - -export function cancel(this, reason) { - if (!$isReadableStream(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStream")); - - if ($isReadableStreamLocked(this)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - - return $readableStreamCancel(this, reason); -} - -export function getReader(this, options) { - if (!$isReadableStream(this)) throw $ERR_INVALID_THIS("ReadableStream"); - - const mode = $toDictionary(options, {}, "ReadableStream.getReader takes an object as first argument").mode; - if (mode === undefined) { - var start_ = $getByIdDirectPrivate(this, "start"); - if (start_) { - $putByIdDirectPrivate(this, "start", undefined); - start_(); - } - - return new ReadableStreamDefaultReader(this); - } - // String conversion is required by spec, hence double equals. - if (mode == "byob") { - return new ReadableStreamBYOBReader(this); - } - - throw $ERR_INVALID_ARG_VALUE("mode", mode, "byob"); -} - -export function pipeThrough(this, streams, options) { - const transforms = streams; - - const readable = transforms["readable"]; - if (!$isReadableStream(readable)) throw $makeTypeError("readable should be ReadableStream"); - - const writable = transforms["writable"]; - const internalWritable = $getInternalWritableStream(writable); - if (!$isWritableStream(internalWritable)) throw $makeTypeError("writable should be WritableStream"); - - let preventClose = false; - let preventAbort = false; - let preventCancel = false; - let signal; - if (!$isUndefinedOrNull(options)) { - if (!$isObject(options)) throw $makeTypeError("options must be an object"); - - preventAbort = !!options["preventAbort"]; - preventCancel = !!options["preventCancel"]; - preventClose = !!options["preventClose"]; - - signal = options["signal"]; - if (signal !== undefined && !$isAbortSignal(signal)) throw $makeTypeError("options.signal must be AbortSignal"); - } - - if (!$isReadableStream(this)) throw $ERR_INVALID_THIS("ReadableStream"); - - if ($isReadableStreamLocked(this)) throw $ERR_INVALID_STATE_TypeError("ReadableStream is locked"); - - if ($isWritableStreamLocked(internalWritable)) throw $makeTypeError("WritableStream is locked"); - - const promise = $readableStreamPipeToWritableStream( - this, - internalWritable, - preventClose, - preventAbort, - preventCancel, - signal, - ); - $markPromiseAsHandled(promise); - - return readable; -} - -export function pipeTo(this, destination) { - if (!$isReadableStream(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStream")); - - if ($isReadableStreamLocked(this)) return Promise.$reject($ERR_INVALID_STATE_TypeError("ReadableStream is locked")); - - // FIXME: https://bugs.webkit.org/show_bug.cgi?id=159869. - // Built-in generator should be able to parse function signature to compute the function length correctly. - let options = $argument(1); - - let preventClose = false; - let preventAbort = false; - let preventCancel = false; - let signal; - if (!$isUndefinedOrNull(options)) { - if (!$isObject(options)) return Promise.$reject($makeTypeError("options must be an object")); - - try { - preventAbort = !!options["preventAbort"]; - preventCancel = !!options["preventCancel"]; - preventClose = !!options["preventClose"]; - - signal = options["signal"]; - } catch (e) { - return Promise.$reject(e); - } - - if (signal !== undefined && !$isAbortSignal(signal)) - return Promise.$reject(new TypeError("options.signal must be AbortSignal")); - } - - const internalDestination = $getInternalWritableStream(destination); - if (!$isWritableStream(internalDestination)) - return Promise.$reject(new TypeError("ReadableStream pipeTo requires a WritableStream")); - - if ($isWritableStreamLocked(internalDestination)) return Promise.$reject(new TypeError("WritableStream is locked")); - - return $readableStreamPipeToWritableStream( - this, - internalDestination, - preventClose, - preventAbort, - preventCancel, - signal, - ); -} - -export function tee(this) { - if (!$isReadableStream(this)) throw $ERR_INVALID_THIS("ReadableStream"); - - return $readableStreamTee(this, false); -} - -$getter; -export function locked(this) { - if (!$isReadableStream(this)) throw $makeGetterTypeError("ReadableStream", "locked"); - - return $isReadableStreamLocked(this); -} - -export function values(this, options) { - var prototype = ReadableStream.prototype; - $readableStreamDefineLazyIterators(prototype); - return prototype.values.$call(this, options); -} - -$linkTimeConstant; -export function lazyAsyncIterator(this) { - var prototype = ReadableStream.prototype; - $readableStreamDefineLazyIterators(prototype); - return prototype[globalThis.Symbol.asyncIterator].$call(this); -} diff --git a/src/js/builtins/ReadableStreamBYOBReader.ts b/src/js/builtins/ReadableStreamBYOBReader.ts deleted file mode 100644 index ead63348478e..000000000000 --- a/src/js/builtins/ReadableStreamBYOBReader.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2017 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeReadableStreamBYOBReader(this, stream) { - if (!$isReadableStream(stream)) throw new TypeError("ReadableStreamBYOBReader needs a ReadableStream"); - if (!$isReadableByteStreamController($getByIdDirectPrivate(stream, "readableStreamController"))) - throw new TypeError("ReadableStreamBYOBReader needs a ReadableByteStreamController"); - if ($isReadableStreamLocked(stream)) throw new TypeError("ReadableStream is locked"); - - $readableStreamReaderGenericInitialize(this, stream); - $putByIdDirectPrivate(this, "readIntoRequests", $createFIFO()); - - return this; -} - -export function cancel(this, reason) { - if (!$isReadableStreamBYOBReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamBYOBReader")); - - if (!$getByIdDirectPrivate(this, "ownerReadableStream")) - return Promise.$reject($ERR_INVALID_STATE_TypeError("The reader is not attached to a stream")); - - return $readableStreamReaderGenericCancel(this, reason); -} - -export function read(this, view: DataView) { - if (!$isReadableStreamBYOBReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamBYOBReader")); - - if (!$getByIdDirectPrivate(this, "ownerReadableStream")) - return Promise.$reject($ERR_INVALID_STATE_TypeError("The reader is not attached to a stream")); - - if (!$isObject(view)) return Promise.$reject($ERR_INVALID_ARG_TYPE("view", "Buffer, TypedArray, or DataView", view)); - - if (!ArrayBuffer.$isView(view)) - return Promise.$reject($ERR_INVALID_ARG_TYPE("view", "Buffer, TypedArray, or DataView", view)); - - if (view.byteLength === 0) return Promise.$reject($makeTypeError("Provided view cannot have a 0 byteLength")); - - return $readableStreamBYOBReaderRead(this, view); -} - -export function releaseLock(this) { - if (!$isReadableStreamBYOBReader(this)) throw $ERR_INVALID_THIS("ReadableStreamBYOBReader"); - - if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return; - - if ($getByIdDirectPrivate(this, "readIntoRequests")?.isNotEmpty()) - throw new TypeError("There are still pending read requests, cannot release the lock"); - - $readableStreamReaderGenericRelease(this); -} - -$getter; -export function closed(this) { - if (!$isReadableStreamBYOBReader(this)) - return Promise.$reject($makeGetterTypeError("ReadableStreamBYOBReader", "closed")); - - return $getByIdDirectPrivate(this, "closedPromiseCapability").promise; -} diff --git a/src/js/builtins/ReadableStreamBYOBRequest.ts b/src/js/builtins/ReadableStreamBYOBRequest.ts deleted file mode 100644 index f5a30576c70f..000000000000 --- a/src/js/builtins/ReadableStreamBYOBRequest.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (C) 2017 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeReadableStreamBYOBRequest(this, controller, view) { - if (arguments.length !== 3 && arguments[2] !== $isReadableStream) - throw new TypeError("ReadableStreamBYOBRequest constructor should not be called directly"); - - return $privateInitializeReadableStreamBYOBRequest.$call(this, controller, view); -} - -export function respond(this, bytesWritten) { - if (!$isReadableStreamBYOBRequest(this)) throw $ERR_INVALID_THIS("ReadableStreamBYOBRequest"); - - if ($getByIdDirectPrivate(this, "associatedReadableByteStreamController") == null) - throw $ERR_INVALID_STATE_TypeError("This BYOB request has been invalidated"); - - return $readableByteStreamControllerRespond( - $getByIdDirectPrivate(this, "associatedReadableByteStreamController"), - bytesWritten, - ); -} - -export function respondWithNewView(this, view) { - if (!$isReadableStreamBYOBRequest(this)) throw $ERR_INVALID_THIS("ReadableStreamBYOBRequest"); - - if ($getByIdDirectPrivate(this, "associatedReadableByteStreamController") == null) - throw $ERR_INVALID_STATE_TypeError("This BYOB request has been invalidated"); - - if (!$isObject(view)) throw $ERR_INVALID_ARG_TYPE("view", "Buffer, TypedArray, or DataView", view); - - if (!ArrayBuffer.$isView(view)) throw $ERR_INVALID_ARG_TYPE("view", "Buffer, TypedArray, or DataView", view); - - return $readableByteStreamControllerRespondWithNewView( - $getByIdDirectPrivate(this, "associatedReadableByteStreamController"), - view, - ); -} - -$getter; -export function view(this) { - if (!$isReadableStreamBYOBRequest(this)) throw $ERR_INVALID_THIS("ReadableStreamBYOBRequest"); - - return $getByIdDirectPrivate(this, "view"); -} diff --git a/src/js/builtins/ReadableStreamDefaultController.ts b/src/js/builtins/ReadableStreamDefaultController.ts deleted file mode 100644 index a4373778fc6c..000000000000 --- a/src/js/builtins/ReadableStreamDefaultController.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeReadableStreamDefaultController(this, stream, underlyingSource, size, highWaterMark) { - if (arguments.length !== 5 && arguments[4] !== $isReadableStream) - throw new TypeError("ReadableStreamDefaultController constructor should not be called directly"); - - return $privateInitializeReadableStreamDefaultController.$call(this, stream, underlyingSource, size, highWaterMark); -} - -export function enqueue(this, chunk) { - if (!$isReadableStreamDefaultController(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultController"); - - if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) { - throw $ERR_INVALID_STATE_TypeError("Controller is already closed"); - } - - return $readableStreamDefaultControllerEnqueue(this, chunk); -} - -export function error(this, err) { - if (!$isReadableStreamDefaultController(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultController"); - $readableStreamDefaultControllerError(this, err); -} - -export function close(this) { - if (!$isReadableStreamDefaultController(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultController"); - - if (!$readableStreamDefaultControllerCanCloseOrEnqueue(this)) - throw $ERR_INVALID_STATE_TypeError("Controller is already closed"); - - $readableStreamDefaultControllerClose(this); -} - -$getter; -export function desiredSize(this) { - if (!$isReadableStreamDefaultController(this)) - throw $makeGetterTypeError("ReadableStreamDefaultController", "desiredSize"); - - return $readableStreamDefaultControllerGetDesiredSize(this); -} diff --git a/src/js/builtins/ReadableStreamDefaultReader.ts b/src/js/builtins/ReadableStreamDefaultReader.ts deleted file mode 100644 index 19e26c34e9a2..000000000000 --- a/src/js/builtins/ReadableStreamDefaultReader.ts +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeReadableStreamDefaultReader(this, stream) { - if (!$isReadableStream(stream)) throw new TypeError("ReadableStreamDefaultReader needs a ReadableStream"); - if ($isReadableStreamLocked(stream)) throw new TypeError("ReadableStream is locked"); - - $readableStreamReaderGenericInitialize(this, stream); - $putByIdDirectPrivate(this, "readRequests", $createFIFO()); - - return this; -} - -export function cancel(this, reason) { - if (!$isReadableStreamDefaultReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamDefaultReader")); - - if (!$getByIdDirectPrivate(this, "ownerReadableStream")) - return Promise.$reject($ERR_INVALID_STATE_TypeError("The reader is not attached to a stream")); - - return $readableStreamReaderGenericCancel(this, reason); -} - -export function readMany(this: ReadableStreamDefaultReader): ReadableStreamDefaultReadManyResult { - if (!$isReadableStreamDefaultReader(this)) - throw new TypeError("ReadableStreamDefaultReader.readMany() should not be called directly"); - - const stream = $getByIdDirectPrivate(this, "ownerReadableStream"); - if (!stream) throw $ERR_INVALID_STATE_TypeError("The reader is not attached to a stream"); - - const state = $getByIdDirectPrivate(stream, "state"); - stream.$disturbed = true; - if (state === $streamErrored) { - throw $getByIdDirectPrivate(stream, "storedError"); - } - - var controller = $getByIdDirectPrivate(stream, "readableStreamController"); - if (controller) { - var queue = $getByIdDirectPrivate(controller, "queue"); - } - - if (!queue && state !== $streamClosed) { - // This is a ReadableStream direct controller implemented in JS - // It hasn't been started yet. - return controller.$pull(controller).$then(function ({ done, value }) { - return done ? { done: true, value: value ? [value] : [], size: 0 } : { value: [value], size: 1, done: false }; - }); - } else if (!queue) { - return { done: true, value: [], size: 0 }; - } - - const content = queue.content; - var size = queue.size; - var values = content.toArray(false); - - var length = values.length; - - if (length > 0) { - var outValues = $newArrayWithSize(length); - if ($isReadableByteStreamController(controller)) { - { - const buf = values[0]; - if (!(ArrayBuffer.$isView(buf) || buf instanceof ArrayBuffer)) { - $putByValDirect(outValues, 0, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)); - } else { - $putByValDirect(outValues, 0, buf); - } - } - - for (var i = 1; i < length; i++) { - const buf = values[i]; - if (!(ArrayBuffer.$isView(buf) || buf instanceof ArrayBuffer)) { - $putByValDirect(outValues, i, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)); - } else { - $putByValDirect(outValues, i, buf); - } - } - } else { - $putByValDirect(outValues, 0, values[0].value); - for (var i = 1; i < length; i++) { - $putByValDirect(outValues, i, values[i].value); - } - } - - if (state !== $streamClosed) { - if ($getByIdDirectPrivate(controller, "closeRequested")) { - $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); - } else if ($isReadableStreamDefaultController(controller)) { - $readableStreamDefaultControllerCallPullIfNeeded(controller); - } else if ($isReadableByteStreamController(controller)) { - $readableByteStreamControllerCallPullIfNeeded(controller); - } - } - $resetQueue($getByIdDirectPrivate(controller, "queue")); - - return { value: outValues, size, done: false }; - } - - var onPullMany = result => { - const resultValue = result.value; - - if (result.done) { - return { value: resultValue ? [resultValue] : [], size: 0, done: true }; - } - var controller = $getByIdDirectPrivate(stream, "readableStreamController"); - - var queue = $getByIdDirectPrivate(controller, "queue"); - var value = [resultValue].concat(queue.content.toArray(false)); - var length = value.length; - - if ($isReadableByteStreamController(controller)) { - for (var i = 0; i < length; i++) { - const buf = value[i]; - if (!(ArrayBuffer.$isView(buf) || buf instanceof ArrayBuffer)) { - const { buffer, byteOffset, byteLength } = buf; - $putByValDirect(value, i, new Uint8Array(buffer, byteOffset, byteLength)); - } - } - } else { - for (var i = 1; i < length; i++) { - $putByValDirect(value, i, value[i].value); - } - } - - var size = queue.size; - if ($getByIdDirectPrivate(controller, "closeRequested")) { - $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); - } else if ($isReadableStreamDefaultController(controller)) { - $readableStreamDefaultControllerCallPullIfNeeded(controller); - } else if ($isReadableByteStreamController(controller)) { - $readableByteStreamControllerCallPullIfNeeded(controller); - } - - $resetQueue($getByIdDirectPrivate(controller, "queue")); - - return { value: value, size: size, done: false }; - }; - - if (state === $streamClosed) { - return { value: [], size: 0, done: true }; - } - - var pullResult = controller.$pull(controller); - if (pullResult && $isPromise(pullResult)) { - return pullResult.then(onPullMany) as any; - } - - return onPullMany(pullResult); -} - -export function read(this) { - if (!$isReadableStreamDefaultReader(this)) return Promise.$reject($ERR_INVALID_THIS("ReadableStreamDefaultReader")); - if (!$getByIdDirectPrivate(this, "ownerReadableStream")) - return Promise.$reject($ERR_INVALID_STATE_TypeError("The reader is not attached to a stream")); - - return $readableStreamDefaultReaderRead(this); -} - -export function releaseLock(this) { - if (!$isReadableStreamDefaultReader(this)) throw $ERR_INVALID_THIS("ReadableStreamDefaultReader"); - - if (!$getByIdDirectPrivate(this, "ownerReadableStream")) return; - - $readableStreamDefaultReaderRelease(this); -} - -$getter; -export function closed(this) { - if (!$isReadableStreamDefaultReader(this)) - return Promise.$reject($makeGetterTypeError("ReadableStreamDefaultReader", "closed")); - - return $getByIdDirectPrivate(this, "closedPromiseCapability").promise; -} diff --git a/src/js/builtins/ReadableStreamInternals.ts b/src/js/builtins/ReadableStreamInternals.ts deleted file mode 100644 index 05583fa36cb1..000000000000 --- a/src/js/builtins/ReadableStreamInternals.ts +++ /dev/null @@ -1,2644 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. All rights reserved. - * Copyright (C) 2015 Igalia. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -// @internal - -export function readableStreamReaderGenericInitialize(reader: ReadableStreamDefaultReader, stream: ReadableStream) { - $putByIdDirectPrivate(reader, "ownerReadableStream", stream); - $putByIdDirectPrivate(stream, "reader", reader); - if ($getByIdDirectPrivate(stream, "state") === $streamReadable) - $putByIdDirectPrivate(reader, "closedPromiseCapability", $newPromiseCapability(Promise)); - else if ($getByIdDirectPrivate(stream, "state") === $streamClosed) - $putByIdDirectPrivate(reader, "closedPromiseCapability", { - promise: Promise.$resolve(), - }); - else { - $assert($getByIdDirectPrivate(stream, "state") === $streamErrored); - $putByIdDirectPrivate(reader, "closedPromiseCapability", { - promise: $newHandledRejectedPromise($getByIdDirectPrivate(stream, "storedError")), - }); - } -} - -export function privateInitializeReadableStreamDefaultController( - this: ReadableStreamDefaultController, - stream: ReadableStream, - underlyingSource: UnderlyingSource, - size: QueuingStrategySize, - highWaterMark: QueuingStrategyHighWaterMark, -) { - if (!$isReadableStream(stream)) throw new TypeError("ReadableStreamDefaultController needs a ReadableStream"); - - // readableStreamController is initialized with null value. - if ($getByIdDirectPrivate(stream, "readableStreamController") !== null) - throw new TypeError("ReadableStream already has a controller"); - - $putByIdDirectPrivate(this, "controlledReadableStream", stream); - $putByIdDirectPrivate(this, "underlyingSource", underlyingSource); - $putByIdDirectPrivate(this, "queue", $newQueue()); - $putByIdDirectPrivate(this, "started", -1); - $putByIdDirectPrivate(this, "closeRequested", false); - $putByIdDirectPrivate(this, "pullAgain", false); - $putByIdDirectPrivate(this, "pulling", false); - $putByIdDirectPrivate(this, "strategy", $validateAndNormalizeQueuingStrategy(size, highWaterMark)); - - return this; -} - -export function readableStreamDefaultControllerError(controller, error) { - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - if (!$isObject(stream) || $getByIdDirectPrivate(stream, "state") !== $streamReadable) return; - $putByIdDirectPrivate(controller, "queue", $newQueue()); - - $readableStreamError(stream, error); -} - -export function readableStreamPipeTo(stream, sink) { - $assert($isReadableStream(stream)); - - const reader = new ReadableStreamDefaultReader(stream); - - $getByIdDirectPrivate(reader, "closedPromiseCapability").promise.$then($readableStreamNoop, sink.error.bind(sink)); - - function doPipe() { - $readableStreamDefaultReaderRead(reader).$then( - function (result) { - if (result.done) { - sink.close(); - return; - } - try { - sink.enqueue(result.value); - } catch { - sink.error("ReadableStream chunk enqueueing in the sink failed"); - return; - } - doPipe(); - }, - function (e) { - sink.error(e); - }, - ); - } - doPipe(); -} - -export function acquireReadableStreamDefaultReader(stream) { - var start = $getByIdDirectPrivate(stream, "start"); - if (start) { - start.$call(stream); - } - - return new ReadableStreamDefaultReader(stream); -} - -// Bound with the underlyingSource/method/controller so the stored -// pullAlgorithm/cancelAlgorithm hold only those values, not the entire -// setupReadableStreamDefaultController activation. -export function readableStreamDefaultControllerPullAlgorithm(underlyingSource, pullMethod, controller) { - return $promiseInvokeOrNoopMethod(underlyingSource, pullMethod, [controller]); -} - -export function readableStreamDefaultControllerCancelAlgorithm(underlyingSource, cancelMethod, reason) { - return $promiseInvokeOrNoopMethod(underlyingSource, cancelMethod, [reason]); -} - -export function readableStreamDefaultControllerCancelAlgorithmWithAsyncContext( - underlyingSource, - cancelMethod, - asyncContext, - reason, -) { - var prev = $getInternalField($asyncContext, 0); - $putInternalField($asyncContext, 0, asyncContext); - // this does not throw, but can returns a rejected promise - var result = $promiseInvokeOrNoopMethod(underlyingSource, cancelMethod, [reason]); - $putInternalField($asyncContext, 0, prev); - return result; -} - -// https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller, starting from step 6. -// The other part is implemented in privateInitializeReadableStreamDefaultController. -export function setupReadableStreamDefaultController( - stream, - underlyingSource, - size, - highWaterMark, - startMethod, - pullMethod, - cancelMethod, -) { - const controller = new ReadableStreamDefaultController( - stream, - underlyingSource, - size, - highWaterMark, - $isReadableStream, - ); - - var asyncContext = stream.$asyncContext; - - $putByIdDirectPrivate( - controller, - "pullAlgorithm", - $readableStreamDefaultControllerPullAlgorithm.bind(undefined, underlyingSource, pullMethod, controller), - ); - $putByIdDirectPrivate( - controller, - "cancelAlgorithm", - asyncContext - ? $readableStreamDefaultControllerCancelAlgorithmWithAsyncContext.bind( - undefined, - underlyingSource, - cancelMethod, - asyncContext, - ) - : $readableStreamDefaultControllerCancelAlgorithm.bind(undefined, underlyingSource, cancelMethod), - ); - $putByIdDirectPrivate(controller, "pull", $readableStreamDefaultControllerPull); - $putByIdDirectPrivate(controller, "cancel", $readableStreamDefaultControllerCancel); - $putByIdDirectPrivate(stream, "readableStreamController", controller); - - $readableStreamDefaultControllerStart(controller); -} - -export function createReadableStreamController(stream, underlyingSource, strategy) { - const type = underlyingSource.type; - const typeString = $toString(type); - - if (typeString === "bytes") { - // if (!$readableByteStreamAPIEnabled()) - // $throwTypeError("ReadableByteStreamController is not implemented"); - - if (strategy.highWaterMark === undefined) strategy.highWaterMark = 0; - if (strategy.size !== undefined) $throwRangeError("Strategy for a ReadableByteStreamController cannot have a size"); - - $putByIdDirectPrivate( - stream, - "readableStreamController", - new ReadableByteStreamController(stream, underlyingSource, strategy.highWaterMark, $isReadableStream), - ); - } else if (typeString === "direct") { - var highWaterMark = strategy?.highWaterMark; - $initializeArrayBufferStream.$call(stream, underlyingSource, highWaterMark); - } else if (type === undefined) { - if (strategy.highWaterMark === undefined) strategy.highWaterMark = 1; - - $setupReadableStreamDefaultController( - stream, - underlyingSource, - strategy.size, - strategy.highWaterMark, - underlyingSource.start, - underlyingSource.pull, - underlyingSource.cancel, - ); - } else throw new RangeError("Invalid type for underlying source"); -} - -export function readableStreamDefaultControllerStartFulfilled(this: ReadableStreamDefaultController) { - $putByIdDirectPrivate(this, "started", 1); - $assert(!$getByIdDirectPrivate(this, "pulling")); - $assert(!$getByIdDirectPrivate(this, "pullAgain")); - $readableStreamDefaultControllerCallPullIfNeeded(this); -} - -export function readableStreamDefaultControllerStartRejected(this: ReadableStreamDefaultController, error) { - $readableStreamDefaultControllerError(this, error); -} - -export function readableStreamDefaultControllerStart(controller) { - if ($getByIdDirectPrivate(controller, "started") !== -1) return; - - const underlyingSource = $getByIdDirectPrivate(controller, "underlyingSource"); - const startMethod = underlyingSource.start; - $putByIdDirectPrivate(controller, "started", 0); - - $promiseInvokeOrNoopMethodNoCatch(underlyingSource, startMethod, [controller]).$then( - $readableStreamDefaultControllerStartFulfilled.bind(controller), - $readableStreamDefaultControllerStartRejected.bind(controller), - ); -} - -// FIXME: Replace readableStreamPipeTo by below function. -// This method implements the latest https://streams.spec.whatwg.org/#readable-stream-pipe-to. -export function readableStreamPipeToWritableStream( - source, - destination, - preventClose, - preventAbort, - preventCancel, - signal, -) { - // const isDirectStream = !!$getByIdDirectPrivate(source, "start"); - - $assert($isReadableStream(source)); - $assert($isWritableStream(destination)); - $assert(!$isReadableStreamLocked(source)); - $assert(!$isWritableStreamLocked(destination)); - $assert(signal === undefined || $isAbortSignal(signal)); - - if ($getByIdDirectPrivate(source, "underlyingByteSource") !== undefined) - return Promise.$reject("Piping to a readable bytestream is not supported"); - - let pipeState: any = { - source: source, - destination: destination, - preventAbort: preventAbort, - preventCancel: preventCancel, - preventClose: preventClose, - signal: signal, - }; - - pipeState.reader = $acquireReadableStreamDefaultReader(source); - pipeState.writer = $acquireWritableStreamDefaultWriter(destination); - - source.$disturbed = true; - - pipeState.shuttingDown = false; - pipeState.promiseCapability = $newPromiseCapability(Promise); - pipeState.pendingReadPromiseCapability = $newPromiseCapability(Promise); - pipeState.pendingReadPromiseCapability.resolve.$call(); - pipeState.pendingWritePromise = Promise.$resolve(); - - if (signal !== undefined) { - const algorithm = reason => { - $pipeToShutdownWithAction( - pipeState, - () => { - const shouldAbortDestination = - !pipeState.preventAbort && $getByIdDirectPrivate(pipeState.destination, "state") === "writable"; - const promiseDestination = shouldAbortDestination - ? $writableStreamAbort(pipeState.destination, reason) - : Promise.$resolve(); - - const shouldAbortSource = - !pipeState.preventCancel && $getByIdDirectPrivate(pipeState.source, "state") === $streamReadable; - const promiseSource = shouldAbortSource - ? $readableStreamCancel(pipeState.source, reason) - : Promise.$resolve(); - - let promiseCapability = $newPromiseCapability(Promise); - let shouldWait = true; - let handleResolvedPromise = () => { - if (shouldWait) { - shouldWait = false; - return; - } - promiseCapability.resolve.$call(); - }; - let handleRejectedPromise = e => { - promiseCapability.reject.$call(undefined, e); - }; - promiseDestination.$then(handleResolvedPromise, handleRejectedPromise); - promiseSource.$then(handleResolvedPromise, handleRejectedPromise); - return promiseCapability.promise; - }, - reason, - ); - }; - const abortAlgorithmIdentifier = (pipeState.abortAlgorithmIdentifier = $addAbortAlgorithmToSignal( - signal, - algorithm, - )); - - if (!abortAlgorithmIdentifier) return pipeState.promiseCapability.promise; - pipeState.signal = signal; - } - - $pipeToErrorsMustBePropagatedForward(pipeState); - $pipeToErrorsMustBePropagatedBackward(pipeState); - $pipeToClosingMustBePropagatedForward(pipeState); - $pipeToClosingMustBePropagatedBackward(pipeState); - - $pipeToLoop(pipeState); - - return pipeState.promiseCapability.promise; -} - -export function pipeToLoopContinue(this, result) { - if (result) $pipeToLoop(this); -} - -export function pipeToLoop(pipeState) { - if (pipeState.shuttingDown) return; - - $pipeToDoReadWrite(pipeState).$then($pipeToLoopContinue.bind(pipeState)); -} - -export function pipeToResolvePendingReadFalse(this) { - this.pendingReadPromiseCapability.resolve.$call(undefined, false); -} - -export function pipeToDoReadWriteOnReady(this) { - if (this.shuttingDown) { - this.pendingReadPromiseCapability.resolve.$call(undefined, false); - return; - } - - $readableStreamDefaultReaderRead(this.reader).$then( - $pipeToDoReadWriteOnRead.bind(this), - $pipeToResolvePendingReadFalse.bind(this), - ); -} - -export function pipeToDoReadWriteOnRead(this, result) { - const canWrite = !result.done && $getByIdDirectPrivate(this.writer, "stream") !== undefined; - this.pendingReadPromiseCapability.resolve.$call(undefined, canWrite); - if (!canWrite) return; - - this.pendingWritePromise = $writableStreamDefaultWriterWrite(this.writer, result.value).$then( - undefined, - $readableStreamNoop, - ); -} - -export function pipeToDoReadWrite(pipeState) { - $assert(!pipeState.shuttingDown); - - pipeState.pendingReadPromiseCapability = $newPromiseCapability(Promise); - $getByIdDirectPrivate(pipeState.writer, "readyPromise").promise.$then( - $pipeToDoReadWriteOnReady.bind(pipeState), - $pipeToResolvePendingReadFalse.bind(pipeState), - ); - return pipeState.pendingReadPromiseCapability.promise; -} - -export function pipeToErrorsMustBePropagatedForward(pipeState) { - const action = () => { - pipeState.pendingReadPromiseCapability.resolve.$call(undefined, false); - const error = $getByIdDirectPrivate(pipeState.source, "storedError"); - if (!pipeState.preventAbort) { - $pipeToShutdownWithAction(pipeState, () => $writableStreamAbort(pipeState.destination, error), error); - return; - } - $pipeToShutdown(pipeState, error); - }; - - if ($getByIdDirectPrivate(pipeState.source, "state") === $streamErrored) { - action(); - return; - } - - $getByIdDirectPrivate(pipeState.reader, "closedPromiseCapability").promise.$then(undefined, action); -} - -export function pipeToErrorsMustBePropagatedBackward(pipeState) { - const action = () => { - const error = $getByIdDirectPrivate(pipeState.destination, "storedError"); - if (!pipeState.preventCancel) { - $pipeToShutdownWithAction(pipeState, () => $readableStreamCancel(pipeState.source, error), error); - return; - } - $pipeToShutdown(pipeState, error); - }; - if ($getByIdDirectPrivate(pipeState.destination, "state") === "errored") { - action(); - return; - } - $getByIdDirectPrivate(pipeState.writer, "closedPromise").promise.$then(undefined, action); -} - -export function pipeToClosingMustBePropagatedForward(pipeState) { - const action = () => { - pipeState.pendingReadPromiseCapability.resolve.$call(undefined, false); - // const error = $getByIdDirectPrivate(pipeState.source, "storedError"); - if (!pipeState.preventClose) { - $pipeToShutdownWithAction(pipeState, () => - $writableStreamDefaultWriterCloseWithErrorPropagation(pipeState.writer), - ); - return; - } - $pipeToShutdown(pipeState); - }; - if ($getByIdDirectPrivate(pipeState.source, "state") === $streamClosed) { - action(); - return; - } - $getByIdDirectPrivate(pipeState.reader, "closedPromiseCapability").promise.$then(action, () => {}); -} - -export function pipeToClosingMustBePropagatedBackward(pipeState) { - if ( - !$writableStreamCloseQueuedOrInFlight(pipeState.destination) && - $getByIdDirectPrivate(pipeState.destination, "state") !== "closed" - ) - return; - - // $assert no chunks have been read/written - - const error = new TypeError("closing is propagated backward"); - if (!pipeState.preventCancel) { - $pipeToShutdownWithAction(pipeState, () => $readableStreamCancel(pipeState.source, error), error); - return; - } - $pipeToShutdown(pipeState, error); -} - -export function pipeToShutdownWithAction(pipeState, action) { - if (pipeState.shuttingDown) return; - - pipeState.shuttingDown = true; - - const hasError = arguments.length > 2; - const error = arguments[2]; - const finalize = () => { - const promise = action(); - promise.$then( - () => { - if (hasError) $pipeToFinalize(pipeState, error); - else $pipeToFinalize(pipeState); - }, - e => { - $pipeToFinalize(pipeState, e); - }, - ); - }; - - if ( - $getByIdDirectPrivate(pipeState.destination, "state") === "writable" && - !$writableStreamCloseQueuedOrInFlight(pipeState.destination) - ) { - pipeState.pendingReadPromiseCapability.promise.$then( - () => { - pipeState.pendingWritePromise.$then(finalize, finalize); - }, - e => $pipeToFinalize(pipeState, e), - ); - return; - } - - finalize(); -} - -export function pipeToShutdown(pipeState) { - if (pipeState.shuttingDown) return; - - pipeState.shuttingDown = true; - - const hasError = arguments.length > 1; - const error = arguments[1]; - const finalize = () => { - if (hasError) $pipeToFinalize(pipeState, error); - else $pipeToFinalize(pipeState); - }; - - if ( - $getByIdDirectPrivate(pipeState.destination, "state") === "writable" && - !$writableStreamCloseQueuedOrInFlight(pipeState.destination) - ) { - pipeState.pendingReadPromiseCapability.promise.$then( - () => { - pipeState.pendingWritePromise.$then(finalize, finalize); - }, - e => $pipeToFinalize(pipeState, e), - ); - return; - } - finalize(); -} - -export function pipeToFinalize(pipeState) { - $writableStreamDefaultWriterRelease(pipeState.writer); - $readableStreamReaderGenericRelease(pipeState.reader); - - const signal = pipeState.signal; - if (signal) $removeAbortAlgorithmFromSignal(signal, pipeState.abortAlgorithmIdentifier); - - if (arguments.length > 1) pipeState.promiseCapability.reject.$call(undefined, arguments[1]); - else pipeState.promiseCapability.resolve.$call(); -} - -const enum TeeStateFlags { - canceled1 = 1 << 0, - canceled2 = 1 << 1, - reading = 1 << 2, - closedOrErrored = 1 << 3, - readAgain = 1 << 4, -} - -export function readableStreamTee(stream, shouldClone) { - $assert($isReadableStream(stream)); - $assert(typeof shouldClone === "boolean"); - - var start_ = $getByIdDirectPrivate(stream, "start"); - if (start_) { - $putByIdDirectPrivate(stream, "start", undefined); - start_(); - } - - const reader = new $ReadableStreamDefaultReader(stream); - - const teeState = { - stream, - flags: 0, - reason1: undefined, - reason2: undefined, - branch1Source: undefined, - branch2Source: undefined, - branch1: undefined, - branch2: undefined, - cancelPromiseCapability: $newPromiseCapability(Promise), - }; - - const pullFunction = $readableStreamTeePullFunction(teeState, reader, shouldClone); - - const branch1Source = { - $pull: pullFunction, - $cancel: $readableStreamTeeBranch1CancelFunction(teeState, stream), - }; - - const branch2Source = { - $pull: pullFunction, - $cancel: $readableStreamTeeBranch2CancelFunction(teeState, stream), - }; - - const branch1 = new $ReadableStream(branch1Source); - const branch2 = new $ReadableStream(branch2Source); - - $getByIdDirectPrivate(reader, "closedPromiseCapability").promise.$then(undefined, function (e) { - const flags = teeState.flags; - if (flags & TeeStateFlags.closedOrErrored) return; - $readableStreamDefaultControllerError(branch1.$readableStreamController, e); - $readableStreamDefaultControllerError(branch2.$readableStreamController, e); - teeState.flags |= TeeStateFlags.closedOrErrored; - - if (teeState.flags & (TeeStateFlags.canceled1 | TeeStateFlags.canceled2)) - teeState.cancelPromiseCapability.resolve.$call(); - }); - - // Additional fields compared to the spec, as they are needed within pull/cancel functions. - teeState.branch1 = branch1; - teeState.branch2 = branch2; - - return [branch1, branch2]; -} - -export function readableStreamTeePullFunction(teeState, reader, shouldClone) { - "use strict"; - - const pullAlgorithm = function () { - if (teeState.flags & TeeStateFlags.reading) { - teeState.flags |= TeeStateFlags.readAgain; - return Promise.$resolve(); - } - teeState.flags |= TeeStateFlags.reading; - $Promise.prototype.$then.$call( - $readableStreamDefaultReaderRead(reader), - function (result) { - $assert($isObject(result)); - $assert(typeof result.done === "boolean"); - const { done, value } = result; - if (done) { - // close steps. - teeState.flags &= ~TeeStateFlags.reading; - if (!(teeState.flags & TeeStateFlags.canceled1)) - $readableStreamDefaultControllerClose(teeState.branch1.$readableStreamController); - if (!(teeState.flags & TeeStateFlags.canceled2)) - $readableStreamDefaultControllerClose(teeState.branch2.$readableStreamController); - if (!(teeState.flags & TeeStateFlags.canceled1) || !(teeState.flags & TeeStateFlags.canceled2)) - teeState.cancelPromiseCapability.resolve.$call(); - return; - } - // chunk steps. - teeState.flags &= ~TeeStateFlags.readAgain; - let chunk1 = value; - let chunk2 = value; - if (!(teeState.flags & TeeStateFlags.canceled2) && shouldClone) { - try { - chunk2 = $structuredCloneForStream(value); - } catch (e) { - $readableStreamDefaultControllerError(teeState.branch1.$readableStreamController, e); - $readableStreamDefaultControllerError(teeState.branch2.$readableStreamController, e); - $readableStreamCancel(teeState.stream, e).$then( - teeState.cancelPromiseCapability.resolve, - teeState.cancelPromiseCapability.reject, - ); - return; - } - } - if (!(teeState.flags & TeeStateFlags.canceled1)) - $readableStreamDefaultControllerEnqueue(teeState.branch1.$readableStreamController, chunk1); - if (!(teeState.flags & TeeStateFlags.canceled2)) - $readableStreamDefaultControllerEnqueue(teeState.branch2.$readableStreamController, chunk2); - teeState.flags &= ~TeeStateFlags.reading; - - Promise.$resolve().$then(() => { - if (teeState.flags & TeeStateFlags.readAgain) pullAlgorithm(); - }); - }, - () => { - // error steps. - teeState.flags &= ~TeeStateFlags.reading; - }, - ); - return Promise.$resolve(); - }; - return pullAlgorithm; -} - -export function readableStreamTeeBranch1CancelFunction(teeState, stream) { - return function (r) { - teeState.flags |= TeeStateFlags.canceled1; - teeState.reason1 = r; - if (teeState.flags & TeeStateFlags.canceled2) { - $readableStreamCancel(stream, [teeState.reason1, teeState.reason2]).$then( - teeState.cancelPromiseCapability.resolve, - teeState.cancelPromiseCapability.reject, - ); - } - return teeState.cancelPromiseCapability.promise; - }; -} - -export function readableStreamTeeBranch2CancelFunction(teeState, stream) { - return function (r) { - teeState.flags |= TeeStateFlags.canceled2; - teeState.reason2 = r; - if (teeState.flags & TeeStateFlags.canceled1) { - $readableStreamCancel(stream, [teeState.reason1, teeState.reason2]).$then( - teeState.cancelPromiseCapability.resolve, - teeState.cancelPromiseCapability.reject, - ); - } - return teeState.cancelPromiseCapability.promise; - }; -} - -$alwaysInline = true; -export function isReadableStream(stream) { - // Spec tells to return true only if stream has a readableStreamController internal slot. - // However, since it is a private slot, it cannot be checked using hasOwnProperty(). - // Therefore, readableStreamController is initialized with null value. - return $isObject(stream) && $getByIdDirectPrivate(stream, "readableStreamController") !== undefined; -} - -$alwaysInline = true; -export function isReadableStreamDefaultReader(reader) { - // Spec tells to return true only if reader has a readRequests internal slot. - // However, since it is a private slot, it cannot be checked using hasOwnProperty(). - // Since readRequests is initialized with an empty array, the following test is ok. - return $isObject(reader) && !!$getByIdDirectPrivate(reader, "readRequests"); -} - -$alwaysInline = true; -export function isReadableStreamDefaultController(controller) { - // Spec tells to return true only if controller has an underlyingSource internal slot. - // However, since it is a private slot, it cannot be checked using hasOwnProperty(). - // underlyingSource is obtained in ReadableStream constructor: if undefined, it is set - // to an empty object. Therefore, following test is ok. - return $isObject(controller) && $getByIdDirectPrivate(controller, "underlyingSource") !== undefined; -} - -// Bound (via `this`) to readDirectStream's per-request state object so the -// onClose callback the native sink stores holds only that small state, not -// the whole readDirectStream activation. -export function readDirectStreamOnClose( - this: { underlyingSource: any; closePromiseCapability: PromiseCapability | undefined }, - stream, - reason, -) { - var underlyingSource = this.underlyingSource; - this.underlyingSource = undefined; - const cancelFn = underlyingSource?.cancel; - if (cancelFn) { - try { - var prom = cancelFn.$call(underlyingSource, reason); - if ($isPromise(prom)) { - $markPromiseAsHandled(prom); - } - } catch {} - } - underlyingSource = undefined; - - if (stream) { - $putByIdDirectPrivate(stream, "readableStreamController", undefined); - $putByIdDirectPrivate(stream, "reader", undefined); - if (reason) { - $putByIdDirectPrivate(stream, "state", $streamErrored); - $putByIdDirectPrivate(stream, "storedError", reason); - } else { - $putByIdDirectPrivate(stream, "state", $streamClosed); - } - stream = undefined; - } - - var closePromiseCapability = this.closePromiseCapability; - if (closePromiseCapability) { - this.closePromiseCapability = undefined; - closePromiseCapability.resolve.$call(); - } -} - -export function readDirectStream(stream, sink, underlyingSource) { - $putByIdDirectPrivate(stream, "underlyingSource", null); // doing this causes isReadableStreamDefaultController to return false - $putByIdDirectPrivate(stream, "start", undefined); - - // Mutable state the close handler needs; bound so it does not capture this - // function's scope. - var state = { __proto__: null, underlyingSource, closePromiseCapability: undefined }; - var close = $readDirectStreamOnClose.bind(state); - - if (!underlyingSource.pull) { - close(); - return; - } - - if (!$isCallable(underlyingSource.pull)) { - close(); - $throwTypeError("pull is not a function"); - return; - } - $putByIdDirectPrivate(stream, "readableStreamController", sink); - const highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); - sink.start({ - highWaterMark: !highWaterMark || highWaterMark < 64 ? 64 : highWaterMark, - }); - - $startDirectStream.$call(sink, stream, underlyingSource.pull, close, stream.$asyncContext); - - $putByIdDirectPrivate(stream, "reader", {}); - - var maybePromise = underlyingSource.pull(sink); - sink = undefined; - if (maybePromise && $isPromise(maybePromise)) { - if (maybePromise.$then) { - return maybePromise.$then($readableStreamNoop); - } - - return maybePromise.then($readableStreamNoop); - } - - if ($getByIdDirectPrivate(stream, "state") === $streamReadable) { - // pull() returned synchronously without closing the sink: the producer - // kept the controller to write more data and call end() later - // (react-dom/server's renderToReadableStream does this while Suspense - // boundaries are still pending). Return a promise that settles when the - // sink closes so native consumers (Bun.serve, FileSink) wait for end() - // instead of finalizing the response early. - return (state.closePromiseCapability = $newPromiseCapability(Promise)).promise; - } -} - -$linkTimeConstant; -export function assignToStream(stream, sink) { - // The stream is either a direct stream or a "default" JS stream - var underlyingSource = $getByIdDirectPrivate(stream, "underlyingSource"); - - // we know it's a direct stream when $underlyingSource is set - if (underlyingSource) { - try { - return $readDirectStream(stream, sink, underlyingSource); - } finally { - underlyingSource = undefined; - stream = undefined; - sink = undefined; - } - } - - return $readStreamIntoSink(stream, sink, true); -} - -interface ResumableSinkState { - stream: ReadableStream | undefined; - sink: any; - reader: ReadableStreamDefaultReader | undefined; - error: Error | null; - reading: boolean; - closed: boolean; -} - -export function resumableSinkReleaseReader(state: ResumableSinkState) { - var reader = state.reader; - if (reader) { - try { - reader.releaseLock(); - } catch {} - state.reader = undefined; - } - state.sink = undefined; - var stream = state.stream; - if (stream) { - var streamState = $getByIdDirectPrivate(stream, "state"); - // make it easy for this to be GC'd - // but don't do property transitions - var readableStreamController = $getByIdDirectPrivate(stream, "readableStreamController"); - if (readableStreamController) { - if ($getByIdDirectPrivate(readableStreamController, "underlyingSource")) - $putByIdDirectPrivate(readableStreamController, "underlyingSource", null); - if ($getByIdDirectPrivate(readableStreamController, "controlledReadableStream")) - $putByIdDirectPrivate(readableStreamController, "controlledReadableStream", null); - - $putByIdDirectPrivate(stream, "readableStreamController", null); - if ($getByIdDirectPrivate(stream, "underlyingSource")) $putByIdDirectPrivate(stream, "underlyingSource", null); - readableStreamController = undefined; - } - - if (stream && !state.error && streamState !== $streamClosed && streamState !== $streamErrored) { - $readableStreamCloseIfPossible(stream); - } - state.stream = undefined; - } -} - -export function resumableSinkEnd(this: ResumableSinkState, err?: any) { - try { - var sink = this.sink; - if (sink) { - if (arguments.length > 0) sink.end(err); - else sink.end(); - } - } catch {} // should never throw - $resumableSinkReleaseReader(this); -} - -// Bound (via `this`) to the per-request state object so the drain callback -// the native ResumableSink stores holds only that state, not the whole -// assignStreamIntoResumableSink activation. ResumableSink.drain() invokes -// this with `this = undefined`, so the state is supplied via `.bind()`. -export async function resumableSinkDrain(this: ResumableSinkState) { - if (this.error || this.closed || this.reading) return; - this.reading = true; - - try { - while (true) { - var { value, done } = await this.reader!.read(); - if (this.closed) break; - - if (done) { - this.closed = true; - // lets cover just in case we have a value when done is true - // this shouldn't happen but just in case - if (value) { - this.sink.write(value); - } - // clean end - return $resumableSinkEnd.$call(this); - } - - if (value) { - // write returns false under backpressure - if (!this.sink.write(value)) { - break; - } - } - } - } catch (e: any) { - this.error = e; - this.closed = true; - try { - const prom = this.stream?.cancel(e); - if ($isPromise(prom)) { - $markPromiseAsHandled(prom); - } - } catch {} - // end with the error NT so we can simplify the flow to only listen to end - queueMicrotask($resumableSinkEnd.bind(this, e)); - } finally { - this.reading = false; - } -} - -// Native ResumableSink invokes this as (undefined, reason) — see -// the native ResumableSink.cancel. The first slot is unused here, but the -// parameter is required so the abort reason lands in the right argument. -export function resumableSinkCancel(this: ResumableSinkState, _, reason: Error | null) { - if (this.closed) return; - let wasClosed = this.closed; - this.closed = true; - var stream = this.stream; - if (stream && !this.error && !wasClosed && stream.$state !== $streamClosed) { - $readableStreamCancel(stream, reason); - } - $resumableSinkReleaseReader(this); -} - -$linkTimeConstant; -export function assignStreamIntoResumableSink(stream, sink) { - const highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark") || 0; - - // Mutable state shared between the drain/cancel handlers; bound so they do - // not capture this function's scope. - var state: ResumableSinkState = { - __proto__: null, - stream, - sink, - reader: undefined, - error: null, - reading: false, - closed: false, - }; - - try { - // always call start even if reader throws - - sink.start({ highWaterMark }); - - state.reader = stream.getReader(); - - var drain = $resumableSinkDrain.bind(state); - - // drain is called when the backpressure is release so we can continue draining - // cancel is called if closed or errored by the other side - sink.setHandlers(drain, $resumableSinkCancel.bind(state)); - - drain(); - } catch (e: any) { - state.error = e; - state.closed = true; - // end with the error - queueMicrotask($resumableSinkEnd.bind(state, e)); - } -} - -// Bound (via `this`) to readStreamIntoSink's per-request state object so the -// onClose callback the native sink stores in m_onClose holds only that small -// state, not the whole readStreamIntoSink activation. -export function readStreamIntoSinkOnClose(this: { didThrow: boolean; didClose: boolean }, stream, reason) { - if (!this.didThrow && !this.didClose && stream && stream.$state !== $streamClosed) { - $readableStreamCancel(stream, reason); - } - this.didClose = true; -} - -export async function readStreamIntoSink(stream: ReadableStream, sink, isNative) { - var started = false; - const highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark") || 0; - - // Mutable state onSinkClose needs; bound so it does not capture this - // function's scope. - var state = { __proto__: null, didThrow: false, didClose: false }; - var onSinkClose = isNative ? $readStreamIntoSinkOnClose.bind(state) : undefined; - - try { - var reader = stream.getReader(); - var many = reader.readMany(); - - if (many && $isPromise(many)) { - // Some time may pass before this Promise is fulfilled. The sink may - // abort, for example. So we have to start it, if only so that we can - // receive a notification when it closes or cancels. - // https://github.com/oven-sh/bun/issues/6758 - if (isNative) $startDirectStream.$call(sink, stream, undefined, onSinkClose, stream.$asyncContext); - sink.start({ highWaterMark }); - started = true; - - many = await many; - } - if (many.done) { - state.didClose = true; - return sink.end(); - } - - if (!started) { - if (isNative) $startDirectStream.$call(sink, stream, undefined, onSinkClose, stream.$asyncContext); - sink.start({ highWaterMark }); - } - - for (var i = 0, values = many.value, length = many.value.length; i < length; i++) { - // The HTTP response sink returns a negative number when the socket is - // backed up; await flush(true) (the pending-flush promise) so we stop - // pulling until it drains. FileSink may return a Promise on every write - // (Windows pipes are always async); awaiting that here would serialize - // every chunk behind a uv_write round-trip, so the negative-number check - // intentionally lets those fall through. - const wrote = sink.write(values[i]); - if (wrote < 0) { - await sink.flush(true); - // The sink's close path resolves the same promise; stop writing into a - // dead sink. - if (state.didClose) break; - } else if ($isPromise(wrote)) { - // Intentionally unawaited (see above). The sink rejects it if the - // destination goes away mid-write (e.g. the subprocess exited); that - // already cancels the stream, so don't report an unhandled rejection. - $markPromiseAsHandled(wrote); - } - } - values = many = undefined; - - var streamState = $getByIdDirectPrivate(stream, "state"); - if (state.didClose || streamState === $streamClosed) { - state.didClose = true; - return sink.end(); - } - - while (true) { - var { value, done } = await reader.read(); - if (done) { - state.didClose = true; - return sink.end(); - } - - const wrote = sink.write(value); - if (wrote < 0) { - await sink.flush(true); - if (state.didClose) return sink.end(); - } else if ($isPromise(wrote)) { - // See the identical branch above. - $markPromiseAsHandled(wrote); - } - } - } catch (e) { - state.didThrow = true; - - try { - reader = undefined; - const prom = stream.cancel(e); - if ($isPromise(prom)) { - $markPromiseAsHandled(prom); - } - } catch {} - - if (sink && !state.didClose) { - state.didClose = true; - try { - sink.close(e); - } catch (j) { - throw new globalThis.AggregateError([e, j]); - } - } - - throw e; - } finally { - if (reader) { - try { - reader.releaseLock(); - } catch {} - reader = undefined; - } - sink = undefined; - if (stream) { - var streamState = $getByIdDirectPrivate(stream, "state"); - // make it easy for this to be GC'd - // but don't do property transitions - var readableStreamController = $getByIdDirectPrivate(stream, "readableStreamController"); - if (readableStreamController) { - if ($getByIdDirectPrivate(readableStreamController, "underlyingSource")) - $putByIdDirectPrivate(readableStreamController, "underlyingSource", null); - if ($getByIdDirectPrivate(readableStreamController, "controlledReadableStream")) - $putByIdDirectPrivate(readableStreamController, "controlledReadableStream", null); - - $putByIdDirectPrivate(stream, "readableStreamController", null); - if ($getByIdDirectPrivate(stream, "underlyingSource")) $putByIdDirectPrivate(stream, "underlyingSource", null); - readableStreamController = undefined; - } - - if (stream && !state.didThrow && streamState !== $streamClosed && streamState !== $streamErrored) { - $readableStreamCloseIfPossible(stream); - } - stream = undefined; - } - } -} - -export function handleDirectStreamError(e) { - var controller = this; - var sink = controller.$sink; - if (sink) { - $putByIdDirectPrivate(controller, "sink", undefined); - try { - sink.close(e); - } catch {} - } - - this.error = this.flush = this.write = this.close = this.end = $onReadableStreamDirectControllerClosed; - - const underlyingSource = this.$underlyingSource; - const underlyingClose = underlyingSource.close; - if (typeof underlyingClose === "function") { - try { - underlyingClose.$call(underlyingSource, e); - } catch {} - } - - try { - var pend = controller._pendingRead; - if (pend) { - controller._pendingRead = undefined; - $rejectPromise(pend, e); - } - } catch {} - var stream = controller.$controlledReadableStream; - if (stream) $readableStreamError(stream, e); -} - -export function handleDirectStreamErrorReject(e) { - $handleDirectStreamError.$call(this, e); - return Promise.$reject(e); -} - -export function onPullDirectStream(controller: ReadableStreamDirectController) { - var stream = controller.$controlledReadableStream; - if (!stream || $getByIdDirectPrivate(stream, "state") !== $streamReadable) return; - - // pull is in progress - // this is a recursive call - // ignore it - if (controller._deferClose === -1) { - return; - } - - controller._deferClose = -1; - controller._deferFlush = -1; - var deferClose; - var deferFlush; - - var asyncContext = stream.$asyncContext; - if (asyncContext) { - var prev = $getInternalField($asyncContext, 0); - $putInternalField($asyncContext, 0, asyncContext); - } - - // Direct streams allow $pull to be called multiple times, unlike the spec. - // Backpressure is handled by the destination, not by the underlying source. - // In this case, we rely on the heuristic that repeatedly draining in the same tick - // is bad for performance - // this code is only run when consuming a direct stream from JS - // without the HTTP server or anything else - try { - var result = controller.$underlyingSource.pull(controller); - - if (result && $isPromise(result)) { - if (controller._handleError === undefined) { - controller._handleError = $handleDirectStreamErrorReject.bind(controller); - } - - result.catch(controller._handleError); - } - } catch (e) { - return $handleDirectStreamErrorReject.$call(controller, e); - } finally { - deferClose = controller._deferClose; - deferFlush = controller._deferFlush; - controller._deferFlush = controller._deferClose = 0; - - if (asyncContext) { - $putInternalField($asyncContext, 0, prev); - } - } - - var promiseToReturn; - - if (controller._pendingRead === undefined) { - controller._pendingRead = promiseToReturn = $newPromise(); - } else { - promiseToReturn = $readableStreamAddReadRequest(stream); - } - - // they called close during $pull() - // we delay that - if (deferClose === 1) { - var reason = controller._deferCloseReason; - controller._deferCloseReason = undefined; - $onCloseDirectStream.$call(controller, reason); - return promiseToReturn; - } - - // not done, but they called flush() - if (deferFlush === 1) { - $onFlushDirectStream.$call(controller); - } - - return promiseToReturn; -} - -export function noopDoneFunction() { - return Promise.$resolve({ value: undefined, done: true }); -} - -$alwaysInline = true; -export function readableStreamNoop() {} - -export function onReadableStreamDirectControllerClosed(_reason) { - $throwTypeError("ReadableStreamDirectController is now closed"); -} - -export function tryUseReadableStreamBufferedFastPath(stream, method) { - // -- Fast path for Blob.prototype.stream(), fetch body streams, and incoming Request body streams -- - const ptr = stream.$bunNativePtr; - if ( - // only available on native streams - ptr && - // don't even attempt it if the stream was used in some way - !$isReadableStreamDisturbed(stream) && - // feature-detect if supported - $isCallable(ptr[method]) - ) { - const promise = ptr[method](); - // if it throws, let it throw without setting $disturbed - stream.$disturbed = true; - - // Clear the lazy load function. - $putByIdDirectPrivate(stream, "start", undefined); - $putByIdDirectPrivate(stream, "reader", {}); - - if (Bun.peek.status(promise) === "fulfilled") { - stream.$reader = undefined; - $readableStreamCloseIfPossible(stream); - return promise; - } - - return promise - .catch($readableStreamBufferedFastPathCatch.bind(stream)) - .finally($readableStreamBufferedFastPathFinally.bind(stream)); - } -} - -export function readableStreamBufferedFastPathCatch(this: ReadableStream, e) { - this.$reader = undefined; - $readableStreamCancel(this, e); - return Promise.$reject(e); -} - -export function readableStreamBufferedFastPathFinally(this: ReadableStream) { - this.$reader = undefined; - $readableStreamCloseIfPossible(this); -} - -export function onCloseDirectStream(reason) { - var stream = this.$controlledReadableStream; - if (!stream || $getByIdDirectPrivate(stream, "state") !== $streamReadable) return; - - if (this._deferClose !== 0) { - this._deferClose = 1; - this._deferCloseReason = reason; - return; - } - - var sink = this.$sink; - if (!sink) return; - - $putByIdDirectPrivate(stream, "state", $streamClosing); - const underlyingSource = this.$underlyingSource; - const underlyingClose = underlyingSource.close; - if (typeof underlyingClose === "function") { - try { - underlyingClose.$call(underlyingSource, reason); - } catch {} - } - - var flushed; - try { - flushed = sink.end(); - $putByIdDirectPrivate(this, "sink", undefined); - } catch (e) { - if (this._pendingRead) { - var read = this._pendingRead; - this._pendingRead = undefined; - $rejectPromise(read, e); - } else { - throw e; - } - - return; - } - - this.error = this.flush = this.write = this.close = this.end = $onReadableStreamDirectControllerClosed; - - var reader = $getByIdDirectPrivate(stream, "reader"); - - if (reader && $isReadableStreamDefaultReader(reader)) { - var _pendingRead = this._pendingRead; - if (_pendingRead && $isPromise(_pendingRead) && flushed?.byteLength) { - this._pendingRead = undefined; - $fulfillPromise(_pendingRead, { value: flushed, done: false }); - $readableStreamCloseIfPossible(stream); - return; - } - } - - if (flushed?.byteLength) { - var requests = $getByIdDirectPrivate(reader, "readRequests"); - if (requests?.isNotEmpty()) { - $readableStreamFulfillReadRequest(stream, flushed, false); - $readableStreamCloseIfPossible(stream); - return; - } - - $putByIdDirectPrivate(stream, "state", $streamReadable); - this.$pull = $onCloseDirectStreamFinalPull.bind({ __proto__: null, flushed, stream }); - // We will close after the next $pull is called otherwise we would lost the last chunk - return; - } - if (this._pendingRead) { - var read = this._pendingRead; - this._pendingRead = undefined; - $putByIdDirectPrivate(this, "pull", $noopDoneFunction); - $fulfillPromise(read, { value: undefined, done: true }); - } - - $readableStreamCloseIfPossible(stream); -} - -export function onCloseDirectStreamFinalPull(this: { flushed: any; stream: ReadableStream | undefined }) { - var thisResult = $createFulfilledPromise({ - value: this.flushed, - done: false, - }); - this.flushed = undefined; - var stream = this.stream; - this.stream = undefined; - if (stream) $readableStreamCloseIfPossible(stream); - return thisResult; -} - -export function onFlushDirectStream() { - var stream = this.$controlledReadableStream; - if (!stream) return; - var sink = this.$sink; - if (!sink) return; - var reader = $getByIdDirectPrivate(stream, "reader"); - if (!reader || !$isReadableStreamDefaultReader(reader)) { - return; - } - - var _pendingRead = this._pendingRead; - this._pendingRead = undefined; - if (_pendingRead && $isPromise(_pendingRead)) { - var flushed = sink.flush(); - if (flushed?.byteLength) { - this._pendingRead = $getByIdDirectPrivate(stream, "readRequests")?.shift(); - $fulfillPromise(_pendingRead, { value: flushed, done: false }); - } else { - this._pendingRead = _pendingRead; - } - } else if ($getByIdDirectPrivate(stream, "readRequests")?.isNotEmpty()) { - var flushed = sink.flush(); - if (flushed?.byteLength) { - $readableStreamFulfillReadRequest(stream, flushed, false); - } - } else if (this._deferFlush === -1) { - this._deferFlush = 1; - } -} - -export function createTextStream(_highWaterMark: number) { - var sink; - var array = []; - var hasString = false; - var hasBuffer = false; - var rope = ""; - var estimatedLength = $toLength(0); - var capability = $newPromiseCapability(Promise); - var calledDone = false; - - sink = { - start() {}, - write(chunk) { - if (typeof chunk === "string") { - var chunkLength = $toLength(chunk.length); - if (chunkLength > 0) { - rope += chunk; - hasString = true; - // TODO: utf16 byte length - estimatedLength += chunkLength; - } - - return chunkLength; - } - - if (!chunk || !($ArrayBuffer.$isView(chunk) || chunk instanceof $ArrayBuffer)) { - $throwTypeError("Expected text, ArrayBuffer or ArrayBufferView"); - } - - const byteLength = $toLength(chunk.byteLength); - if (byteLength > 0) { - hasBuffer = true; - if (rope.length > 0) { - $arrayPush(array, rope); - $arrayPush(array, chunk); - rope = ""; - } else { - $arrayPush(array, chunk); - } - } - estimatedLength += byteLength; - return byteLength; - }, - - flush() { - return 0; - }, - - end() { - if (calledDone) { - return ""; - } - return sink.fulfill(); - }, - - fulfill() { - calledDone = true; - const result = sink.finishInternal(); - - $fulfillPromise(capability.promise, result); - return result; - }, - - finishInternal() { - if (!hasString && !hasBuffer) { - return ""; - } - - if (hasString && !hasBuffer) { - if (rope.charCodeAt(0) === 0xfeff) { - rope = rope.slice(1); - } - - return rope; - } - - if (hasBuffer && !hasString) { - return new globalThis.TextDecoder("utf-8", { ignoreBOM: true }).decode(Bun.concatArrayBuffers(array)); - } - - // worst case: mixed content - - var arrayBufferSink = new Bun.ArrayBufferSink(); - arrayBufferSink.start({ - highWaterMark: estimatedLength, - asUint8Array: true, - }); - for (let item of array) { - arrayBufferSink.write(item); - } - array.length = 0; - if (rope.length > 0) { - if (rope.charCodeAt(0) === 0xfeff) { - rope = rope.slice(1); - } - - arrayBufferSink.write(rope); - rope = ""; - } - - // TODO: use builtin - return new globalThis.TextDecoder("utf-8", { ignoreBOM: true }).decode(arrayBufferSink.end()); - }, - - close() { - try { - if (!calledDone) { - calledDone = true; - sink.fulfill(); - } - } catch {} - }, - }; - - return [sink, capability]; -} - -export function initializeTextStream(underlyingSource, highWaterMark: number) { - var [sink, closingPromise] = $createTextStream(highWaterMark); - - var controller = { - $underlyingSource: underlyingSource, - $pull: $onPullDirectStream, - $controlledReadableStream: this, - $sink: sink, - close: $onCloseDirectStream, - write: sink.write, - error: $handleDirectStreamError, - end: $onCloseDirectStream, - $close: $onCloseDirectStream, - flush: $onFlushDirectStream, - _pendingRead: undefined, - _deferClose: 0, - _deferFlush: 0, - _deferCloseReason: undefined, - _handleError: undefined, - }; - - $putByIdDirectPrivate(this, "readableStreamController", controller); - $putByIdDirectPrivate(this, "underlyingSource", null); - $putByIdDirectPrivate(this, "start", undefined); - return closingPromise; -} - -export function initializeArrayStream(underlyingSource, _highWaterMark: number) { - var array = []; - var closingPromise = $newPromiseCapability(Promise); - var calledDone = false; - - function fulfill() { - calledDone = true; - closingPromise.resolve.$call(undefined, array); - return array; - } - - var sink = { - start() {}, - write(chunk) { - $arrayPush(array, chunk); - return chunk.byteLength || chunk.length; - }, - - flush() { - return 0; - }, - - end() { - if (calledDone) { - return []; - } - return fulfill(); - }, - - close() { - if (!calledDone) { - fulfill(); - } - }, - }; - - var controller = { - $underlyingSource: underlyingSource, - $pull: $onPullDirectStream, - $controlledReadableStream: this, - $sink: sink, - close: $onCloseDirectStream, - write: sink.write, - error: $handleDirectStreamError, - end: $onCloseDirectStream, - $close: $onCloseDirectStream, - flush: $onFlushDirectStream, - _pendingRead: undefined, - _deferClose: 0, - _deferFlush: 0, - _deferCloseReason: undefined, - _handleError: undefined, - }; - - $putByIdDirectPrivate(this, "readableStreamController", controller); - $putByIdDirectPrivate(this, "underlyingSource", null); - $putByIdDirectPrivate(this, "start", undefined); - return closingPromise; -} - -export function initializeArrayBufferStream(underlyingSource, highWaterMark: number) { - // This is the fallback implementation for direct streams - // When we don't know what the destination type is - // We assume it is a Uint8Array. - - var opts = - highWaterMark && typeof highWaterMark === "number" - ? { highWaterMark, stream: true, asUint8Array: true } - : { stream: true, asUint8Array: true }; - var sink = new Bun.ArrayBufferSink(); - sink.start(opts); - - var controller = { - $underlyingSource: underlyingSource, - $pull: $onPullDirectStream, - $controlledReadableStream: this, - $sink: sink, - close: $onCloseDirectStream, - write: sink.write.bind(sink), - error: $handleDirectStreamError, - end: $onCloseDirectStream, - $close: $onCloseDirectStream, - flush: $onFlushDirectStream, - _pendingRead: undefined, - _deferClose: 0, - _deferFlush: 0, - _deferCloseReason: undefined, - _handleError: undefined, - }; - - $putByIdDirectPrivate(this, "readableStreamController", controller); - $putByIdDirectPrivate(this, "underlyingSource", null); - $putByIdDirectPrivate(this, "start", undefined); -} - -export function readableStreamError(stream, error) { - $assert($isReadableStream(stream)); - $putByIdDirectPrivate(stream, "state", $streamErrored); - $putByIdDirectPrivate(stream, "storedError", error); - const reader = $getByIdDirectPrivate(stream, "reader"); - - if (!reader) return; - - $getByIdDirectPrivate(reader, "closedPromiseCapability").reject.$call(undefined, error); - const promise = $getByIdDirectPrivate(reader, "closedPromiseCapability").promise; - $markPromiseAsHandled(promise); - - if ($isReadableStreamDefaultReader(reader)) { - $readableStreamDefaultReaderErrorReadRequests(reader, error); - } else { - $assert($isReadableStreamBYOBReader(reader)); - const requests = $getByIdDirectPrivate(reader, "readIntoRequests"); - $putByIdDirectPrivate(reader, "readIntoRequests", $createFIFO()); - for (var request = requests.shift(); request; request = requests.shift()) $rejectPromise(request, error); - } -} - -export function readableStreamDefaultControllerShouldCallPull(controller) { - if (!$readableStreamDefaultControllerCanCloseOrEnqueue(controller)) return false; - if (!($getByIdDirectPrivate(controller, "started") === 1)) return false; - - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - - if ( - (!$isReadableStreamLocked(stream) || - !$getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty()) && - $readableStreamDefaultControllerGetDesiredSize(controller) <= 0 - ) - return false; - const desiredSize = $readableStreamDefaultControllerGetDesiredSize(controller); - $assert(desiredSize !== null); - return desiredSize > 0; -} - -export function readableStreamDefaultControllerCallPullIfNeeded(controller) { - // FIXME: use $readableStreamDefaultControllerShouldCallPull - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - - if (!$readableStreamDefaultControllerCanCloseOrEnqueue(controller)) return; - if (!($getByIdDirectPrivate(controller, "started") === 1)) return; - if ( - (!$isReadableStreamLocked(stream) || - !$getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty()) && - $readableStreamDefaultControllerGetDesiredSize(controller) <= 0 - ) - return; - - if ($getByIdDirectPrivate(controller, "pulling")) { - $putByIdDirectPrivate(controller, "pullAgain", true); - return; - } - - $assert(!$getByIdDirectPrivate(controller, "pullAgain")); - $putByIdDirectPrivate(controller, "pulling", true); - $getByIdDirectPrivate(controller, "pullAlgorithm") - .$call(undefined) - .$then( - $readableStreamDefaultControllerPullFulfilled.bind(controller), - $readableStreamDefaultControllerPullRejected.bind(controller), - ); -} - -export function readableStreamDefaultControllerPullFulfilled(this: ReadableStreamDefaultController) { - $putByIdDirectPrivate(this, "pulling", false); - if ($getByIdDirectPrivate(this, "pullAgain")) { - $putByIdDirectPrivate(this, "pullAgain", false); - - $readableStreamDefaultControllerCallPullIfNeeded(this); - } -} - -export function readableStreamDefaultControllerPullRejected(this: ReadableStreamDefaultController, error) { - $readableStreamDefaultControllerError(this, error); -} - -$alwaysInline = true; -export function isReadableStreamLocked(stream) { - $assert($isReadableStream(stream)); - return ( - // Case 1. Is there a reader actively using it? - !!$getByIdDirectPrivate(stream, "reader") || - // Case 2. Has the native reader been released? - // Case 3. Has it been converted into a Node.js NativeReadable? - stream.$bunNativePtr === -1 - ); -} - -export function readableStreamDefaultControllerGetDesiredSize(controller) { - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - if (!stream) return null; - const state = $getByIdDirectPrivate(stream, "state"); - - if (state === $streamErrored) return null; - if (state === $streamClosed) return 0; - - return $getByIdDirectPrivate(controller, "strategy").highWaterMark - $getByIdDirectPrivate(controller, "queue").size; -} - -$alwaysInline = true; -export function readableStreamReaderGenericCancel(reader, reason) { - const stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); - $assert(!!stream); - return $readableStreamCancel(stream, reason); -} - -export function readableStreamCancel(stream: ReadableStream, reason: any) { - stream.$disturbed = true; - const state = $getByIdDirectPrivate(stream, "state"); - if (state === $streamClosed) return Promise.$resolve(); - if (state === $streamErrored) return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); - $readableStreamClose(stream); - - // Spec (ReadableStreamCancel step 6): perform each pending readIntoRequest's - // close steps with undefined, i.e. resolve { value: undefined, done: true }. - // This lives here and not in readableStreamClose - at ordinary close a BYOB - // read stays pending until the source responds with byobRequest.respond(0). - const reader = $getByIdDirectPrivate(stream, "reader"); - if (reader && $isReadableStreamBYOBReader(reader)) { - const readIntoRequests = $getByIdDirectPrivate(reader, "readIntoRequests"); - if (readIntoRequests?.isNotEmpty()) { - $putByIdDirectPrivate(reader, "readIntoRequests", $createFIFO()); - for (var request = readIntoRequests.shift(); request; request = readIntoRequests.shift()) - $fulfillPromise(request, { value: undefined, done: true }); - } - } - - const controller = $getByIdDirectPrivate(stream, "readableStreamController"); - if (controller === null) return Promise.$resolve(); - - const cancel = controller.$cancel; - if (cancel) return cancel(controller, reason).$then($readableStreamNoop); - - const close = controller.close; - if (close) return Promise.$resolve(controller.close(reason)); - - $throwTypeError("ReadableStreamController has no cancel or close method"); -} - -$alwaysInline = true; -export function readableStreamDefaultControllerCancel(controller, reason) { - $putByIdDirectPrivate(controller, "queue", $newQueue()); - return $getByIdDirectPrivate(controller, "cancelAlgorithm").$call(undefined, reason); -} - -export function readableStreamDefaultControllerPull(controller) { - var queue = $getByIdDirectPrivate(controller, "queue"); - const content = queue.content; - if (content.isNotEmpty()) { - const chunk = $dequeueValue(queue); - if ($getByIdDirectPrivate(controller, "closeRequested") && content.isEmpty()) { - $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); - } else $readableStreamDefaultControllerCallPullIfNeeded(controller); - - return $createFulfilledPromise({ value: chunk, done: false }); - } - const pendingPromise = $readableStreamAddReadRequest($getByIdDirectPrivate(controller, "controlledReadableStream")); - $readableStreamDefaultControllerCallPullIfNeeded(controller); - return pendingPromise; -} - -export function readableStreamDefaultControllerClose(controller) { - $assert($readableStreamDefaultControllerCanCloseOrEnqueue(controller)); - $putByIdDirectPrivate(controller, "closeRequested", true); - if ($getByIdDirectPrivate(controller, "queue")?.content?.isEmpty()) { - $readableStreamCloseIfPossible($getByIdDirectPrivate(controller, "controlledReadableStream")); - } -} - -export function readableStreamCloseIfPossible(stream) { - switch ($getByIdDirectPrivate(stream, "state")) { - case $streamReadable: - case $streamClosing: { - $readableStreamClose(stream); - break; - } - } -} - -export function readableStreamClose(stream) { - $assert( - $getByIdDirectPrivate(stream, "state") === $streamReadable || - $getByIdDirectPrivate(stream, "state") === $streamClosing, - ); - $putByIdDirectPrivate(stream, "state", $streamClosed); - const reader = $getByIdDirectPrivate(stream, "reader"); - if (!reader) return; - - if ($isReadableStreamDefaultReader(reader)) { - const requests = $getByIdDirectPrivate(reader, "readRequests"); - if (requests.isNotEmpty()) { - $putByIdDirectPrivate(reader, "readRequests", $createFIFO()); - - for (var request = requests.shift(); request; request = requests.shift()) - $fulfillPromise(request, { value: undefined, done: true }); - } - } - // Note: pending BYOB readIntoRequests are intentionally NOT drained here. - // Spec (ReadableStreamClose) only handles default readers; a BYOB read - // pending at close stays pending until the source calls - // byobRequest.respond(0), which returns a zero-length view of the caller's - // (transferred) buffer. The drain-with-undefined step belongs to - // ReadableStreamCancel only. - - // Direct streams store an empty `{}` sentinel in the reader slot (see - // $readDirectStream) to mark themselves locked without a real reader, so it - // has no closedPromiseCapability to resolve. - const closedPromiseCapability = $getByIdDirectPrivate(reader, "closedPromiseCapability"); - if (closedPromiseCapability) closedPromiseCapability.resolve.$call(); -} - -export function readableStreamFulfillReadRequest(stream, chunk, done) { - const readRequest = $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests").shift(); - $fulfillPromise(readRequest, { value: chunk, done: done }); -} - -export function readableStreamDefaultControllerEnqueue(controller, chunk) { - const stream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - // this is checked by callers - $assert($readableStreamDefaultControllerCanCloseOrEnqueue(controller)); - - if ( - $isReadableStreamLocked(stream) && - $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests")?.isNotEmpty() - ) { - $readableStreamFulfillReadRequest(stream, chunk, false); - $readableStreamDefaultControllerCallPullIfNeeded(controller); - return; - } - - try { - let chunkSize = 1; - if ($getByIdDirectPrivate(controller, "strategy").size !== undefined) - chunkSize = $getByIdDirectPrivate(controller, "strategy").size(chunk); - $enqueueValueWithSize($getByIdDirectPrivate(controller, "queue"), chunk, chunkSize); - } catch (error) { - $readableStreamDefaultControllerError(controller, error); - throw error; - } - $readableStreamDefaultControllerCallPullIfNeeded(controller); -} - -export function readableStreamDefaultReaderRead(reader) { - const stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); - $assert(!!stream); - const state = $getByIdDirectPrivate(stream, "state"); - - stream.$disturbed = true; - if (state === $streamClosed) return $createFulfilledPromise({ value: undefined, done: true }); - if (state === $streamErrored) return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); - $assert(state === $streamReadable); - - return $getByIdDirectPrivate(stream, "readableStreamController").$pull( - $getByIdDirectPrivate(stream, "readableStreamController"), - ); -} - -export function readableStreamAddReadRequest(stream) { - $assert($isReadableStreamDefaultReader($getByIdDirectPrivate(stream, "reader"))); - $assert($getByIdDirectPrivate(stream, "state") == $streamReadable); - - const readRequest = $newPromise(); - - $getByIdDirectPrivate($getByIdDirectPrivate(stream, "reader"), "readRequests").push(readRequest); - - return readRequest; -} - -export function isReadableStreamDisturbed(stream) { - $assert($isReadableStream(stream)); - return stream.$disturbed; -} - -$visibility = "Private"; -export function readableStreamDefaultReaderRelease(reader) { - $readableStreamReaderGenericRelease(reader); - $readableStreamDefaultReaderErrorReadRequests( - reader, - $ERR_STREAM_RELEASE_LOCK("Stream reader cancelled via releaseLock()"), - ); -} - -$visibility = "Private"; -export function readableStreamReaderGenericRelease(reader) { - $assert(!!$getByIdDirectPrivate(reader, "ownerReadableStream")); - $assert($getByIdDirectPrivate($getByIdDirectPrivate(reader, "ownerReadableStream"), "reader") === reader); - - if ($getByIdDirectPrivate($getByIdDirectPrivate(reader, "ownerReadableStream"), "state") === $streamReadable) - $getByIdDirectPrivate(reader, "closedPromiseCapability").reject.$call( - undefined, - $ERR_STREAM_RELEASE_LOCK("Stream reader cancelled via releaseLock()"), - ); - else - $putByIdDirectPrivate(reader, "closedPromiseCapability", { - promise: $newHandledRejectedPromise($ERR_STREAM_RELEASE_LOCK("Stream reader cancelled via releaseLock()")), - }); - - const promise = $getByIdDirectPrivate(reader, "closedPromiseCapability").promise; - $markPromiseAsHandled(promise); - - var stream = $getByIdDirectPrivate(reader, "ownerReadableStream"); - if (stream.$bunNativePtr) { - $getByIdDirectPrivate($getByIdDirectPrivate(stream, "readableStreamController"), "underlyingSource").$resume(false); - } - $putByIdDirectPrivate(stream, "reader", undefined); - $putByIdDirectPrivate(reader, "ownerReadableStream", undefined); -} - -export function readableStreamDefaultReaderErrorReadRequests(reader, error) { - const requests = $getByIdDirectPrivate(reader, "readRequests"); - $putByIdDirectPrivate(reader, "readRequests", $createFIFO()); - for (var request = requests.shift(); request; request = requests.shift()) $rejectPromise(request, error); -} - -export function readableStreamDefaultControllerCanCloseOrEnqueue(controller) { - if ($getByIdDirectPrivate(controller, "closeRequested")) { - return false; - } - - const controlledReadableStream = $getByIdDirectPrivate(controller, "controlledReadableStream"); - - if (!$isObject(controlledReadableStream)) { - return false; - } - - return $getByIdDirectPrivate(controlledReadableStream, "state") === $streamReadable; -} - -export function readableStreamFromAsyncIterator(target, fn) { - var cancelled = false, - iter: AsyncIterator; - - // We must eagerly start the async generator to ensure that it works if objects are reused later. - // This impacts Astro, amongst others. - iter = fn.$call(target); - fn = target = undefined; - - if (!$isAsyncGenerator(iter) && typeof iter.next !== "function") { - throw new TypeError("Expected an async generator"); - } - - var runningAsyncIteratorPromise; - async function runAsyncIterator(controller) { - var closingError: Error | undefined, value, done, immediateTask; - - try { - while (!cancelled && !done) { - const promise = iter.next(controller); - - if (cancelled) { - return; - } - - if ($isPromise(promise) && $isPromiseFulfilled(promise)) { - clearImmediate(immediateTask); - ({ value, done } = $peekPromiseSettledValue(promise)); - $assert(!$isPromise(value), "Expected a value, not a promise"); - } else { - immediateTask = setImmediate(() => immediateTask && controller?.flush?.(true)); - ({ value, done } = await promise); - - if (cancelled) { - return; - } - } - - if (!$isUndefinedOrNull(value)) { - // See readStreamIntoSink: the HTTP response sink returns a negative - // number when the socket is backed up; await the drain via - // flush(true). FileSink's Promise return is intentionally not - // awaited here, so mark it handled. - const wrote = controller.write(value); - if (wrote < 0) { - clearImmediate(immediateTask); - immediateTask = undefined; - await controller.flush(true); - } else if ($isPromise(wrote)) { - $markPromiseAsHandled(wrote); - } - } - } - } catch (e) { - closingError = e; - } finally { - clearImmediate(immediateTask); - immediateTask = undefined; - // "iter" will be undefined if the stream was closed above. - - // Stream was closed before we tried writing to it. - if (closingError?.code === "ERR_INVALID_THIS") { - await iter?.return?.(); - return; - } - - if (closingError) { - try { - await iter.throw?.(closingError); - } finally { - iter = undefined; - // eslint-disable-next-line no-throw-literal - throw closingError; - } - } else { - await controller.end(); - if (iter) { - await iter.return?.(); - } - } - iter = undefined; - } - } - - return new ReadableStream({ - type: "direct", - - cancel(reason) { - $debug("readableStreamFromAsyncIterator.cancel", reason); - cancelled = true; - - if (iter) { - const thisIter = iter; - iter = undefined; - if (reason) { - // We return the value so that the caller can await it. - return thisIter.throw?.(reason); - } else { - // undefined === Abort. - // - // We don't want to throw here because it will almost - // inevitably become an uncatchable exception. So instead, we call the - // synthetic return method if it exists to signal that the stream is - // done. - return thisIter?.return?.(); - } - } - }, - - close() { - cancelled = true; - }, - - async pull(controller) { - // pull() may be called multiple times before a single call completes. - // - // But, we only call into the stream once while a stream is in-progress. - if (!runningAsyncIteratorPromise) { - const asyncIteratorPromise = runAsyncIterator(controller); - runningAsyncIteratorPromise = asyncIteratorPromise; - try { - const result = await asyncIteratorPromise; - return result; - } catch (e) { - // The stream's sink already swapped its methods to the - // closed-throw stub; the consumer is gone, so swallow the - // "controller is now closed" error instead of letting it surface as - // an unhandled rejection. Builtin async functions used to return - // JSInternalPromise so this never reached the global tracker. - if (controller.write === $onReadableStreamDirectControllerClosed) return; - throw e; - } finally { - if (runningAsyncIteratorPromise === asyncIteratorPromise) { - runningAsyncIteratorPromise = undefined; - } - } - } - - return runningAsyncIteratorPromise; - }, - }); -} - -export function createLazyLoadedStreamPrototype(): typeof ReadableStreamDefaultController { - function callClose(controller: ReadableStreamDefaultController) { - try { - var source = controller.$underlyingSource; - if (!$readableStreamDefaultControllerCanCloseOrEnqueue(controller)) return; - controller.close(); - } catch (e) { - globalThis.reportError(e); - } finally { - if (source?.$stream) { - source.$stream = undefined; - } - - if (source) { - source.$data = undefined; - } - } - } - - // This was a type: "bytes" until Bun v1.1.44, but pendingPullIntos was not really - // compatible with how we send data to the stream, and "mode: 'byob'" wasn't - // supported so changing it isn't an observable change. - // - // When we receive chunks of data from native code, we sometimes read more - // than what the input buffer provided. When that happens, we return a typed - // array instead of the number of bytes read. - // - // When that happens, the ReadableByteStreamController creates (byteLength / autoAllocateChunkSize) pending pull into descriptors. - // So if that number is something like 16 * 1024, and we actually read 2 MB, you're going to create 128 pending pull into descriptors. - // - // And those pendingPullIntos were often never actually drained. - class NativeReadableStreamSource { - constructor(handle, autoAllocateChunkSize, drainValue) { - $putByIdDirectPrivate(this, "stream", handle); - this.pull = this.#pull.bind(this); - this.cancel = this.#cancel.bind(this); - this.autoAllocateChunkSize = autoAllocateChunkSize; - - if (drainValue !== undefined) { - this.start = controller => { - this.start = undefined; - this.#controller = new WeakRef(controller); - controller.enqueue(drainValue); - }; - } - - handle.onClose = this.#onClose.bind(this); - handle.onDrain = this.#onDrain.bind(this); - } - - #onDrain(chunk) { - var controller = this.#controller?.deref?.(); - if (controller) { - controller.enqueue(chunk); - } - } - - #hasResized = false; - - #adjustHighWaterMark(result) { - const autoAllocateChunkSize = this.autoAllocateChunkSize; - if (result >= autoAllocateChunkSize && !this.#hasResized) { - this.#hasResized = true; - this.autoAllocateChunkSize = Math.min(autoAllocateChunkSize * 2, 1024 * 1024 * 2); - } - } - - #controller?: WeakRef; - - // eslint-disable-next-line no-unused-vars - pull; - // eslint-disable-next-line no-unused-vars - cancel; - // eslint-disable-next-line no-unused-vars - start; - - autoAllocateChunkSize = 0; - #closed = false; - - // EOF signal array passed to `handle.pull(view, closer)`. Native code - // writes `closer[0] = true` synchronously on EOF and the pull callback - // reads it back (including after awaiting a pending pull promise). - // MUST be per-instance: if this were a factory-scope constant it would - // be shared across every NativeReadableStreamSource backed by the same - // prototype (e.g. stdin + a fetch() response body, or two concurrent - // fetch() bodies), and one instance's EOF could incorrectly close - // another. See #29787. - #closer: [boolean] = [false]; - - $data?: Uint8Array; - - // @ts-ignore-next-line - $stream: ReadableStream; - - #onClose() { - this.#closed = true; - var controller = this.#controller?.deref?.(); - this.#controller = undefined; - this.$data = undefined; - - $putByIdDirectPrivate(this, "stream", undefined); - if (controller) { - $enqueueJob(callClose, controller); - } - } - - #getInternalBuffer(chunkSize) { - var chunk = this.$data; - // #handleNumberResult stores the unfilled tail (view.subarray(result)) - // here, so consecutive reads write into advancing offsets of the same - // backing ArrayBuffer and the enqueued chunks share it. Rotate only - // when there is no buffer or autoAllocateChunkSize has grown past the - // one we allocated — the tail itself is reused until a read fills it - // exactly and #handleNumberResult sets $data = undefined. The previous - // check was `chunk.length < chunkSize`, which is true after any - // nonzero read, so every pull allocated a fresh 256KB-2MB Gigacage - // buffer while the previous one was still pinned by the consumer's - // subarray — on Windows that drove commit charge to tens of GB before - // VirtualAlloc(MEM_COMMIT) failed in pas_compact_heap_reservation. - if (!chunk || chunk.buffer.byteLength < chunkSize) { - this.$data = chunk = new Uint8Array(chunkSize); - } - return chunk; - } - - #handleArrayBufferViewResult(result, view, isClosed, controller) { - if (result.byteLength > 0) { - controller.enqueue(result); - } - - if (isClosed) { - $enqueueJob(callClose, controller); - return undefined; - } - - return view; - } - - #handleNumberResult(result, view, isClosed, controller) { - if (result > 0) { - const remaining = view.length - result; - let toEnqueue = view; - - if (remaining > 0) { - toEnqueue = view.subarray(0, result); - view = view.subarray(result); - } else { - view = undefined; - } - - controller.enqueue(toEnqueue); - } - - if (isClosed) { - $enqueueJob(callClose, controller); - return undefined; - } - - return view; - } - - #onNativeReadableStreamResult(result, view, isClosed, controller) { - if (typeof result === "number") { - if (!isClosed) this.#adjustHighWaterMark(result); - return this.#handleNumberResult(result, view, isClosed, controller); - } else if (typeof result === "boolean") { - $enqueueJob(callClose, controller); - return undefined; - } else if ($isTypedArrayView(result)) { - if (!isClosed) this.#adjustHighWaterMark(result.byteLength); - return this.#handleArrayBufferViewResult(result, view, isClosed, controller); - } - - $debug("Unknown result type", result); - throw $ERR_INVALID_STATE("Internal error: invalid result from pull. This is a bug in Bun. Please report it."); - } - - #pull(controller) { - var handle = $getByIdDirectPrivate(this, "stream"); - - if (!handle || this.#closed) { - this.#controller = undefined; - this.#closed = true; - $putByIdDirectPrivate(this, "stream", undefined); - $enqueueJob(callClose, controller); - this.$data = undefined; - return; - } - - if (!this.#controller) { - this.#controller = new WeakRef(controller); - } - - const closer = this.#closer; - closer[0] = false; - - if (this.$data) { - let drainResult = handle.drain(); - if (drainResult) { - this.$data = this.#onNativeReadableStreamResult(drainResult, this.$data, closer[0], controller); - return; - } - } - - const view = this.#getInternalBuffer(this.autoAllocateChunkSize); - const result = handle.pull(view, closer); - if ($isPromise(result)) { - return result.$then( - result => { - this.$data = this.#onNativeReadableStreamResult(result, view, closer[0], controller); - if (this.#closed) { - this.$data = undefined; - } - }, - err => { - this.$data = undefined; - this.#closed = true; - this.#controller = undefined; - controller.error(err); - this.#onClose(); - }, - ); - } - - this.$data = this.#onNativeReadableStreamResult(result, view, closer[0], controller); - if (this.#closed) { - this.$data = undefined; - } - } - - #cancel(reason) { - var handle = $getByIdDirectPrivate(this, "stream"); - this.$data = undefined; - if (handle) { - handle.updateRef(false); - handle.cancel(reason); - $putByIdDirectPrivate(this, "stream", undefined); - } - } - } - // this is reuse of an existing private symbol - NativeReadableStreamSource.prototype.$resume = function (has_ref) { - var handle = $getByIdDirectPrivate(this, "stream"); - if (handle) handle.updateRef(has_ref); - }; - - return NativeReadableStreamSource; -} - -export function lazyLoadStream(stream, autoAllocateChunkSize) { - $debug("lazyLoadStream", stream, autoAllocateChunkSize); - var handle = stream.$bunNativePtr; - if (handle === -1) return; - var Prototype = $lazyStreamPrototypeMap.$get($getPrototypeOf(handle)); - if (Prototype === undefined) { - $lazyStreamPrototypeMap.$set($getPrototypeOf(handle), (Prototype = $createLazyLoadedStreamPrototype())); - } - - stream.$disturbed = true; - - if (autoAllocateChunkSize === undefined) { - // This default is what Node.js uses as well. - autoAllocateChunkSize = 256 * 1024; - } - - const chunkSizeOrCompleteBuffer = handle.start(autoAllocateChunkSize); - let chunkSize, drainValue; - if ($isTypedArrayView(chunkSizeOrCompleteBuffer)) { - chunkSize = 0; - drainValue = chunkSizeOrCompleteBuffer; - } else { - chunkSize = chunkSizeOrCompleteBuffer; - drainValue = handle.drain(); - } - - // empty file, no need for native back-and-forth on this - if (chunkSize === 0) { - if ((drainValue?.byteLength ?? 0) > 0) { - return { - start(controller) { - controller.enqueue(drainValue); - controller.close(); - }, - pull(controller) { - controller.close(); - }, - }; - } - - return { - start(controller) { - controller.close(); - }, - pull(controller) { - controller.close(); - }, - }; - } - - return new Prototype(handle, Math.max(chunkSize, autoAllocateChunkSize), drainValue); -} - -export async function readableStreamIntoArrayProcessManyResult(this: ReadableStreamDefaultReader, result) { - let { done, value } = result; - var chunks = value || []; - - while (!done) { - var thisResult = this.readMany(); - if ($isPromise(thisResult)) { - thisResult = await thisResult; - } - - ({ done, value = [] } = thisResult); - const length = value.length || 0; - if (length > 1) { - chunks = chunks.concat(value); - } else if (length === 1) { - chunks.push(value[0]); - } - } - - return chunks; -} - -export function readableStreamIntoArray(stream) { - var reader = stream.getReader(); - var manyResult; - try { - // readMany() throws synchronously when the stream is already errored. - manyResult = reader.readMany(); - } catch (e) { - return Promise.$reject(e); - } - - if (manyResult && $isPromise(manyResult)) { - return manyResult.$then($readableStreamIntoArrayProcessManyResult.bind(reader)); - } - - return $readableStreamIntoArrayProcessManyResult.$call(reader, manyResult); -} - -export function withoutUTF8BOM(result) { - if (result.charCodeAt(0) === 0xfeff) { - return result.slice(1); - } - - return result; -} - -export function readableStreamIntoText(stream: ReadableStream) { - const highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); - const [textStream, closer] = $createTextStream(highWaterMark); - const prom = $readStreamIntoSink(stream, textStream, false); - - if (prom && $isPromise(prom)) { - return Promise.$resolve(prom).$then(closer.promise).$then($withoutUTF8BOM); - } - - return closer.promise.$then($withoutUTF8BOM); -} - -export function readableStreamToArrayBufferDirect( - stream: ReadableStream, - underlyingSource: any, - asUint8Array: boolean, -) { - var sink = new Bun.ArrayBufferSink(); - $putByIdDirectPrivate(stream, "underlyingSource", null); - $putByIdDirectPrivate(stream, "start", undefined); - $putByIdDirectPrivate(stream, "reader", {}); - stream.$disturbed = true; - var highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); - sink.start({ highWaterMark, asUint8Array }); - var capability = $newPromiseCapability(Promise); - var ended = false; - var pull = underlyingSource.pull; - var close = underlyingSource.close; - - var controller = { - start() {}, - close(_reason) { - if (!ended) { - ended = true; - if (close) { - close(); - } - - $fulfillPromise(capability.promise, sink.end()); - } - }, - end() { - if (!ended) { - ended = true; - if (close) { - close(); - } - $fulfillPromise(capability.promise, sink.end()); - } - }, - flush() { - return 0; - }, - write: sink.write.bind(sink), - }; - - var didError = false; - try { - var firstPull = pull(controller); - } catch (e) { - didError = true; - $putByIdDirectPrivate(stream, "reader", undefined); - $readableStreamError(stream, e); - return Promise.$reject(e); - } finally { - if (!$isPromise(firstPull) && !didError) { - if (stream) { - $putByIdDirectPrivate(stream, "reader", undefined); - $readableStreamCloseIfPossible(stream); - } - controller = close = sink = pull = stream = undefined; - return capability.promise; - } - } - - $assert($isPromise(firstPull)); - return firstPull.then( - () => { - if (!didError && stream) { - $putByIdDirectPrivate(stream, "reader", undefined); - $readableStreamCloseIfPossible(stream); - } - controller = close = sink = pull = stream = undefined; - return capability.promise; - }, - e => { - didError = true; - $putByIdDirectPrivate(stream, "reader", undefined); - if ($getByIdDirectPrivate(stream, "state") === $streamReadable) $readableStreamError(stream, e); - return Promise.$reject(e); - }, - ); -} - -export async function readableStreamToTextDirect(stream, underlyingSource) { - const capability = $initializeTextStream.$call(stream, underlyingSource, undefined); - var reader = stream.getReader(); - - while ($getByIdDirectPrivate(stream, "state") === $streamReadable) { - var thisResult = await reader.read(); - if (thisResult.done) { - break; - } - } - - try { - reader.releaseLock(); - } catch {} - reader = undefined; - stream = undefined; - - return capability.promise; -} - -export async function readableStreamToArrayDirect(stream, underlyingSource) { - const capability = $initializeArrayStream.$call(stream, underlyingSource, undefined); - underlyingSource = undefined; - var reader = stream.getReader(); - try { - while ($getByIdDirectPrivate(stream, "state") === $streamReadable) { - var thisResult = await reader.read(); - if (thisResult.done) { - break; - } - } - - try { - reader.releaseLock(); - } catch {} - reader = undefined; - - return Promise.$resolve(capability.promise); - } finally { - stream = undefined; - reader = undefined; - } -} - -export function readableStreamDefineLazyIterators(prototype) { - var asyncIterator = globalThis.Symbol.asyncIterator; - - var ReadableStreamAsyncIterator = async function* ReadableStreamAsyncIterator(stream, preventCancel) { - var reader = stream.getReader(); - var deferredError; - try { - while (true) { - var done, value; - const firstResult = reader.readMany(); - if ($isPromise(firstResult)) { - ({ done, value } = await firstResult); - } else { - ({ done, value } = firstResult); - } - - if (done) { - return; - } - yield* value; - } - } catch (e) { - deferredError = e; - throw e; - } finally { - reader.releaseLock(); - - if (!preventCancel && !$isReadableStreamLocked(stream)) { - const promise = stream.cancel(deferredError); - if (Bun.peek.status(promise) === "rejected") { - $markPromiseAsHandled(promise); - } - } - } - }; - var createAsyncIterator = function asyncIterator() { - return ReadableStreamAsyncIterator(this, false); - }; - var createValues = function values({ preventCancel = false } = { preventCancel: false }) { - return ReadableStreamAsyncIterator(this, preventCancel); - }; - $Object.$defineProperty(prototype, asyncIterator, { value: createAsyncIterator }); - $Object.$defineProperty(prototype, "values", { value: createValues }); - return prototype; -} diff --git a/src/js/builtins/StreamInternals.ts b/src/js/builtins/StreamInternals.ts deleted file mode 100644 index daf9b96569b9..000000000000 --- a/src/js/builtins/StreamInternals.ts +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * Copyright (C) 2015 Igalia. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -// @internal - -export function markPromiseAsHandled(promise: Promise) { - $assert($isPromise(promise)); - $pokePromiseAsHandled(promise); -} - -export function shieldingPromiseResolve(result) { - const promise = Promise.$resolve(result); - if (promise.$then === undefined) promise.$then = $Promise.prototype.$then; - return promise; -} - -export function promiseInvokeOrNoopMethodNoCatch(object, method, args) { - if (method === undefined) return Promise.$resolve(); - return $shieldingPromiseResolve(method.$apply(object, args)); -} - -export function promiseInvokeOrNoopNoCatch(object, key, args) { - return $promiseInvokeOrNoopMethodNoCatch(object, object[key], args); -} - -export function promiseInvokeOrNoopMethod(object, method, args) { - try { - return $promiseInvokeOrNoopMethodNoCatch(object, method, args); - } catch (error) { - return Promise.$reject(error); - } -} - -export function promiseInvokeOrNoop(object, key, args) { - try { - return $promiseInvokeOrNoopNoCatch(object, key, args); - } catch (error) { - return Promise.$reject(error); - } -} - -export function promiseInvokeOrFallbackOrNoop(object, key1, args1, key2, args2) { - try { - const method = object[key1]; - if (method === undefined) return $promiseInvokeOrNoopNoCatch(object, key2, args2); - return $shieldingPromiseResolve(method.$apply(object, args1)); - } catch (error) { - return Promise.$reject(error); - } -} - -export function validateAndNormalizeQueuingStrategy(size, highWaterMark) { - if (size !== undefined && typeof size !== "function") throw new TypeError("size parameter must be a function"); - - const newHighWaterMark = $toNumber(highWaterMark); - - if (newHighWaterMark !== newHighWaterMark || newHighWaterMark < 0) - throw new RangeError("highWaterMark value is negative or not a number"); - - return { size: size, highWaterMark: newHighWaterMark }; -} - -import type Dequeue from "internal/fifo"; -$linkTimeConstant; -export function createFIFO(): Dequeue { - const Dequeue = require("internal/fifo"); - return new Dequeue(); -} - -export function newQueue() { - return { content: $createFIFO(), size: 0 }; -} - -export function dequeueValue(queue) { - const record = queue.content.shift(); - queue.size -= record.size; - // As described by spec, below case may occur due to rounding errors. - if (queue.size < 0) queue.size = 0; - return record.value; -} - -export function enqueueValueWithSize(queue, value, size) { - size = $toNumber(size); - if (!isFinite(size) || size < 0) throw new RangeError("size has an incorrect value"); - - queue.content.push({ value, size }); - queue.size += size; -} - -export function peekQueueValue(queue) { - return queue.content.peek()?.value; -} - -export function resetQueue(queue) { - $assert("content" in queue); - $assert("size" in queue); - queue.content.clear(); - queue.size = 0; -} - -export function extractSizeAlgorithm(strategy) { - const sizeAlgorithm = strategy.size; - - if (sizeAlgorithm === undefined) return () => 1; - - if (typeof sizeAlgorithm !== "function") throw new TypeError("strategy.size must be a function"); - - return chunk => { - return sizeAlgorithm(chunk); - }; -} - -export function extractHighWaterMark(strategy, defaultHWM) { - const highWaterMark = strategy.highWaterMark; - - if (highWaterMark === undefined) return defaultHWM; - - if (highWaterMark !== highWaterMark || highWaterMark < 0) - throw new RangeError("highWaterMark value is negative or not a number"); - - return $toNumber(highWaterMark); -} - -export function extractHighWaterMarkFromQueuingStrategyInit(init: { highWaterMark?: number }) { - if (!$isObject(init)) throw new TypeError("QueuingStrategyInit argument must be an object."); - const { highWaterMark } = init; - if (highWaterMark === undefined) throw new TypeError("QueuingStrategyInit.highWaterMark member is required."); - - return $toNumber(highWaterMark); -} - -export function createFulfilledPromise(value) { - const promise = $newPromise(); - $fulfillPromise(promise, value); - return promise; -} - -export function toDictionary(value, defaultValue, errorMessage) { - if ($isUndefinedOrNull(value)) return defaultValue; - if (!$isObject(value)) throw $ERR_INVALID_ARG_TYPE(errorMessage); - return value; -} diff --git a/src/js/builtins/TextDecoderStream.ts b/src/js/builtins/TextDecoderStream.ts deleted file mode 100644 index e64ad22f9fd3..000000000000 --- a/src/js/builtins/TextDecoderStream.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeTextDecoderStream() { - const label = arguments.length >= 1 ? arguments[0] : "utf-8"; - const options = arguments.length >= 2 ? arguments[1] : {}; - - const startAlgorithm = () => { - return Promise.$resolve(); - }; - const transformAlgorithm = chunk => { - const decoder = $getByIdDirectPrivate(this, "textDecoder"); - let buffer; - try { - buffer = decoder.decode(chunk, { stream: true }); - } catch (e) { - return Promise.$reject(e); - } - if (buffer) { - const transformStream = $getByIdDirectPrivate(this, "textDecoderStreamTransform"); - const controller = $getByIdDirectPrivate(transformStream, "controller"); - $transformStreamDefaultControllerEnqueue(controller, buffer); - } - return Promise.$resolve(); - }; - const flushAlgorithm = () => { - const decoder = $getByIdDirectPrivate(this, "textDecoder"); - let buffer; - try { - buffer = decoder.decode(undefined, { stream: false }); - } catch (e) { - return Promise.$reject(e); - } - if (buffer) { - const transformStream = $getByIdDirectPrivate(this, "textDecoderStreamTransform"); - const controller = $getByIdDirectPrivate(transformStream, "controller"); - $transformStreamDefaultControllerEnqueue(controller, buffer); - } - return Promise.$resolve(); - }; - - const transform = $createTransformStream(startAlgorithm, transformAlgorithm, flushAlgorithm); - $putByIdDirectPrivate(this, "textDecoderStreamTransform", transform); - - const fatal = !!options.fatal; - const ignoreBOM = !!options.ignoreBOM; - const decoder = new TextDecoder(label, { fatal, ignoreBOM }); - - $putByIdDirectPrivate(this, "fatal", fatal); - $putByIdDirectPrivate(this, "ignoreBOM", ignoreBOM); - $putByIdDirectPrivate(this, "encoding", decoder.encoding); - $putByIdDirectPrivate(this, "textDecoder", decoder); - - return this; -} - -$getter; -export function encoding() { - if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) throw $ERR_INVALID_THIS("TextDecoderStream"); - - return $getByIdDirectPrivate(this, "encoding"); -} - -$getter; -export function fatal() { - if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) throw $ERR_INVALID_THIS("TextDecoderStream"); - - return $getByIdDirectPrivate(this, "fatal"); -} - -$getter; -export function ignoreBOM() { - if (!$getByIdDirectPrivate(this, "textDecoderStreamTransform")) throw $ERR_INVALID_THIS("TextDecoderStream"); - - return $getByIdDirectPrivate(this, "ignoreBOM"); -} - -$getter; -export function readable() { - const transform = $getByIdDirectPrivate(this, "textDecoderStreamTransform"); - if (!transform) throw $ERR_INVALID_THIS("TextDecoderStream"); - - return $getByIdDirectPrivate(transform, "readable"); -} - -$getter; -export function writable() { - const transform = $getByIdDirectPrivate(this, "textDecoderStreamTransform"); - if (!transform) throw $ERR_INVALID_THIS("TextDecoderStream"); - - return $getByIdDirectPrivate(transform, "writable"); -} diff --git a/src/js/builtins/TextEncoderStream.ts b/src/js/builtins/TextEncoderStream.ts deleted file mode 100644 index c9dda44bea71..000000000000 --- a/src/js/builtins/TextEncoderStream.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF - * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeTextEncoderStream() { - const startAlgorithm = () => { - return Promise.$resolve(); - }; - const transformAlgorithm = chunk => { - const encoder = $getByIdDirectPrivate(this, "textEncoderStreamEncoder"); - try { - var buffer = encoder.encode(chunk); - } catch (e) { - return Promise.$reject(e); - } - if (buffer.length) { - const transformStream = $getByIdDirectPrivate(this, "textEncoderStreamTransform"); - const controller = $getByIdDirectPrivate(transformStream, "controller"); - $transformStreamDefaultControllerEnqueue(controller, buffer); - } - return Promise.$resolve(); - }; - const flushAlgorithm = () => { - const encoder = $getByIdDirectPrivate(this, "textEncoderStreamEncoder"); - const buffer = encoder.flush(); - if (buffer.length) { - const transformStream = $getByIdDirectPrivate(this, "textEncoderStreamTransform"); - const controller = $getByIdDirectPrivate(transformStream, "controller"); - $transformStreamDefaultControllerEnqueue(controller, buffer); - } - return Promise.$resolve(); - }; - - const transform = $createTransformStream(startAlgorithm, transformAlgorithm, flushAlgorithm); - $putByIdDirectPrivate(this, "textEncoderStreamTransform", transform); - $putByIdDirectPrivate(this, "textEncoderStreamEncoder", new $TextEncoderStreamEncoder()); - - return this; -} - -$getter; -export function encoding() { - if (!$getByIdDirectPrivate(this, "textEncoderStreamTransform")) throw $ERR_INVALID_THIS("TextEncoderStream"); - - return "utf-8"; -} - -$getter; -export function readable() { - const transform = $getByIdDirectPrivate(this, "textEncoderStreamTransform"); - if (!transform) throw $ERR_INVALID_THIS("TextEncoderStream"); - - return $getByIdDirectPrivate(transform, "readable"); -} - -$getter; -export function writable() { - const transform = $getByIdDirectPrivate(this, "textEncoderStreamTransform"); - if (!transform) throw $ERR_INVALID_THIS("TextEncoderStream"); - - return $getByIdDirectPrivate(transform, "writable"); -} diff --git a/src/js/builtins/TransformStream.ts b/src/js/builtins/TransformStream.ts deleted file mode 100644 index f8bb7d34388e..000000000000 --- a/src/js/builtins/TransformStream.ts +++ /dev/null @@ -1,107 +0,0 @@ -// @ts-nocheck -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeTransformStream(this) { - let transformer = arguments[0]; - - // This is the path for CreateTransformStream. - if ($isObject(transformer) && $getByIdDirectPrivate(transformer, "TransformStream")) return this; - - let writableStrategy = arguments[1]; - let readableStrategy = arguments[2]; - - if (transformer === undefined) transformer = null; - - if (readableStrategy === undefined) readableStrategy = {}; - - if (writableStrategy === undefined) writableStrategy = {}; - - let transformerDict = {}; - if (transformer !== null) { - if ("start" in transformer) { - transformerDict["start"] = transformer["start"]; - if (typeof transformerDict["start"] !== "function") $throwTypeError("transformer.start should be a function"); - } - if ("transform" in transformer) { - transformerDict["transform"] = transformer["transform"]; - if (typeof transformerDict["transform"] !== "function") - $throwTypeError("transformer.transform should be a function"); - } - if ("flush" in transformer) { - transformerDict["flush"] = transformer["flush"]; - if (typeof transformerDict["flush"] !== "function") $throwTypeError("transformer.flush should be a function"); - } - - if ("readableType" in transformer) throw new RangeError("TransformStream transformer has a readableType"); - if ("writableType" in transformer) throw new RangeError("TransformStream transformer has a writableType"); - } - - const readableHighWaterMark = $extractHighWaterMark(readableStrategy, 0); - const readableSizeAlgorithm = $extractSizeAlgorithm(readableStrategy); - - const writableHighWaterMark = $extractHighWaterMark(writableStrategy, 1); - const writableSizeAlgorithm = $extractSizeAlgorithm(writableStrategy); - - const startPromiseCapability = $newPromiseCapability(Promise); - $initializeTransformStream( - this, - startPromiseCapability.promise, - writableHighWaterMark, - writableSizeAlgorithm, - readableHighWaterMark, - readableSizeAlgorithm, - ); - $setUpTransformStreamDefaultControllerFromTransformer(this, transformer, transformerDict); - - if ("start" in transformerDict) { - const controller = $getByIdDirectPrivate(this, "controller"); - const startAlgorithm = () => $promiseInvokeOrNoopMethodNoCatch(transformer, transformerDict["start"], [controller]); - startAlgorithm().$then( - () => { - // FIXME: We probably need to resolve start promise with the result of the start algorithm. - startPromiseCapability.resolve.$call(); - }, - error => { - startPromiseCapability.reject.$call(undefined, error); - }, - ); - } else startPromiseCapability.resolve.$call(); - - return this; -} - -$getter; -export function readable() { - if (!$isTransformStream(this)) throw $ERR_INVALID_THIS("TransformStream"); - - return $getByIdDirectPrivate(this, "readable"); -} - -export function writable() { - if (!$isTransformStream(this)) throw $ERR_INVALID_THIS("TransformStream"); - - return $getByIdDirectPrivate(this, "writable"); -} diff --git a/src/js/builtins/TransformStreamDefaultController.ts b/src/js/builtins/TransformStreamDefaultController.ts deleted file mode 100644 index 84eb6ff6e918..000000000000 --- a/src/js/builtins/TransformStreamDefaultController.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeTransformStreamDefaultController(this) { - return this; -} - -$getter; -export function desiredSize(this) { - if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); - - const stream = $getByIdDirectPrivate(this, "stream"); - const readable = $getByIdDirectPrivate(stream, "readable"); - const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); - - return $readableStreamDefaultControllerGetDesiredSize(readableController); -} - -export function enqueue(this, chunk) { - if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); - - $transformStreamDefaultControllerEnqueue(this, chunk); -} - -export function error(this, e) { - if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); - - $transformStreamDefaultControllerError(this, e); -} - -export function terminate(this) { - if (!$isTransformStreamDefaultController(this)) throw $ERR_INVALID_THIS("TransformStreamDefaultController"); - - $transformStreamDefaultControllerTerminate(this); -} diff --git a/src/js/builtins/TransformStreamInternals.ts b/src/js/builtins/TransformStreamInternals.ts deleted file mode 100644 index 833fafdc6b3e..000000000000 --- a/src/js/builtins/TransformStreamInternals.ts +++ /dev/null @@ -1,349 +0,0 @@ -// @ts-nocheck -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -// @internal - -export function isTransformStream(stream) { - return $isObject(stream) && !!$getByIdDirectPrivate(stream, "readable"); -} - -export function isTransformStreamDefaultController(controller) { - return $isObject(controller) && !!$getByIdDirectPrivate(controller, "transformAlgorithm"); -} - -export function createTransformStream( - startAlgorithm, - transformAlgorithm, - flushAlgorithm, - writableHighWaterMark, - writableSizeAlgorithm, - readableHighWaterMark, - readableSizeAlgorithm, -) { - if (writableHighWaterMark === undefined) writableHighWaterMark = 1; - if (writableSizeAlgorithm === undefined) writableSizeAlgorithm = () => 1; - if (readableHighWaterMark === undefined) readableHighWaterMark = 0; - if (readableSizeAlgorithm === undefined) readableSizeAlgorithm = () => 1; - $assert(writableHighWaterMark >= 0); - $assert(readableHighWaterMark >= 0); - - const transform = {}; - $putByIdDirectPrivate(transform, "TransformStream", true); - - const stream = new TransformStream(transform); - const startPromiseCapability = $newPromiseCapability(Promise); - $initializeTransformStream( - stream, - startPromiseCapability.promise, - writableHighWaterMark, - writableSizeAlgorithm, - readableHighWaterMark, - readableSizeAlgorithm, - ); - - const controller = new TransformStreamDefaultController(); - $setUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); - - startAlgorithm().$then( - () => { - startPromiseCapability.resolve.$call(); - }, - error => { - startPromiseCapability.reject.$call(undefined, error); - }, - ); - - return stream; -} - -export function initializeTransformStream( - stream, - startPromise, - writableHighWaterMark, - writableSizeAlgorithm, - readableHighWaterMark, - readableSizeAlgorithm, -) { - const startAlgorithm = () => { - return startPromise; - }; - const writeAlgorithm = chunk => { - return $transformStreamDefaultSinkWriteAlgorithm(stream, chunk); - }; - const abortAlgorithm = reason => { - return $transformStreamDefaultSinkAbortAlgorithm(stream, reason); - }; - const closeAlgorithm = () => { - return $transformStreamDefaultSinkCloseAlgorithm(stream); - }; - const writable = $createWritableStream( - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - writableHighWaterMark, - writableSizeAlgorithm, - ); - - const pullAlgorithm = () => { - return $transformStreamDefaultSourcePullAlgorithm(stream); - }; - const cancelAlgorithm = reason => { - $transformStreamErrorWritableAndUnblockWrite(stream, reason); - return Promise.$resolve(); - }; - const underlyingSource = {}; - $putByIdDirectPrivate(underlyingSource, "start", startAlgorithm); - $putByIdDirectPrivate(underlyingSource, "pull", pullAlgorithm); - $putByIdDirectPrivate(underlyingSource, "cancel", cancelAlgorithm); - const options = {}; - $putByIdDirectPrivate(options, "size", readableSizeAlgorithm); - $putByIdDirectPrivate(options, "highWaterMark", readableHighWaterMark); - const readable = new ReadableStream(underlyingSource, options); - - // The writable to expose to JS through writable getter. - $putByIdDirectPrivate(stream, "writable", writable); - // The writable to use for the actual transform algorithms. - $putByIdDirectPrivate(stream, "internalWritable", $getInternalWritableStream(writable)); - - $putByIdDirectPrivate(stream, "readable", readable); - $putByIdDirectPrivate(stream, "backpressure", undefined); - $putByIdDirectPrivate(stream, "backpressureChangePromise", undefined); - - $transformStreamSetBackpressure(stream, true); - $putByIdDirectPrivate(stream, "controller", undefined); -} - -export function transformStreamError(stream, e) { - const readable = $getByIdDirectPrivate(stream, "readable"); - const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); - $readableStreamDefaultControllerError(readableController, e); - - $transformStreamErrorWritableAndUnblockWrite(stream, e); -} - -export function transformStreamErrorWritableAndUnblockWrite(stream, e) { - $transformStreamDefaultControllerClearAlgorithms($getByIdDirectPrivate(stream, "controller")); - - const writable = $getByIdDirectPrivate(stream, "internalWritable"); - $writableStreamDefaultControllerErrorIfNeeded($getByIdDirectPrivate(writable, "controller"), e); - - if ($getByIdDirectPrivate(stream, "backpressure")) $transformStreamSetBackpressure(stream, false); -} - -export function transformStreamSetBackpressure(stream, backpressure) { - $assert($getByIdDirectPrivate(stream, "backpressure") !== backpressure); - - const backpressureChangePromise = $getByIdDirectPrivate(stream, "backpressureChangePromise"); - if (backpressureChangePromise !== undefined) backpressureChangePromise.resolve.$call(); - - $putByIdDirectPrivate(stream, "backpressureChangePromise", $newPromiseCapability(Promise)); - $putByIdDirectPrivate(stream, "backpressure", backpressure); -} - -export function setUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm) { - $assert($isTransformStream(stream)); - $assert($getByIdDirectPrivate(stream, "controller") === undefined); - - $putByIdDirectPrivate(controller, "stream", stream); - $putByIdDirectPrivate(stream, "controller", controller); - $putByIdDirectPrivate(controller, "transformAlgorithm", transformAlgorithm); - $putByIdDirectPrivate(controller, "flushAlgorithm", flushAlgorithm); -} - -export function setUpTransformStreamDefaultControllerFromTransformer(stream, transformer, transformerDict) { - const controller = new TransformStreamDefaultController(); - let transformAlgorithm = chunk => { - try { - $transformStreamDefaultControllerEnqueue(controller, chunk); - } catch (e) { - return Promise.$reject(e); - } - return Promise.$resolve(); - }; - let flushAlgorithm = () => { - return Promise.$resolve(); - }; - - if ("transform" in transformerDict) - transformAlgorithm = chunk => { - return $promiseInvokeOrNoopMethod(transformer, transformerDict["transform"], [chunk, controller]); - }; - - if ("flush" in transformerDict) { - flushAlgorithm = () => { - return $promiseInvokeOrNoopMethod(transformer, transformerDict["flush"], [controller]); - }; - } - - $setUpTransformStreamDefaultController(stream, controller, transformAlgorithm, flushAlgorithm); -} - -export function transformStreamDefaultControllerClearAlgorithms(controller) { - // We set transformAlgorithm to true to allow GC but keep the isTransformStreamDefaultController check. - $putByIdDirectPrivate(controller, "transformAlgorithm", true); - $putByIdDirectPrivate(controller, "flushAlgorithm", undefined); -} - -export function transformStreamDefaultControllerEnqueue(controller, chunk) { - const stream = $getByIdDirectPrivate(controller, "stream"); - const readable = $getByIdDirectPrivate(stream, "readable"); - const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); - - $assert(readableController !== undefined); - if (!$readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) - $throwTypeError("TransformStream.readable cannot close or enqueue"); - - try { - $readableStreamDefaultControllerEnqueue(readableController, chunk); - } catch (e) { - $transformStreamErrorWritableAndUnblockWrite(stream, e); - throw $getByIdDirectPrivate(readable, "storedError"); - } - - const backpressure = !$readableStreamDefaultControllerShouldCallPull(readableController); - if (backpressure !== $getByIdDirectPrivate(stream, "backpressure")) { - $assert(backpressure); - $transformStreamSetBackpressure(stream, true); - } -} - -export function transformStreamDefaultControllerError(controller, e) { - $transformStreamError($getByIdDirectPrivate(controller, "stream"), e); -} - -export function transformStreamDefaultControllerPerformTransform(controller, chunk) { - const promiseCapability = $newPromiseCapability(Promise); - - const transformPromise = $getByIdDirectPrivate(controller, "transformAlgorithm").$call(undefined, chunk); - transformPromise.$then( - () => { - promiseCapability.resolve(); - }, - r => { - $transformStreamError($getByIdDirectPrivate(controller, "stream"), r); - promiseCapability.reject.$call(undefined, r); - }, - ); - return promiseCapability.promise; -} - -export function transformStreamDefaultControllerTerminate(controller) { - const stream = $getByIdDirectPrivate(controller, "stream"); - const readable = $getByIdDirectPrivate(stream, "readable"); - const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); - - // FIXME: Update readableStreamDefaultControllerClose to make this check. - if ($readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) - $readableStreamDefaultControllerClose(readableController); - const error = $makeTypeError("the stream has been terminated"); - $transformStreamErrorWritableAndUnblockWrite(stream, error); -} - -export function transformStreamDefaultSinkWriteAlgorithm(stream, chunk) { - const writable = $getByIdDirectPrivate(stream, "internalWritable"); - - $assert($getByIdDirectPrivate(writable, "state") === "writable"); - - const controller = $getByIdDirectPrivate(stream, "controller"); - - if ($getByIdDirectPrivate(stream, "backpressure")) { - const promiseCapability = $newPromiseCapability(Promise); - - const backpressureChangePromise = $getByIdDirectPrivate(stream, "backpressureChangePromise"); - $assert(backpressureChangePromise !== undefined); - backpressureChangePromise.promise.$then( - () => { - const state = $getByIdDirectPrivate(writable, "state"); - if (state === "erroring") { - promiseCapability.reject.$call(undefined, $getByIdDirectPrivate(writable, "storedError")); - return; - } - - $assert(state === "writable"); - $transformStreamDefaultControllerPerformTransform(controller, chunk).$then( - () => { - promiseCapability.resolve(); - }, - e => { - promiseCapability.reject.$call(undefined, e); - }, - ); - }, - e => { - promiseCapability.reject.$call(undefined, e); - }, - ); - - return promiseCapability.promise; - } - return $transformStreamDefaultControllerPerformTransform(controller, chunk); -} - -export function transformStreamDefaultSinkAbortAlgorithm(stream, reason) { - $transformStreamError(stream, reason); - return Promise.$resolve(); -} - -export function transformStreamDefaultSinkCloseAlgorithm(stream) { - const readable = $getByIdDirectPrivate(stream, "readable"); - const controller = $getByIdDirectPrivate(stream, "controller"); - const readableController = $getByIdDirectPrivate(readable, "readableStreamController"); - - const flushAlgorithm = $getByIdDirectPrivate(controller, "flushAlgorithm"); - $assert(flushAlgorithm !== undefined); - const flushPromise = $getByIdDirectPrivate(controller, "flushAlgorithm").$call(); - $transformStreamDefaultControllerClearAlgorithms(controller); - - const promiseCapability = $newPromiseCapability(Promise); - flushPromise.$then( - () => { - if ($getByIdDirectPrivate(readable, "state") === $streamErrored) { - promiseCapability.reject.$call(undefined, $getByIdDirectPrivate(readable, "storedError")); - return; - } - - // FIXME: Update readableStreamDefaultControllerClose to make this check. - if ($readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) - $readableStreamDefaultControllerClose(readableController); - promiseCapability.resolve(); - }, - r => { - $transformStreamError($getByIdDirectPrivate(controller, "stream"), r); - promiseCapability.reject.$call(undefined, $getByIdDirectPrivate(readable, "storedError")); - }, - ); - return promiseCapability.promise; -} - -export function transformStreamDefaultSourcePullAlgorithm(stream) { - $assert($getByIdDirectPrivate(stream, "backpressure")); - $assert($getByIdDirectPrivate(stream, "backpressureChangePromise") !== undefined); - - $transformStreamSetBackpressure(stream, false); - - return $getByIdDirectPrivate(stream, "backpressureChangePromise").promise; -} diff --git a/src/js/builtins/WritableStreamDefaultController.ts b/src/js/builtins/WritableStreamDefaultController.ts deleted file mode 100644 index 05cf16ba0656..000000000000 --- a/src/js/builtins/WritableStreamDefaultController.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeWritableStreamDefaultController(this) { - $putByIdDirectPrivate(this, "queue", $newQueue()); - $putByIdDirectPrivate(this, "abortSteps", reason => { - const result = $getByIdDirectPrivate(this, "abortAlgorithm").$call(undefined, reason); - $writableStreamDefaultControllerClearAlgorithms(this); - return result; - }); - - $putByIdDirectPrivate(this, "errorSteps", () => { - $resetQueue($getByIdDirectPrivate(this, "queue")); - }); - - return this; -} - -export function error(this, e) { - if ($getByIdDirectPrivate(this, "abortSteps") === undefined) - throw $ERR_INVALID_THIS("WritableStreamDefaultController"); - - const stream = $getByIdDirectPrivate(this, "stream"); - if ($getByIdDirectPrivate(stream, "state") !== "writable") return; - $writableStreamDefaultControllerError(this, e); -} diff --git a/src/js/builtins/WritableStreamDefaultWriter.ts b/src/js/builtins/WritableStreamDefaultWriter.ts deleted file mode 100644 index 87de5aa8e2e4..000000000000 --- a/src/js/builtins/WritableStreamDefaultWriter.ts +++ /dev/null @@ -1,101 +0,0 @@ -// @ts-nocheck -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -export function initializeWritableStreamDefaultWriter(stream) { - // stream can be a WritableStream if WritableStreamDefaultWriter constructor is called directly from JS - // or an InternalWritableStream in other code paths. - const internalStream = $getInternalWritableStream(stream); - if (internalStream) stream = internalStream; - - if (!$isWritableStream(stream)) $throwTypeError("WritableStreamDefaultWriter constructor takes a WritableStream"); - - $setUpWritableStreamDefaultWriter(this, stream); - return this; -} - -$getter; -export function closed() { - if (!$isWritableStreamDefaultWriter(this)) - return Promise.$reject($makeGetterTypeError("WritableStreamDefaultWriter", "closed")); - - return $getByIdDirectPrivate(this, "closedPromise").promise; -} - -$getter; -export function desiredSize() { - if (!$isWritableStreamDefaultWriter(this)) throw $ERR_INVALID_THIS("WritableStreamDefaultWriter"); - - if ($getByIdDirectPrivate(this, "stream") === undefined) $throwTypeError("WritableStreamDefaultWriter has no stream"); - - return $writableStreamDefaultWriterGetDesiredSize(this); -} - -$getter; -export function ready() { - if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); - - return $getByIdDirectPrivate(this, "readyPromise").promise; -} - -export function abort(reason) { - if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); - - if ($getByIdDirectPrivate(this, "stream") === undefined) - return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); - - return $writableStreamDefaultWriterAbort(this, reason); -} - -export function close() { - if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); - - const stream = $getByIdDirectPrivate(this, "stream"); - if (stream === undefined) return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); - - if ($writableStreamCloseQueuedOrInFlight(stream)) - return Promise.$reject($makeTypeError("WritableStreamDefaultWriter is being closed")); - - return $writableStreamDefaultWriterClose(this); -} - -export function releaseLock() { - if (!$isWritableStreamDefaultWriter(this)) throw $ERR_INVALID_THIS("WritableStreamDefaultWriter"); - - const stream = $getByIdDirectPrivate(this, "stream"); - if (stream === undefined) return; - - $assert($getByIdDirectPrivate(stream, "writer") !== undefined); - $writableStreamDefaultWriterRelease(this); -} - -export function write(chunk) { - if (!$isWritableStreamDefaultWriter(this)) return Promise.$reject($ERR_INVALID_THIS("WritableStreamDefaultWriter")); - - if ($getByIdDirectPrivate(this, "stream") === undefined) - return Promise.$reject($makeTypeError("WritableStreamDefaultWriter has no stream")); - - return $writableStreamDefaultWriterWrite(this, chunk); -} diff --git a/src/js/builtins/WritableStreamInternals.ts b/src/js/builtins/WritableStreamInternals.ts deleted file mode 100644 index 9fe4583c6407..000000000000 --- a/src/js/builtins/WritableStreamInternals.ts +++ /dev/null @@ -1,791 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * Copyright (C) 2015 Igalia - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -// @internal - -export function isWritableStream(stream) { - return $isObject(stream) && !!$getByIdDirectPrivate(stream, "underlyingSink"); -} - -export function isWritableStreamDefaultWriter(writer) { - return $isObject(writer) && !!$getByIdDirectPrivate(writer, "closedPromise"); -} - -export function acquireWritableStreamDefaultWriter(stream) { - return new WritableStreamDefaultWriter(stream); -} - -// https://streams.spec.whatwg.org/#create-writable-stream -export function createWritableStream( - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark, - sizeAlgorithm, -) { - $assert(typeof highWaterMark === "number" && highWaterMark === highWaterMark && highWaterMark >= 0); - - const internalStream = {}; - $initializeWritableStreamSlots(internalStream, {}); - const controller = new WritableStreamDefaultController(); - - $setUpWritableStreamDefaultController( - internalStream, - controller, - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark, - sizeAlgorithm, - ); - - return $createWritableStreamFromInternal(internalStream); -} - -export function createInternalWritableStreamFromUnderlyingSink(underlyingSink, strategy) { - const stream = {}; - - if (underlyingSink === undefined) underlyingSink = {}; - - if (strategy === undefined) strategy = {}; - - if (!$isObject(underlyingSink)) $throwTypeError("WritableStream constructor takes an object as first argument"); - - if ("type" in underlyingSink) $throwRangeError("Invalid type is specified"); - - const sizeAlgorithm = $extractSizeAlgorithm(strategy); - const highWaterMark = $extractHighWaterMark(strategy, 1); - - const underlyingSinkDict = {}; - if ("start" in underlyingSink) { - underlyingSinkDict["start"] = underlyingSink["start"]; - if (typeof underlyingSinkDict["start"] !== "function") $throwTypeError("underlyingSink.start should be a function"); - } - if ("write" in underlyingSink) { - underlyingSinkDict["write"] = underlyingSink["write"]; - if (typeof underlyingSinkDict["write"] !== "function") $throwTypeError("underlyingSink.write should be a function"); - } - if ("close" in underlyingSink) { - underlyingSinkDict["close"] = underlyingSink["close"]; - if (typeof underlyingSinkDict["close"] !== "function") $throwTypeError("underlyingSink.close should be a function"); - } - if ("abort" in underlyingSink) { - underlyingSinkDict["abort"] = underlyingSink["abort"]; - if (typeof underlyingSinkDict["abort"] !== "function") $throwTypeError("underlyingSink.abort should be a function"); - } - - $initializeWritableStreamSlots(stream, underlyingSink); - $setUpWritableStreamDefaultControllerFromUnderlyingSink( - stream, - underlyingSink, - underlyingSinkDict, - highWaterMark, - sizeAlgorithm, - ); - - return stream; -} - -export function initializeWritableStreamSlots(stream, underlyingSink) { - $putByIdDirectPrivate(stream, "state", "writable"); - $putByIdDirectPrivate(stream, "storedError", undefined); - $putByIdDirectPrivate(stream, "writer", undefined); - $putByIdDirectPrivate(stream, "controller", undefined); - $putByIdDirectPrivate(stream, "inFlightWriteRequest", undefined); - $putByIdDirectPrivate(stream, "closeRequest", undefined); - $putByIdDirectPrivate(stream, "inFlightCloseRequest", undefined); - $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); - $putByIdDirectPrivate(stream, "writeRequests", $createFIFO()); - $putByIdDirectPrivate(stream, "backpressure", false); - $putByIdDirectPrivate(stream, "underlyingSink", underlyingSink); -} - -export function writableStreamCloseForBindings(stream) { - if ($isWritableStreamLocked(stream)) - return Promise.$reject($makeTypeError("WritableStream.close method can only be used on non locked WritableStream")); - - if ($writableStreamCloseQueuedOrInFlight(stream)) - return Promise.$reject( - $makeTypeError("WritableStream.close method can only be used on a being close WritableStream"), - ); - - return $writableStreamClose(stream); -} - -export function writableStreamAbortForBindings(stream, reason) { - if ($isWritableStreamLocked(stream)) - return Promise.$reject($makeTypeError("WritableStream.abort method can only be used on non locked WritableStream")); - - return $writableStreamAbort(stream, reason); -} - -export function isWritableStreamLocked(stream) { - return $getByIdDirectPrivate(stream, "writer") !== undefined; -} - -export function setUpWritableStreamDefaultWriter(writer, stream) { - if ($isWritableStreamLocked(stream)) $throwTypeError("WritableStream is locked"); - - $putByIdDirectPrivate(writer, "stream", stream); - $putByIdDirectPrivate(stream, "writer", writer); - - const readyPromiseCapability = $newPromiseCapability(Promise); - const closedPromiseCapability = $newPromiseCapability(Promise); - $putByIdDirectPrivate(writer, "readyPromise", readyPromiseCapability); - $putByIdDirectPrivate(writer, "closedPromise", closedPromiseCapability); - - const state = $getByIdDirectPrivate(stream, "state"); - if (state === "writable") { - if ($writableStreamCloseQueuedOrInFlight(stream) || !$getByIdDirectPrivate(stream, "backpressure")) - readyPromiseCapability.resolve.$call(); - } else if (state === "erroring") { - readyPromiseCapability.reject.$call(undefined, $getByIdDirectPrivate(stream, "storedError")); - $markPromiseAsHandled(readyPromiseCapability.promise); - } else if (state === "closed") { - readyPromiseCapability.resolve.$call(); - closedPromiseCapability.resolve.$call(); - } else { - $assert(state === "errored"); - const storedError = $getByIdDirectPrivate(stream, "storedError"); - readyPromiseCapability.reject.$call(undefined, storedError); - $markPromiseAsHandled(readyPromiseCapability.promise); - closedPromiseCapability.reject.$call(undefined, storedError); - $markPromiseAsHandled(closedPromiseCapability.promise); - } -} - -export function writableStreamAbort(stream, reason) { - const state = $getByIdDirectPrivate(stream, "state"); - if (state === "closed" || state === "errored") return Promise.$resolve(); - - const pendingAbortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); - if (pendingAbortRequest !== undefined) return pendingAbortRequest.promise.promise; - - $assert(state === "writable" || state === "erroring"); - let wasAlreadyErroring = false; - if (state === "erroring") { - wasAlreadyErroring = true; - reason = undefined; - } - - const abortPromiseCapability = $newPromiseCapability(Promise); - $putByIdDirectPrivate(stream, "pendingAbortRequest", { - promise: abortPromiseCapability, - reason: reason, - wasAlreadyErroring: wasAlreadyErroring, - }); - - if (!wasAlreadyErroring) $writableStreamStartErroring(stream, reason); - return abortPromiseCapability.promise; -} - -export function writableStreamClose(stream) { - const state = $getByIdDirectPrivate(stream, "state"); - if (state === "closed" || state === "errored") - return Promise.$reject($makeTypeError("Cannot close a writable stream that is closed or errored")); - - $assert(state === "writable" || state === "erroring"); - $assert(!$writableStreamCloseQueuedOrInFlight(stream)); - - const closePromiseCapability = $newPromiseCapability(Promise); - $putByIdDirectPrivate(stream, "closeRequest", closePromiseCapability); - - const writer = $getByIdDirectPrivate(stream, "writer"); - if (writer !== undefined && $getByIdDirectPrivate(stream, "backpressure") && state === "writable") - $getByIdDirectPrivate(writer, "readyPromise").resolve.$call(); - - $writableStreamDefaultControllerClose($getByIdDirectPrivate(stream, "controller")); - - return closePromiseCapability.promise; -} - -export function writableStreamAddWriteRequest(stream) { - $assert($isWritableStreamLocked(stream)); - $assert($getByIdDirectPrivate(stream, "state") === "writable"); - - const writePromiseCapability = $newPromiseCapability(Promise); - const writeRequests = $getByIdDirectPrivate(stream, "writeRequests"); - writeRequests.push(writePromiseCapability); - return writePromiseCapability.promise; -} - -export function writableStreamCloseQueuedOrInFlight(stream) { - return ( - $getByIdDirectPrivate(stream, "closeRequest") !== undefined || - $getByIdDirectPrivate(stream, "inFlightCloseRequest") !== undefined - ); -} - -export function writableStreamDealWithRejection(stream, error) { - const state = $getByIdDirectPrivate(stream, "state"); - if (state === "writable") { - $writableStreamStartErroring(stream, error); - return; - } - - $assert(state === "erroring"); - $writableStreamFinishErroring(stream); -} - -export function writableStreamFinishErroring(stream) { - $assert($getByIdDirectPrivate(stream, "state") === "erroring"); - $assert(!$writableStreamHasOperationMarkedInFlight(stream)); - - $putByIdDirectPrivate(stream, "state", "errored"); - - const controller = $getByIdDirectPrivate(stream, "controller"); - $getByIdDirectPrivate(controller, "errorSteps").$call(); - - const storedError = $getByIdDirectPrivate(stream, "storedError"); - const requests = $getByIdDirectPrivate(stream, "writeRequests"); - for (var request = requests.shift(); request; request = requests.shift()) - request.reject.$call(undefined, storedError); - - // TODO: is this still necessary? - $putByIdDirectPrivate(stream, "writeRequests", $createFIFO()); - - const abortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); - if (abortRequest === undefined) { - $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - return; - } - - $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); - if (abortRequest.wasAlreadyErroring) { - abortRequest.promise.reject.$call(undefined, storedError); - $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - return; - } - - $getByIdDirectPrivate(controller, "abortSteps") - .$call(undefined, abortRequest.reason) - .$then( - () => { - abortRequest.promise.resolve.$call(); - $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - }, - reason => { - abortRequest.promise.reject.$call(undefined, reason); - $writableStreamRejectCloseAndClosedPromiseIfNeeded(stream); - }, - ); -} - -export function writableStreamFinishInFlightClose(stream) { - const inFlightCloseRequest = $getByIdDirectPrivate(stream, "inFlightCloseRequest"); - inFlightCloseRequest.resolve.$call(); - - $putByIdDirectPrivate(stream, "inFlightCloseRequest", undefined); - - const state = $getByIdDirectPrivate(stream, "state"); - $assert(state === "writable" || state === "erroring"); - - if (state === "erroring") { - $putByIdDirectPrivate(stream, "storedError", undefined); - const abortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); - if (abortRequest !== undefined) { - abortRequest.promise.resolve.$call(); - $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); - } - } - - $putByIdDirectPrivate(stream, "state", "closed"); - - const writer = $getByIdDirectPrivate(stream, "writer"); - if (writer !== undefined) $getByIdDirectPrivate(writer, "closedPromise").resolve.$call(); - - $assert($getByIdDirectPrivate(stream, "pendingAbortRequest") === undefined); - $assert($getByIdDirectPrivate(stream, "storedError") === undefined); -} - -export function writableStreamFinishInFlightCloseWithError(stream, error) { - const inFlightCloseRequest = $getByIdDirectPrivate(stream, "inFlightCloseRequest"); - $assert(inFlightCloseRequest !== undefined); - inFlightCloseRequest.reject.$call(undefined, error); - - $putByIdDirectPrivate(stream, "inFlightCloseRequest", undefined); - - const state = $getByIdDirectPrivate(stream, "state"); - $assert(state === "writable" || state === "erroring"); - - const abortRequest = $getByIdDirectPrivate(stream, "pendingAbortRequest"); - if (abortRequest !== undefined) { - abortRequest.promise.reject.$call(undefined, error); - $putByIdDirectPrivate(stream, "pendingAbortRequest", undefined); - } - - $writableStreamDealWithRejection(stream, error); -} - -export function writableStreamFinishInFlightWrite(stream) { - const inFlightWriteRequest = $getByIdDirectPrivate(stream, "inFlightWriteRequest"); - $assert(inFlightWriteRequest !== undefined); - inFlightWriteRequest.resolve.$call(); - - $putByIdDirectPrivate(stream, "inFlightWriteRequest", undefined); -} - -export function writableStreamFinishInFlightWriteWithError(stream, error) { - const inFlightWriteRequest = $getByIdDirectPrivate(stream, "inFlightWriteRequest"); - $assert(inFlightWriteRequest !== undefined); - inFlightWriteRequest.reject.$call(undefined, error); - - $putByIdDirectPrivate(stream, "inFlightWriteRequest", undefined); - - const state = $getByIdDirectPrivate(stream, "state"); - $assert(state === "writable" || state === "erroring"); - - $writableStreamDealWithRejection(stream, error); -} - -export function writableStreamHasOperationMarkedInFlight(stream) { - return ( - $getByIdDirectPrivate(stream, "inFlightWriteRequest") !== undefined || - $getByIdDirectPrivate(stream, "inFlightCloseRequest") !== undefined - ); -} - -export function writableStreamMarkCloseRequestInFlight(stream) { - const closeRequest = $getByIdDirectPrivate(stream, "closeRequest"); - $assert($getByIdDirectPrivate(stream, "inFlightCloseRequest") === undefined); - $assert(closeRequest !== undefined); - - $putByIdDirectPrivate(stream, "inFlightCloseRequest", closeRequest); - $putByIdDirectPrivate(stream, "closeRequest", undefined); -} - -export function writableStreamMarkFirstWriteRequestInFlight(stream) { - const writeRequests = $getByIdDirectPrivate(stream, "writeRequests"); - $assert($getByIdDirectPrivate(stream, "inFlightWriteRequest") === undefined); - $assert(writeRequests.isNotEmpty()); - - const writeRequest = writeRequests.shift(); - $putByIdDirectPrivate(stream, "inFlightWriteRequest", writeRequest); -} - -export function writableStreamRejectCloseAndClosedPromiseIfNeeded(stream) { - $assert($getByIdDirectPrivate(stream, "state") === "errored"); - - const storedError = $getByIdDirectPrivate(stream, "storedError"); - - const closeRequest = $getByIdDirectPrivate(stream, "closeRequest"); - if (closeRequest !== undefined) { - $assert($getByIdDirectPrivate(stream, "inFlightCloseRequest") === undefined); - closeRequest.reject.$call(undefined, storedError); - $putByIdDirectPrivate(stream, "closeRequest", undefined); - } - - const writer = $getByIdDirectPrivate(stream, "writer"); - if (writer !== undefined) { - const closedPromise = $getByIdDirectPrivate(writer, "closedPromise"); - closedPromise.reject.$call(undefined, storedError); - $markPromiseAsHandled(closedPromise.promise); - } -} - -export function writableStreamStartErroring(stream, reason) { - $assert($getByIdDirectPrivate(stream, "storedError") === undefined); - $assert($getByIdDirectPrivate(stream, "state") === "writable"); - - const controller = $getByIdDirectPrivate(stream, "controller"); - $assert(controller !== undefined); - - $putByIdDirectPrivate(stream, "state", "erroring"); - $putByIdDirectPrivate(stream, "storedError", reason); - - const writer = $getByIdDirectPrivate(stream, "writer"); - if (writer !== undefined) $writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason); - - if (!$writableStreamHasOperationMarkedInFlight(stream) && $getByIdDirectPrivate(controller, "started") === 1) - $writableStreamFinishErroring(stream); -} - -export function writableStreamUpdateBackpressure(stream, backpressure) { - $assert($getByIdDirectPrivate(stream, "state") === "writable"); - $assert(!$writableStreamCloseQueuedOrInFlight(stream)); - - const writer = $getByIdDirectPrivate(stream, "writer"); - if (writer !== undefined && backpressure !== $getByIdDirectPrivate(stream, "backpressure")) { - if (backpressure) $putByIdDirectPrivate(writer, "readyPromise", $newPromiseCapability(Promise)); - else $getByIdDirectPrivate(writer, "readyPromise").resolve.$call(); - } - $putByIdDirectPrivate(stream, "backpressure", backpressure); -} - -export function writableStreamDefaultWriterAbort(writer, reason) { - const stream = $getByIdDirectPrivate(writer, "stream"); - $assert(stream !== undefined); - return $writableStreamAbort(stream, reason); -} - -export function writableStreamDefaultWriterClose(writer) { - const stream = $getByIdDirectPrivate(writer, "stream"); - $assert(stream !== undefined); - return $writableStreamClose(stream); -} - -export function writableStreamDefaultWriterCloseWithErrorPropagation(writer) { - const stream = $getByIdDirectPrivate(writer, "stream"); - $assert(stream !== undefined); - - const state = $getByIdDirectPrivate(stream, "state"); - - if ($writableStreamCloseQueuedOrInFlight(stream) || state === "closed") return Promise.$resolve(); - - if (state === "errored") return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); - - $assert(state === "writable" || state === "erroring"); - return $writableStreamDefaultWriterClose(writer); -} - -export function writableStreamDefaultWriterEnsureClosedPromiseRejected(writer, error) { - let closedPromiseCapability = $getByIdDirectPrivate(writer, "closedPromise"); - let closedPromise = closedPromiseCapability.promise; - - if ($peekPromiseStatus(closedPromise) !== 0) { - closedPromiseCapability = $newPromiseCapability(Promise); - closedPromise = closedPromiseCapability.promise; - $putByIdDirectPrivate(writer, "closedPromise", closedPromiseCapability); - } - - closedPromiseCapability.reject.$call(undefined, error); - $markPromiseAsHandled(closedPromise); -} - -export function writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, error) { - let readyPromiseCapability = $getByIdDirectPrivate(writer, "readyPromise"); - let readyPromise = readyPromiseCapability.promise; - - if ($peekPromiseStatus(readyPromise) !== 0) { - readyPromiseCapability = $newPromiseCapability(Promise); - readyPromise = readyPromiseCapability.promise; - $putByIdDirectPrivate(writer, "readyPromise", readyPromiseCapability); - } - - readyPromiseCapability.reject.$call(undefined, error); - $markPromiseAsHandled(readyPromise); -} - -export function writableStreamDefaultWriterGetDesiredSize(writer) { - const stream = $getByIdDirectPrivate(writer, "stream"); - $assert(stream !== undefined); - - const state = $getByIdDirectPrivate(stream, "state"); - - if (state === "errored" || state === "erroring") return null; - - if (state === "closed") return 0; - - return $writableStreamDefaultControllerGetDesiredSize($getByIdDirectPrivate(stream, "controller")); -} - -export function writableStreamDefaultWriterRelease(writer) { - const stream = $getByIdDirectPrivate(writer, "stream"); - $assert(stream !== undefined); - $assert($getByIdDirectPrivate(stream, "writer") === writer); - - const releasedError = $makeTypeError("writableStreamDefaultWriterRelease"); - - $writableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError); - $writableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError); - - $putByIdDirectPrivate(stream, "writer", undefined); - $putByIdDirectPrivate(writer, "stream", undefined); -} - -export function writableStreamDefaultWriterWrite(writer, chunk) { - const stream = $getByIdDirectPrivate(writer, "stream"); - $assert(stream !== undefined); - - const controller = $getByIdDirectPrivate(stream, "controller"); - $assert(controller !== undefined); - const chunkSize = $writableStreamDefaultControllerGetChunkSize(controller, chunk); - - if (stream !== $getByIdDirectPrivate(writer, "stream")) - return Promise.$reject($makeTypeError("writer is not stream's writer")); - - const state = $getByIdDirectPrivate(stream, "state"); - if (state === "errored") return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); - - if ($writableStreamCloseQueuedOrInFlight(stream) || state === "closed") - return Promise.$reject($makeTypeError("stream is closing or closed")); - - if ($writableStreamCloseQueuedOrInFlight(stream) || state === "closed") - return Promise.$reject($makeTypeError("stream is closing or closed")); - - if (state === "erroring") return Promise.$reject($getByIdDirectPrivate(stream, "storedError")); - - $assert(state === "writable"); - - const promise = $writableStreamAddWriteRequest(stream); - $writableStreamDefaultControllerWrite(controller, chunk, chunkSize); - return promise; -} - -export function setUpWritableStreamDefaultController( - stream, - controller, - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark, - sizeAlgorithm, -) { - $assert($isWritableStream(stream)); - $assert($getByIdDirectPrivate(stream, "controller") === undefined); - - $putByIdDirectPrivate(controller, "stream", stream); - $putByIdDirectPrivate(stream, "controller", controller); - - $resetQueue($getByIdDirectPrivate(controller, "queue")); - - $putByIdDirectPrivate(controller, "started", -1); - $putByIdDirectPrivate(controller, "startAlgorithm", startAlgorithm); - $putByIdDirectPrivate(controller, "strategySizeAlgorithm", sizeAlgorithm); - $putByIdDirectPrivate(controller, "strategyHWM", highWaterMark); - $putByIdDirectPrivate(controller, "writeAlgorithm", writeAlgorithm); - $putByIdDirectPrivate(controller, "closeAlgorithm", closeAlgorithm); - $putByIdDirectPrivate(controller, "abortAlgorithm", abortAlgorithm); - - const backpressure = $writableStreamDefaultControllerGetBackpressure(controller); - $writableStreamUpdateBackpressure(stream, backpressure); - - $writableStreamDefaultControllerStart(controller); -} - -export function writableStreamDefaultControllerStart(controller) { - if ($getByIdDirectPrivate(controller, "started") !== -1) return; - - $putByIdDirectPrivate(controller, "started", 0); - - const startAlgorithm = $getByIdDirectPrivate(controller, "startAlgorithm"); - $putByIdDirectPrivate(controller, "startAlgorithm", undefined); - const stream = $getByIdDirectPrivate(controller, "stream"); - return Promise.$resolve(startAlgorithm.$call()).$then( - () => { - const state = $getByIdDirectPrivate(stream, "state"); - $assert(state === "writable" || state === "erroring"); - $putByIdDirectPrivate(controller, "started", 1); - $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); - }, - error => { - const state = $getByIdDirectPrivate(stream, "state"); - $assert(state === "writable" || state === "erroring"); - $putByIdDirectPrivate(controller, "started", 1); - $writableStreamDealWithRejection(stream, error); - }, - ); -} - -export function setUpWritableStreamDefaultControllerFromUnderlyingSink( - stream, - underlyingSink, - underlyingSinkDict, - highWaterMark, - sizeAlgorithm, -) { - // @ts-ignore - const controller = new $WritableStreamDefaultController(); - - let startAlgorithm: (...args: any[]) => any = () => {}; - let writeAlgorithm: (...args: any[]) => any = () => { - return Promise.$resolve(); - }; - let closeAlgorithm: (...args: any[]) => any = () => { - return Promise.$resolve(); - }; - let abortAlgorithm: (...args: any[]) => any = () => { - return Promise.$resolve(); - }; - - if ("start" in underlyingSinkDict) { - const startMethod = underlyingSinkDict["start"]; - startAlgorithm = () => $promiseInvokeOrNoopMethodNoCatch(underlyingSink, startMethod, [controller]); - } - if ("write" in underlyingSinkDict) { - const writeMethod = underlyingSinkDict["write"]; - writeAlgorithm = chunk => $promiseInvokeOrNoopMethod(underlyingSink, writeMethod, [chunk, controller]); - } - if ("close" in underlyingSinkDict) { - const closeMethod = underlyingSinkDict["close"]; - closeAlgorithm = () => $promiseInvokeOrNoopMethod(underlyingSink, closeMethod, []); - } - if ("abort" in underlyingSinkDict) { - const abortMethod = underlyingSinkDict["abort"]; - abortAlgorithm = reason => $promiseInvokeOrNoopMethod(underlyingSink, abortMethod, [reason]); - } - - $setUpWritableStreamDefaultController( - stream, - controller, - startAlgorithm, - writeAlgorithm, - closeAlgorithm, - abortAlgorithm, - highWaterMark, - sizeAlgorithm, - ); -} - -export function writableStreamDefaultControllerAdvanceQueueIfNeeded(controller) { - const stream = $getByIdDirectPrivate(controller, "stream"); - - if ($getByIdDirectPrivate(controller, "started") !== 1) return; - - $assert(stream !== undefined); - if ($getByIdDirectPrivate(stream, "inFlightWriteRequest") !== undefined) return; - - const state = $getByIdDirectPrivate(stream, "state"); - $assert(state !== "closed" || state !== "errored"); - if (state === "erroring") { - $writableStreamFinishErroring(stream); - return; - } - - const queue = $getByIdDirectPrivate(controller, "queue"); - - if (queue.content?.isEmpty() ?? false) return; - - const value = $peekQueueValue(queue); - if (value === $isCloseSentinel) $writableStreamDefaultControllerProcessClose(controller); - else $writableStreamDefaultControllerProcessWrite(controller, value); -} - -export function isCloseSentinel() {} - -export function writableStreamDefaultControllerClearAlgorithms(controller) { - $putByIdDirectPrivate(controller, "writeAlgorithm", undefined); - $putByIdDirectPrivate(controller, "closeAlgorithm", undefined); - $putByIdDirectPrivate(controller, "abortAlgorithm", undefined); - $putByIdDirectPrivate(controller, "strategySizeAlgorithm", undefined); -} - -export function writableStreamDefaultControllerClose(controller) { - $enqueueValueWithSize($getByIdDirectPrivate(controller, "queue"), $isCloseSentinel, 0); - $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); -} - -export function writableStreamDefaultControllerError(controller, error) { - const stream = $getByIdDirectPrivate(controller, "stream"); - $assert(stream !== undefined); - $assert($getByIdDirectPrivate(stream, "state") === "writable"); - - $writableStreamDefaultControllerClearAlgorithms(controller); - $writableStreamStartErroring(stream, error); -} - -export function writableStreamDefaultControllerErrorIfNeeded(controller, error) { - const stream = $getByIdDirectPrivate(controller, "stream"); - if ($getByIdDirectPrivate(stream, "state") === "writable") $writableStreamDefaultControllerError(controller, error); -} - -export function writableStreamDefaultControllerGetBackpressure(controller) { - const desiredSize = $writableStreamDefaultControllerGetDesiredSize(controller); - return desiredSize <= 0; -} - -export function writableStreamDefaultControllerGetChunkSize(controller, chunk) { - try { - return $getByIdDirectPrivate(controller, "strategySizeAlgorithm").$call(undefined, chunk); - } catch (e) { - $writableStreamDefaultControllerErrorIfNeeded(controller, e); - return 1; - } -} - -export function writableStreamDefaultControllerGetDesiredSize(controller) { - return $getByIdDirectPrivate(controller, "strategyHWM") - $getByIdDirectPrivate(controller, "queue").size; -} - -export function writableStreamDefaultControllerProcessClose(controller) { - const stream = $getByIdDirectPrivate(controller, "stream"); - - $writableStreamMarkCloseRequestInFlight(stream); - $dequeueValue($getByIdDirectPrivate(controller, "queue")); - - $assert($getByIdDirectPrivate(controller, "queue").content?.isEmpty()); - - const sinkClosePromise = $getByIdDirectPrivate(controller, "closeAlgorithm").$call(); - $writableStreamDefaultControllerClearAlgorithms(controller); - - sinkClosePromise.$then( - () => { - $writableStreamFinishInFlightClose(stream); - }, - reason => { - $writableStreamFinishInFlightCloseWithError(stream, reason); - }, - ); -} - -export function writableStreamDefaultControllerProcessWrite(controller, chunk) { - const stream = $getByIdDirectPrivate(controller, "stream"); - - $writableStreamMarkFirstWriteRequestInFlight(stream); - - const sinkWritePromise = $getByIdDirectPrivate(controller, "writeAlgorithm").$call(undefined, chunk); - - sinkWritePromise.$then( - () => { - $writableStreamFinishInFlightWrite(stream); - const state = $getByIdDirectPrivate(stream, "state"); - $assert(state === "writable" || state === "erroring"); - - $dequeueValue($getByIdDirectPrivate(controller, "queue")); - if (!$writableStreamCloseQueuedOrInFlight(stream) && state === "writable") { - const backpressure = $writableStreamDefaultControllerGetBackpressure(controller); - $writableStreamUpdateBackpressure(stream, backpressure); - } - $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); - }, - reason => { - const state = $getByIdDirectPrivate(stream, "state"); - if (state === "writable") $writableStreamDefaultControllerClearAlgorithms(controller); - - $writableStreamFinishInFlightWriteWithError(stream, reason); - }, - ); -} - -export function writableStreamDefaultControllerWrite(controller, chunk, chunkSize) { - try { - $enqueueValueWithSize($getByIdDirectPrivate(controller, "queue"), chunk, chunkSize); - - const stream = $getByIdDirectPrivate(controller, "stream"); - - const state = $getByIdDirectPrivate(stream, "state"); - if (!$writableStreamCloseQueuedOrInFlight(stream) && state === "writable") { - const backpressure = $writableStreamDefaultControllerGetBackpressure(controller); - $writableStreamUpdateBackpressure(stream, backpressure); - } - $writableStreamDefaultControllerAdvanceQueueIfNeeded(controller); - } catch (e) { - $writableStreamDefaultControllerErrorIfNeeded(controller, e); - } -} diff --git a/src/js/internal/sql/query.ts b/src/js/internal/sql/query.ts index ef21dfa92d8c..258f10f05922 100644 --- a/src/js/internal/sql/query.ts +++ b/src/js/internal/sql/query.ts @@ -289,7 +289,7 @@ class Query> extends PublicPromise { // Only mark as handled if there's a rejection handler const hasRejectionHandler = arguments.length >= 2 && arguments[1] != null; if (hasRejectionHandler) { - $markPromiseAsHandled(result); + $pokePromiseAsHandled(result); } return result; @@ -303,7 +303,7 @@ class Query> extends PublicPromise { this.#runAsyncAndCatch(); const result = super.catch.$apply(this, arguments); - $markPromiseAsHandled(result); + $pokePromiseAsHandled(result); return result; } diff --git a/src/js/internal/streams/native-readable.ts b/src/js/internal/streams/native-readable.ts index 2cefb1c3cdda..dbe73be44741 100644 --- a/src/js/internal/streams/native-readable.ts +++ b/src/js/internal/streams/native-readable.ts @@ -6,7 +6,11 @@ // Bun, `fromWeb` is able to check if the stream is backed by a native handle, // to which it will take this path. const Readable = require("internal/streams/readable"); -const transferToNativeReadable = $newCppFunction("ReadableStream.cpp", "jsFunctionTransferToNativeReadableStream", 1); +const transferToNativeReadable = $newCppFunction( + "streams/BunStreamConsumers.cpp", + "jsFunctionTransferToNativeReadableStream", + 1, +); const { errorOrDestroy } = require("internal/streams/destroy"); const kRefCount = Symbol("refCount"); diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index 27fd67eab9cb..cfb4f1bc837e 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -1047,7 +1047,7 @@ impl JSValue { /// promise's await-chain frames to this error's stack. /// /// `this` is the error value (must be a `JSError` or `Exception` cell); - /// no-op otherwise — see `bindings.cpp:Bun__attachAsyncStackFromPromise`. + /// no-op otherwise — see `AsyncStackTrace.cpp:Bun__attachAsyncStackFromPromise`. pub fn attach_async_stack_from_promise(self, global: &JSGlobalObject, promise: &JSPromise) { Bun__attachAsyncStackFromPromise(global, self, promise) } diff --git a/src/jsc/STREAMS.md b/src/jsc/STREAMS.md index 308882cb85ec..708e75228ba6 100644 --- a/src/jsc/STREAMS.md +++ b/src/jsc/STREAMS.md @@ -1,397 +1,89 @@ -# **Bun Streams Architecture: High-Performance I/O in JavaScript** - -### **Table of Contents** - -1. [**Overview & Core Philosophy**](#1-overview--core-philosophy) -2. [**Foundational Concepts**](#2-foundational-concepts) - - 2.1. The Stream Tagging System: Enabling Optimization - - 2.2. The `Body` Mixin: An Intelligent Gateway -3. [**Deep Dive: The Major Performance Optimizations**](#3-deep-dive-the-major-performance-optimizations) - - 3.1. Optimization 1: Synchronous Coercion - Eliminating Streams Entirely - - 3.2. Optimization 2: The Direct Path - Zero-Copy Native Piping - - 3.3. Optimization 3: `readMany()` - Efficient Async Iteration -4. [**Low-Level Implementation Details**](#4-low-level-implementation-details) - - 4.1. The Native Language: `streams.rs` Primitives - - 4.2. Native Sink In-Depth: `HTTPSResponseSink` and Buffering - - 4.3. The Native Collector: `Body.ValueBufferer` - - 4.4. Memory and String Optimizations -5. [**The Unified System: A Complete Data Flow Example**](#5-the-unified-system-a-complete-data-flow-example) -6. [**Conclusion**](#6-conclusion) - ---- - -## 1. Overview & Core Philosophy - -Streams in Bun makes I/O performance in JavaScript competitive with lower-level languages like Go, Rust, and C, while presenting a fully WHATWG-compliant API. - -The core philosophy is **"native-first, JS-fallback"**. Bun assumes that for many high-performance use cases, the JavaScript layer should act as a high-level controller for system-level I/O operations. We try to execute I/O operations with minimal abstraction cost, bypassing the JavaScript virtual machine entirely for performance-critical paths. - -This document details the specific architectural patterns, from the JS/native boundary down to the I/O layer, that enable this level of performance. - -## 2. Foundational Concepts - -To understand Bun's stream optimizations, two foundational concepts must be understood first: the tagging system and the `Body` mixin's role as a state machine. - -### 2.1. The Stream Tagging System: Enabling Optimization - -Identifying the _source_ of a `ReadableStream` at the native level unlocks many optimization opportunities. This is achieved by "tagging" the stream object internally. - -- **Mechanism:** Every `ReadableStream` in Bun holds a private field, `bunNativePtr`, which can point to a native Rust struct representing the stream's underlying source. -- **Identification:** A C++ binding, `ReadableStreamTag__tagged` (from `ReadableStream.rs`), is the primary entry point for this identification. When native code needs to consume a stream (e.g., when sending a `Response` body), it calls this function on the JS `ReadableStream` object to determine its origin. - -```rust -// src/runtime/webcore/ReadableStream.rs -#[repr(i32)] -pub enum Tag { - JavaScript = 0, // A generic, user-defined stream. This is the "slow path". - Blob = 1, // An in-memory blob. Fast path available. - File = 2, // Backed by a native file reader. Fast path available. - Bytes = 4, // Backed by a native network byte stream. Fast path available. - Direct = 3, // Internal native-to-native stream. - Invalid = -1, -} -``` - -This tag is the key that unlocks all subsequent optimizations. It allows the runtime to dispatch to the correct, most efficient implementation path. - -### 2.2. The `Body` Mixin: An Intelligent Gateway - -The `Body` mixin (used by `Request` and `Response`) is not merely a stream container; it's a sophisticated state machine and the primary API gateway to Bun's optimization paths. A `Body`'s content is represented by the `Body.Value` union in native code, which can be a static buffer (`.InternalBlob`, `.WTFStringImpl`) or a live stream (`.Locked`). - -Methods like `.text()`, `.json()`, and `.arrayBuffer()` are not simple stream consumers. They are entry points to a decision tree that aggressively seeks the fastest possible way to fulfill the request. - -```mermaid -stateDiagram-v2 - direction TB - [*] --> StaticBuffer : new Response("hello") - - state StaticBuffer { - [*] --> Ready - Ready : Data in memory - Ready : .WTFStringImpl | .InternalBlob - } - - StaticBuffer --> Locked : Access .body - StaticBuffer --> Used : .text() ⚡ - - state Locked { - [*] --> Streaming - Streaming : ReadableStream created - Streaming : Tagged (File/Bytes/etc) - } - - Locked --> Used : consume stream - Used --> [*] : Complete - - note right of StaticBuffer - Fast Path - Skip streams entirely! - end note - - note right of Locked - Slow Path - Full streaming - end note - - classDef buffer fill:#fbbf24,stroke:#92400e,stroke-width:3px,color:#451a03 - classDef stream fill:#60a5fa,stroke:#1e40af,stroke-width:3px,color:#172554 - classDef final fill:#34d399,stroke:#14532d,stroke-width:3px,color:#052e16 - - class StaticBuffer buffer - class Locked stream - class Used final -``` - -**Diagram 1: `Body.Value` State Transitions** - -## 3. Deep Dive: The Major Performance Optimizations - -### 3.1. Optimization 1: Synchronous Coercion - Eliminating Streams Entirely - -This is the most impactful optimization for a vast number of common API and data processing tasks. - -**The Conventional Problem:** In other JavaScript runtimes, consuming a response body with `.text()` is an inherently asynchronous, multi-step process involving the creation of multiple streams, readers, and promises, which incurs significant overhead. - -**Bun's fast path:** Bun correctly assumes that for many real-world scenarios (e.g., small JSON API responses), the entire response body is already available in a single, contiguous memory buffer when the consuming method is called. It therefore **bypasses the entire stream processing model** and returns the buffer directly. - -**Implementation Architecture & Data Flow:** - -```mermaid -flowchart TB - A["response.text()"] --> B{Check Body Type} - - B -->|"✅ Already Buffered
(InternalBlob, etc.)"|C[⚡ FAST PATH] - B -->|"❌ Is Stream
(.Locked)"|D[🐌 SLOW PATH] - - subgraph fast[" "] - C --> C1[Get buffer pointer] - C1 --> C2[Decode to string] - C2 --> C3[Return resolved Promise] - end - - subgraph slow[" "] - D --> D1[Create pending Promise] - D1 --> D2[Setup native buffering] - D2 --> D3[Collect all chunks] - D3 --> D4[Decode & resolve Promise] - end - - C3 --> E["✨ Result available immediately
(0 async operations)"] - D4 --> F["⏳ Result after I/O completes
(multiple async operations)"] - - style fast fill:#dcfce7,stroke:#166534,stroke-width:3px - style slow fill:#fee2e2,stroke:#991b1b,stroke-width:3px - style C fill:#22c55e,stroke:#166534,stroke-width:3px,color:#14532d - style C1 fill:#86efac,stroke:#166534,stroke-width:2px,color:#14532d - style C2 fill:#86efac,stroke:#166534,stroke-width:2px,color:#14532d - style C3 fill:#86efac,stroke:#166534,stroke-width:2px,color:#14532d - style D fill:#ef4444,stroke:#991b1b,stroke-width:3px,color:#ffffff - style D1 fill:#fca5a5,stroke:#991b1b,stroke-width:2px,color:#450a0a - style D2 fill:#fca5a5,stroke:#991b1b,stroke-width:2px,color:#450a0a - style D3 fill:#fca5a5,stroke:#991b1b,stroke-width:2px,color:#450a0a - style D4 fill:#fca5a5,stroke:#991b1b,stroke-width:2px,color:#450a0a - style E fill:#166534,stroke:#14532d,stroke-width:4px,color:#ffffff - style F fill:#dc2626,stroke:#991b1b,stroke-width:3px,color:#ffffff -``` - -**Diagram 2: Synchronous Coercion Logic Flow** - -1. **Entry Point:** A JS call to `response.text()` triggers `readableStreamToText` (`ReadableStream.ts`), which immediately calls `tryUseReadableStreamBufferedFastPath`. -2. **Native Check:** `tryUseReadableStreamBufferedFastPath` calls the native binding `jsFunctionGetCompleteRequestOrResponseBodyValueAsArrayBuffer` (`Response.rs`). -3. **State Inspection:** This native function inspects the `Body.Value` tag. If the tag is `.InternalBlob`, `.Blob` (and not a disk-backed file), or `.WTFStringImpl`, the complete data is already in memory. -4. **Synchronous Data Transfer:** The function **synchronously** returns the underlying buffer as a native `ArrayBuffer` handle to JavaScript. The `Body` state is immediately transitioned to `.Used`. The buffer's ownership is often transferred (`.transfer` lifetime), avoiding a data copy. -5. **JS Resolution:** The JS layer receives a promise that is **already fulfilled** with the complete `ArrayBuffer`. It then performs the final conversion (e.g., `TextDecoder.decode()`) in a single step. - -**Architectural Impact:** This optimization transforms a complex, multi-tick asynchronous operation into a single, synchronous native call followed by a single conversion step. The performance gain is an order of magnitude or more, as it eliminates the allocation and processing overhead of the entire stream and promise chain. - -### 3.2. Optimization 2: The Direct Path - Zero-Copy Native Piping - -This optimization targets high-throughput scenarios like serving files or proxying requests, where both the data source and destination are native. - -**The Conventional Problem:** Piping a file to an HTTP response in other runtimes involves a costly per-chunk round trip through the JavaScript layer: `Native (read) -> JS (chunk as Uint8Array) -> JS (response.write) -> Native (socket)`. - -**Bun's direct path:** Bun's runtime inspects the source and sink of a pipe. If it identifies a compatible native pair, it establishes a direct data channel between them entirely within the native layer. - -**Implementation Architecture & Data Flow:** - -```mermaid -%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#2563eb','primaryTextColor':'#fff','primaryBorderColor':'#3b82f6','lineColor':'#94a3b8','secondaryColor':'#fbbf24','background':'#f8fafc','mainBkg':'#ffffff','secondBkg':'#f1f5f9'}}}%% -graph TD - subgraph " " - subgraph js["🟨 JavaScript Layer"] - C["📄 new Response(file.stream())"] - end - subgraph native["⚡ Native Layer (Rust)"] - A["💾 Disk I/O
FileReader Source"] - B["🔌 Socket Buffer
HTTPSResponseSink"] - A -."🚀 Zero-Copy View
streams.Result.temporary".-> B - B -."🔙 Backpressure Signal".-> A - end - end - B ==>|"📡 Send"|D["🌐 Network"] - C ==>|"Direct Native
Connection"|A - - style js fill:#fef3c7,stroke:#92400e,stroke-width:3px,color:#451a03 - style native fill:#dbeafe,stroke:#1e40af,stroke-width:3px,color:#172554 - style A fill:#60a5fa,stroke:#1e40af,stroke-width:2px,color:#172554 - style B fill:#60a5fa,stroke:#1e40af,stroke-width:2px,color:#172554 - style C fill:#fbbf24,stroke:#92400e,stroke-width:2px,color:#451a03 - style D fill:#22c55e,stroke:#166534,stroke-width:2px,color:#ffffff - - classDef jsClass fill:#fef3c7,stroke:#f59e0b,stroke-width:2px - classDef nativeClass fill:#dbeafe,stroke:#3b82f6,stroke-width:2px - classDef networkClass fill:#d1fae5,stroke:#10b981,stroke-width:2px -``` - -**Diagram 3: Direct Path for File Serving** - -1. **Scenario:** A server handler returns `new Response(Bun.file("video.mp4").stream())`. -2. **Tagging:** The stream is created with a `File` tag, and its `bunNativePtr` points to a native `webcore.FileReader` struct. The HTTP server's response sink is a native `HTTPSResponseSink`. -3. **Connection via `assignToStream`:** The server's internal logic triggers `assignToStream` (`ReadableStreamInternals.ts`). This function detects the native source via its tag and dispatches to `readDirectStream`. -4. **Native Handoff:** `readDirectStream` calls the C++ binding `$startDirectStream`, which passes pointers to the native `FileReader` (source) and `HTTPSResponseSink` (sink) to the Rust engine. -5. **Zero-Copy Native Data Flow:** The Rust layer takes over. The `FileReader` reads a chunk from the disk. It yields a `streams.Result.temporary` variant, which is a **zero-copy view** into a shared read buffer. This view is passed directly to the `HTTPSResponseSink.write()` method, which appends it to its internal socket write buffer. When possible, Bun will skip the FileReader and use the `sendfile` system call for even less system call interactions. - -**Architectural Impact:** - -- **No Per-Chunk JS Execution:** The JavaScript event loop is not involved in the chunk-by-chunk transfer. -- **Zero Intermediate Copies:** Data moves from the kernel's page cache directly to the network socket's send buffer. -- **Hardware-Limited Throughput:** This architecture removes the runtime as a bottleneck, allowing I/O performance to be limited primarily by hardware speed. - -### 3.3. Optimization 3: `readMany()` - Efficient Async Iteration - -Bun optimizes the standard `for-await-of` loop syntax for streams. - -**The Conventional Problem:** A naive `[Symbol.asyncIterator]` implementation calls `await reader.read()` for every chunk, which is inefficient if many small chunks arrive in quick succession. - -**Bun's Solution:** Bun provides a custom, non-standard `reader.readMany()` method that synchronously drains the stream's entire internal buffer into a JavaScript array. - -**Implementation Architecture & Data Flow:** - -```mermaid -flowchart TB - subgraph trad["Traditional for-await-of"] - direction TB - T1["🔄 for await (chunk of stream)"] - T2["await read() → chunk1"] - T3["Process chunk1"] - T4["await read() → chunk2"] - T5["Process chunk2"] - T6["await read() → chunk3"] - T7["..."] - T1 --> T2 --> T3 --> T4 --> T5 --> T6 --> T7 - end - - subgraph bun["Bun's readMany() Optimization"] - direction TB - B1["🚀 for await (chunks of stream)"] - B2["readMany()"] - B3{"Buffer
Status?"} - B4["⚡ Return [c1, c2, c3]
SYNCHRONOUS"] - B5["Process ALL chunks
in one go"] - B6["await (only if empty)"] - - B1 --> B2 - B2 --> B3 - B3 -->|"Has Data"|B4 - B3 -->|"Empty"|B6 - B4 --> B5 - B5 --> B2 - B6 --> B2 - end - - trad --> P1["❌ Performance Impact
• Promise per chunk
• await per chunk
• High overhead"] - bun --> P2["✅ Performance Win
• Batch processing
• Minimal promises
• Low overhead"] - - style trad fill:#fee2e2,stroke:#7f1d1d,stroke-width:3px - style bun fill:#dcfce7,stroke:#14532d,stroke-width:3px - style T2 fill:#ef4444,stroke:#7f1d1d,color:#ffffff - style T4 fill:#ef4444,stroke:#7f1d1d,color:#ffffff - style T6 fill:#ef4444,stroke:#7f1d1d,color:#ffffff - style B4 fill:#22c55e,stroke:#14532d,stroke-width:3px,color:#ffffff - style B5 fill:#22c55e,stroke:#14532d,stroke-width:3px,color:#ffffff - style P1 fill:#dc2626,stroke:#7f1d1d,stroke-width:3px,color:#ffffff - style P2 fill:#16a34a,stroke:#14532d,stroke-width:3px,color:#ffffff -``` - -**Diagram 4: `readMany()` Async Iterator Flow** - -**Architectural Impact:** This pattern coalesces multiple chunks into a single macro-task. It drastically reduces the number of promise allocations and `await` suspensions required to process a stream, leading to significantly lower CPU usage and higher throughput for chunked data processing. - -### **4. Low-Level Implementation Details** - -The high-level optimizations are made possible by a robust and carefully designed native foundation in Rust. - -#### **4.1. The Native Language: `streams.rs` Primitives** - -The entire native architecture is built upon a set of generic, powerful Rust primitives that define the contracts for data flow. - -- **`streams.Result` Union:** This is the universal data-carrying type for all native stream reads. Its variants are not just data containers; they are crucial signals from the source to the sink. - - `owned: bun.ByteList`: Represents a heap-allocated buffer. The receiver is now responsible for freeing this memory. This is used when data must outlive the current scope. - - `temporary: bun.ByteList`: A borrowed, read-only view into a source's internal buffer. This is the key to **zero-copy reads**, as the sink can process the data without taking ownership or performing a copy. It is only valid for the duration of the function call. - - `owned_and_done` / `temporary_and_done`: These variants bundle the final data chunk with the end-of-stream signal. This is a critical latency optimization, as it collapses two distinct events (data and close) into one, saving an I/O round trip. - - `into_array`: Used for BYOB (Bring-Your-Own-Buffer) readers. It contains a handle to the JS-provided `ArrayBufferView` (`value: JSValue`) and the number of bytes written (`len`). This confirms a zero-copy write directly into JS-managed memory. - - `pending: *Pending`: A handle to a future/promise, used to signal that the result is not yet available and the operation should be suspended. - -- **`streams.Signal` V-Table:** This struct provides a generic, type-erased interface (`start`, `ready`, `close`) for a sink to communicate backpressure and state changes to a source. - - **`start()`**: Tells the source to begin producing data. - - **`ready()`**: The sink calls this to signal it has processed data and is ready for more, effectively managing backpressure. - - **`close()`**: The sink calls this to tell the source to stop, either due to completion or an error. - This v-table decouples native components, allowing any native source to be connected to any native sink without direct knowledge of each other's concrete types, which is essential for the Direct Path optimization. - -#### **4.2. Native Sink In-Depth: `HTTPSResponseSink` and Buffering** - -The `HTTPServerWritable` struct (instantiated as `HTTPSResponseSink` in `streams.rs`) is part of what makes Bun's HTTP server fast. - -- **Intelligent Write Buffering:** The `write` method (`writeBytes`, `writeLatin1`, etc.) does not immediately issue a `write` syscall. It appends the incoming `streams.Result` slice to its internal `buffer: bun.ByteList`. This coalesces multiple small, high-frequency writes (common in streaming LLM responses or SSE) into a single, larger, more efficient syscall. - -- **Backpressure Logic (`send` method):** The `send` method attempts to write the buffer to the underlying `uWebSockets` socket. - - It uses the optimized `res.tryEnd()` for the final chunk. - - If `res.write()` or `res.tryEnd()` returns a "backpressure" signal, the sink immediately sets `this.has_backpressure = true` and registers an `onWritable` callback. - - The `onWritable` callback is triggered by the OS/`uWebSockets` when the socket can accept more data. It clears the backpressure flag, attempts to send the rest of the buffered data, and then signals `ready()` back to the source stream via its `streams.Signal`. This creates a tight, efficient, native backpressure loop. - -- **The Auto-Flusher (`onAutoFlush`):** This mechanism provides a perfect balance between throughput and latency. - - **Mechanism:** When `write` is called but the `highWaterMark` is not reached, `registerAutoFlusher` queues a task that runs AFTER all JavaScript microtasks are completed. - - **Execution:** The `onAutoFlush` method is executed by the event loop at the very end of the current tick, after all JavaScript microtasks are completed. It checks `!this.hasBackpressure()` and, if the buffer is not empty, calls `sendWithoutAutoFlusher` to flush the buffered data. - - **Architectural Impact:** This allows multiple `writer.write()` calls within a single synchronous block of JS code to be batched into one syscall, but guarantees that the data is sent immediately after the current JS task completes, ensuring low, predictable latency for real-time applications. - -#### **4.3. The Native Collector: `Body.ValueBufferer`** - -When a consuming method like `.text()` is called on a body that cannot be resolved synchronously, the `Body.ValueBufferer` (`Body.rs`) is used to efficiently collect all chunks into a single native buffer. - -- **Instantiation:** A `Body.ValueBufferer` is created with a callback, `onFinishedBuffering`, which will be invoked upon completion to resolve the original JS promise. -- **Native Piping (`onStreamPipe`):** For a `ByteStream` source, the bufferer sets itself as the `pipe` destination. The `ByteStream.onData` method, instead of interacting with JavaScript, now directly calls the bufferer's `onStreamPipe` function. This function appends the received `streams.Result` slice to its internal `stream_buffer`. The entire collection loop happens natively. -- **Completion:** When a chunk with the `_and_done` flag is received, `onStreamPipe` calls the `onFinishedBuffering` callback, passing the final, fully concatenated buffer. This callback then resolves the original JavaScript promise. - -**Architectural Impact:** This pattern ensures that even when a body must be fully buffered, the collection process is highly efficient. Data chunks are concatenated in native memory without repeatedly crossing the JS boundary, minimizing overhead. - -#### **4.4. Memory and String Optimizations** - -- **`Blob` and `Blob.Store` (`Blob.rs`):** A `Blob` is a lightweight handle to a `Blob.Store`. The store can be backed by memory (`.bytes`), a file (`.file`), or an S3 object (`.s3`). This allows Bun to implement optimized operations based on the blob's backing store (e.g., `Bun.write(file1, file2)` becomes a native file copy via `copy_file.rs`). -- **`Blob.slice()` as a Zero-Copy View:** `blob.slice()` is a constant-time operation that creates a new `Blob` handle pointing to the same store but with a different `offset` and `size`, avoiding any data duplication. -- **`is_all_ascii` Flag:** `Blob`s and `ByteStream`s track whether their content is known to be pure ASCII. This allows `.text()` to skip expensive UTF-8 validation and decoding for a large class of text-based data, treating the Latin-1 bytes directly as a string. -- **`WTFStringImpl` Integration:** Bun avoids copying JS strings by default, instead storing a pointer to WebKit's internal `WTF::StringImpl` (`Body.Value.WTFStringImpl`). The conversion to a UTF-8 byte buffer is deferred until it's absolutely necessary (e.g., writing to a socket), avoiding copies for string-based operations that might never touch the network. - -## 5. The Unified System: A Complete Data Flow Example - -This diagram illustrates how the components work together when a `fetch` response is consumed. - -```mermaid -%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#2563eb','primaryTextColor':'#fff','primaryBorderColor':'#3b82f6','lineColor':'#94a3b8','secondaryColor':'#fbbf24','tertiaryColor':'#a78bfa','background':'#f8fafc','mainBkg':'#ffffff','secondBkg':'#f1f5f9'}}}%% -graph TD - subgraph flow["🚀 Response Consumption Flow"] - A["📱 JS Code"] --> B{"🎯 response.text()"} - B --> C{"❓ Is Body
Buffered?"} - C -->|"✅ Yes"|D["⚡ Optimization 1
Sync Coercion"] - C -->|"❌ No"|E{"❓ Is Stream
Native?"} - D --> F(("📄 Final String")) - - E -->|"✅ Yes"|G["🚀 Optimization 2
Direct Pipe to
Native ValueBufferer"] - E -->|"❌ No"|H["🐌 JS Fallback
read() loop"] - - G --> I{"💾 Native
Buffering"} - H --> I - - I --> J["🔤 Decode
Buffer"] - J --> F - end - - subgraph Legend - direction LR - L1("🟨 JS Layer") - L2("🟦 Native Layer") - style L1 fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#92400e - style L2 fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e40af - end - - style flow fill:#f8fafc,stroke:#64748b,stroke-width:2px - style A fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#92400e - style B fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#92400e - style H fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#991b1b - style C fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#4338ca - style E fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#4338ca - style D fill:#dbeafe,stroke:#3b82f6,stroke-width:3px,color:#1e40af - style G fill:#dbeafe,stroke:#3b82f6,stroke-width:3px,color:#1e40af - style I fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#4338ca - style J fill:#e0e7ff,stroke:#6366f1,stroke-width:2px,color:#4338ca - style F fill:#d1fae5,stroke:#10b981,stroke-width:4px,color:#065f46 - -``` - -**Diagram 5: Unified Consumption Flow** - -1. User calls `response.text()`. -2. Bun checks if the body is already fully buffered in memory. -3. **Path 1 (Fastest):** If yes, it performs the **Synchronous Coercion** optimization and returns a resolved promise. -4. **Path 2 (Fast):** If no, it checks the stream's tag. If it's a native source (`File`, `Bytes`), it uses the **Direct Path** to pipe the stream to a native `Body.ValueBufferer`. -5. **Path 3 (Slowest):** If it's a generic `JavaScript` stream, it falls back to a JS-based `read()` loop that pushes chunks to the `Body.ValueBufferer`. -6. Once the bufferer is full, the final buffer is decoded and the original promise is resolved. - -## 6. Conclusion - -Streams in Bun aggressively optimize common paths, while providing a fully WHATWG-compliant API. - -- **Key Architectural Principle:** Dispatching between generic and optimized paths based on runtime type information (tagging) is the central strategy. -- **Primary Optimizations:** The **Synchronous Coercion Fast Path** and the **Direct Native Piping Path** are the two most significant innovations, eliminating entire layers of abstraction for common use cases. -- **Supporting Optimizations:** Efficient async iteration (`readMany`), intelligent sink-side buffering (`AutoFlusher`), and careful memory management (`owned` vs. `temporary` buffers, object pooling) contribute to a system that is fast at every level. - -This deep integration between the native and JavaScript layers allows Bun to deliver performance that rivals, and in many cases exceeds, that of systems written in lower-level languages, without sacrificing the productivity and ecosystem of JavaScript. +# Web Streams in Bun + +Bun's WHATWG Streams implementation (`ReadableStream`, `WritableStream`, `TransformStream`, +their controllers/readers/writers, `TextEncoderStream`/`TextDecoderStream`, and the +`ByteLength`/`Count` queuing strategies) is written entirely in C++ under +`src/jsc/bindings/webcore/streams/` — 33 translation units, zero JavaScript builtins. + +Each TU owns one spec object or one spec algorithm group. Public classes +(`JSReadableStream.cpp`, `JSWritableStream.cpp`, …) hold the per-instance state and the +prototype/constructor tables; the abstract-operation files (`ReadableStreamOperations.cpp`, +`WritableStreamOperations.cpp`, `TransformStreamOperations.cpp`, `WebStreamsMisc.cpp`) hold +the cross-object spec algorithms. `WebStreamsInternals.h` declares every internal operation +with a `// userJS: yes/no` annotation stating whether it can re-enter user JavaScript; +`StreamsForward.h` holds the forward declarations and every kind/state enum. + +## State model + +Spec-internal slots (`[[state]]`, `[[queue]]`, `[[storedError]]`, `[[controller]]`, …) are +C++ members on the JSC cell — plain fields for POD state, `JSC::WriteBarrier<>` for anything +that references the JS heap. There are no JS private properties. Every class with barriers +implements `visitChildrenImpl` (declared in the same header as the fields); when adding a +field, add it to the visitor in the same change. + +## No per-instance algorithm closures + +The spec describes `[[pullAlgorithm]]`, `[[cancelAlgorithm]]`, etc. as closures captured at +construction. Bun does not store closures. Instead: + +- Each controller carries a **kind tag** (`SourceKind`, `SinkKind`, `TransformerKind` in + `StreamsForward.h`) plus an `m_algorithmContext` cell. Algorithm invocation is a total + `switch` over the kind — user `underlyingSource` methods, tee branches, transform halves, + cross-realm transfers, and Bun's native sources are all arms of the same switch. +- Promise reactions and deferred jobs go through **`JSStreamsRuntime`** + (`JSStreamsRuntime.{h,cpp}`), a single per-global cell reached via + `globalObject->streamsRuntime()`. It lazily materializes one shared `JSFunction` per + reaction handler; handlers are registered with + `promise->performPromiseThenWithContext(vm, global, onFulfilled, onRejected, result, contextCell)` + and receive their context cell as `argument(1)`. A second, smaller list of handlers is + bound per use-site via `JSBoundFunction` for objects we don't control. Both lists are + closed sets — see the header comment in `JSStreamsRuntime.h` before adding one. Capturing + `JSNativeStdFunction`s and per-stream `JSFunction`s are not used anywhere in the subsystem. + +## The Bun layer + +Everything Bun adds beyond the spec lives beside the spec code and is tagged, not subclassed: + +- **`BunStreamMode` + `ControllerKind` + lazy materialization** (`JSReadableStream.{h,cpp}`). + A `ReadableStream` created by native code (`Bun.file().stream()`, a fetch body, a spawned + process's stdout) starts with no controller (`ControllerKind::None`) and a mode of + `DirectPending` or `NativePending`; `materializeIfNeeded` installs the real controller on + first observable use. Streams nobody reads never allocate a controller. +- **The direct controller** (`JSDirectStreamController.{h,cpp}`, `DirectSinkKind`). Bun's + `type: "direct"` streams get a dedicated controller with an ArrayBuffer/text/array sink + instead of the spec queue. +- **The native source adapter** (`BunStreamSource.{h,cpp}`). Bridges a native (Rust) source + onto a default controller as `SourceKind::Native`, including the pull/backpressure + handshake and BYOB-style chunk-size negotiation. +- **Consumer fast paths** (`BunStreamConsumers.{h,cpp}`). `Bun.readableStreamTo{Text,Bytes, +Blob,JSON,Array,ArrayBuffer,FormData}` and the `Request`/`Response` body consumers. Fully + buffered or native-backed bodies short-circuit; only genuinely streaming JS sources pay + for a read loop. +- **The extern "C" surface** (`WebStreamsExports.cpp`). Every function the Rust runtime + calls into the streams subsystem (creating/cancelling/draining streams, attaching sinks, + querying tags) is declared here and only here. `GlobalObject::assignToStream` and the + generated `*JSSink` classes (`src/codegen/generate-jssink.ts`) enter through it. + +## Working on this code + +- Edit the `.cpp`/`.h` directly and rebuild with `bun bd`. There is no codegen step for the + stream classes themselves (only the JSSink classes are generated). For a fast per-TU + syntax check without a full build, look up the TU's compile command in + `build/debug/compile_commands.json` and re-run it with `-fsyntax-only`. +- Each TU compiles standalone (see `noUnifyDirs` in `scripts/build/unified.ts`): file-local + `static` helpers are written assuming TU isolation, so don't move them into headers + without renaming. +- Exception discipline: any call that can enter user JS needs `RETURN_IF_EXCEPTION` under a + `ThrowScope` before its result is used. The `// userJS:` annotations in + `WebStreamsInternals.h` are the source of truth for which operations can. +- GC discipline: new `WriteBarrier` fields must be visited; values held across a call that + can allocate must be rooted. Prove changes with a stress test + (`Bun.gc(true)` in a loop), not by inspection. + +## References + +- The WHATWG Streams spec (https://streams.spec.whatwg.org/) is the algorithm source of + truth; function-level comments in the implementation cite its operation names. +- Tests: `test/js/web/streams/`, `test/js/web/fetch/`, and the vendored WPT subset in + `test/js/third_party/wpt-streams/` (its `expectations.json` records the expected result + of every subtest). diff --git a/src/jsc/bindings/AsyncStackTrace.cpp b/src/jsc/bindings/AsyncStackTrace.cpp new file mode 100644 index 000000000000..6956902cbaff --- /dev/null +++ b/src/jsc/bindings/AsyncStackTrace.cpp @@ -0,0 +1,178 @@ +#include "root.h" + +#include "AsyncStackTrace.h" + +#include "BunClientData.h" +#include "ErrorStackFrame.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace JSC; + +// Walk a promise's reaction chain to find the async generators awaiting it, +// and collect them as async StackFrames. Used when an error is created from +// native code at the top of the event loop (e.g. run_from_js_thread in node_fs.rs) +// where there's no JS call stack, but the promise being rejected has an await +// chain that tells us where the user's code is. +// +// This replicates the minimal chain-walking from JSC's private +// Interpreter::getAsyncStackTrace for the common case (direct await). Promise +// combinators (all/race/any) are not traced through — we stop at them. +static void collectAsyncStackFramesFromPromise(JSC::VM& vm, JSC::JSCell* owner, JSC::JSPromise* promise, WTF::Vector& results, size_t maxStackSize) +{ + if (!JSC::Options::useAsyncStackTrace() || !promise) + return; + + JSC::AssertNoGC assertNoGC; + + auto dynamicCastValue = [](JSC::JSValue v, T** out) -> bool { + if (!v || !v.isCell()) + return false; + *out = dynamicDowncast(v.asCell()); + return *out != nullptr; + }; + + auto unwrapGeneratorFromContext = [&](JSC::JSValue context) -> JSC::JSAsyncFunctionGenerator* { + JSC::InternalFieldTuple* tuple = nullptr; + if (dynamicCastValue(context, &tuple)) + context = tuple->getInternalField(0); + JSC::JSAsyncFunctionGenerator* generator = nullptr; + dynamicCastValue(context, &generator); + return generator; + }; + + // Walk reaction->context → generator. If context is not a generator (e.g. + // thenable-chain from `return promise` without await inside an async + // function), follow reaction->promise() to the next promise in the chain. + // Cap hops to avoid pathological chains. + // + // The pending reaction can be stored two ways: + // - Inline in the JSPromise itself (the common single-await / single-then + // fast path). InternalMicrotask carries the await generator context in + // m_slot; FulfillHandler/RejectHandler carry the result promise in + // payloadCell() and the handler in m_slot. + // - As a heap-allocated JSPromiseReaction list once a second handler is + // attached, headed at payloadCell(). + auto getAwaitingGenerator = [&](JSC::JSPromise* p) -> JSC::JSAsyncFunctionGenerator* { + for (unsigned hops = 0; p && hops < 32; hops++) { + if (p->status() != JSC::JSPromise::Status::Pending) + return nullptr; + switch (p->inlineReactionKind()) { + case JSC::JSPromise::InlineReactionKind::InternalMicrotask: { + if (auto* generator = unwrapGeneratorFromContext(p->inlineReactionContext())) + return generator; + // No generator in the context. For the resolve-with-promise fast + // path (`return promise` without await inside an async function), + // the reaction's cell payload is the outer promise being resolved — + // follow it to the next promise in the chain. Combinator reactions + // store a JSPromiseCombinatorsGlobalContext there, so the downcast + // fails and we stop, as before. + if (auto* next = dynamicDowncast(p->payloadCell())) { + p = next; + continue; + } + return nullptr; + } + case JSC::JSPromise::InlineReactionKind::FulfillHandler: + case JSC::JSPromise::InlineReactionKind::RejectHandler: { + p = p->inlineHandlerResultPromise(); + continue; + } + case JSC::JSPromise::InlineReactionKind::None: + break; + } + auto* reaction = dynamicDowncast(p->payloadCell()); + if (!reaction) + return nullptr; + if (auto* generator = unwrapGeneratorFromContext(JSC::JSPromiseReaction::tryGetContext(reaction))) + return generator; + // No generator in context — follow the thenable chain to the + // promise this reaction resolves/rejects. + if (!dynamicCastValue(reaction->promise(), &p)) + return nullptr; + } + return nullptr; + }; + + auto computeBytecodeIndex = [&](JSC::CodeBlock* codeBlock, JSC::JSAsyncFunctionGenerator* generator) -> JSC::BytecodeIndex { + JSC::BytecodeIndex bytecodeIndex(0); + JSC::JSValue stateValue = generator->internalField(JSC::JSAsyncFunctionGenerator::Field::State).get(); + if (stateValue.isInt32()) { + int32_t state = stateValue.asInt32(); + size_t numberOfJumpTables = codeBlock->numberOfUnlinkedSwitchJumpTables(); + if (state > 0 && numberOfJumpTables > 0) { + size_t lastTableIndex = numberOfJumpTables - 1; + const JSC::UnlinkedSimpleJumpTable& jumpTable = codeBlock->unlinkedSwitchJumpTable(lastTableIndex); + int32_t offset = jumpTable.offsetForValue(state); + if (offset) + bytecodeIndex = JSC::BytecodeIndex(offset); + } + } + return bytecodeIndex; + }; + + auto appendFrame = [&](JSC::JSAsyncFunctionGenerator* generator) { + JSC::JSFunction* asyncFunction = nullptr; + if (!dynamicCastValue(generator->next(), &asyncFunction)) + return; + if (asyncFunction->isHostOrPrivateBuiltinFunction()) + return; + JSC::FunctionExecutable* executable = asyncFunction->jsExecutable(); + if (!executable) + return; + if (JSC::CodeBlock* codeBlock = executable->codeBlockForCall()) { + JSC::BytecodeIndex bytecodeIndex = computeBytecodeIndex(codeBlock, generator); + results.append(JSC::StackFrame(vm, owner, asyncFunction, codeBlock, bytecodeIndex, /* isAsyncFrame */ true)); + } else { + results.append(JSC::StackFrame(vm, owner, asyncFunction, /* isAsyncFrame */ true)); + } + }; + + JSC::JSAsyncFunctionGenerator* gen = getAwaitingGenerator(promise); + while (gen && results.size() < maxStackSize) { + appendFrame(gen); + JSC::JSPromise* returnPromise = nullptr; + if (!dynamicCastValue(gen->context(), &returnPromise)) + break; + gen = getAwaitingGenerator(returnPromise); + } +} + +extern "C" void Bun__attachAsyncStackFromPromise(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue errorValue, JSC::JSPromise* promise) +{ + auto& vm = JSC::getVM(globalObject); + auto* instance = dynamicDowncast(JSC::JSValue::decode(errorValue)); + if (!instance || !promise) + return; + + // Don't overwrite an existing stack trace. User-provided errors (e.g. via + // StreamError.JSValue or Body.ValueError.JSValue) may already have a + // meaningful synchronous stack from where they were created. Also skip if + // .stack was already accessed — setStackFrames after materialization + // would desync m_stackTrace from the cached property. + if (instance->hasMaterializedErrorInfo()) + return; + if (auto* existing = instance->stackTrace(); existing && !existing->isEmpty()) + return; + + size_t limit = globalObject->stackTraceLimit().value_or(10); + if (!limit) + return; + + WTF::Vector frames; + collectAsyncStackFramesFromPromise(vm, instance, promise, frames, limit); + if (frames.isEmpty()) + return; + + instance->setStackFrames(vm, WTF::move(frames)); +} diff --git a/src/jsc/bindings/AsyncStackTrace.h b/src/jsc/bindings/AsyncStackTrace.h new file mode 100644 index 000000000000..f879f4879b26 --- /dev/null +++ b/src/jsc/bindings/AsyncStackTrace.h @@ -0,0 +1,22 @@ +// Async stack recovery for errors created from native code with no JavaScript frames on +// the stack: walk the pending promise's reaction chain to the async functions awaiting it +// and use their frames as the error's stack. See AsyncStackTrace.cpp. +#pragma once + +#include "root.h" + +#include + +// Attaches an async stack (from `promise`'s await chain) to `errorValue` when it is an +// ErrorInstance with no stack of its own; no-op otherwise. Never throws. +extern "C" void Bun__attachAsyncStackFromPromise(JSC::JSGlobalObject*, JSC::EncodedJSValue errorValue, JSC::JSPromise*); + +namespace Bun { + +// C++ convenience wrapper over Bun__attachAsyncStackFromPromise. +inline void attachAsyncStackFromPromise(JSC::JSGlobalObject* globalObject, JSC::JSValue error, JSC::JSPromise* promise) +{ + Bun__attachAsyncStackFromPromise(globalObject, JSC::JSValue::encode(error), promise); +} + +} // namespace Bun diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index f9cebdd276b5..02a1127e00df 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -25,6 +25,7 @@ #include #include "headers.h" #include "BunObject.h" +#include "webcore/streams/BunStreamConsumers.h" #include "WebCoreJSBuiltins.h" #include #include "DOMJITIDLConvert.h" @@ -109,7 +110,7 @@ static JSValue constructEnvObject(VM& vm, JSObject* object) return uncheckedDowncast(object->globalObject())->processEnvObject(); } -static inline JSC::EncodedJSValue flattenArrayOfBuffersIntoArrayBufferOrUint8Array(JSGlobalObject* lexicalGlobalObject, JSValue arrayValue, size_t maxLength, bool asUint8Array) +JSC::EncodedJSValue flattenArrayOfBuffersIntoArrayBufferOrUint8Array(JSGlobalObject* lexicalGlobalObject, JSValue arrayValue, size_t maxLength, bool asUint8Array) { auto& vm = JSC::getVM(lexicalGlobalObject); @@ -986,13 +987,13 @@ JSC_DEFINE_HOST_FUNCTION(functionFileURLToPath, (JSC::JSGlobalObject * globalObj plugin constructPluginObject ReadOnly|DontDelete|PropertyCallback randomUUIDv7 Bun__randomUUIDv7 DontDelete|Function 2 randomUUIDv5 Bun__randomUUIDv5 DontDelete|Function 3 - readableStreamToArray JSBuiltin Builtin|Function 1 - readableStreamToArrayBuffer JSBuiltin Builtin|Function 1 - readableStreamToBytes JSBuiltin Builtin|Function 1 - readableStreamToBlob JSBuiltin Builtin|Function 1 - readableStreamToFormData JSBuiltin Builtin|Function 1 - readableStreamToJSON JSBuiltin Builtin|Function 1 - readableStreamToText JSBuiltin Builtin|Function 1 + readableStreamToArray WebCore::jsFunctionReadableStreamToArray DontDelete|Function 1 + readableStreamToArrayBuffer WebCore::jsFunctionReadableStreamToArrayBuffer DontDelete|Function 1 + readableStreamToBytes WebCore::jsFunctionReadableStreamToBytes DontDelete|Function 1 + readableStreamToBlob WebCore::jsFunctionReadableStreamToBlob DontDelete|Function 1 + readableStreamToFormData WebCore::jsFunctionReadableStreamToFormData DontDelete|Function 1 + readableStreamToJSON WebCore::jsFunctionReadableStreamToJSON DontDelete|Function 1 + readableStreamToText WebCore::jsFunctionReadableStreamToText DontDelete|Function 1 registerMacro BunObject_callback_registerMacro DontEnum|DontDelete|Function 1 resolve BunObject_callback_resolve DontDelete|Function 1 resolveSync BunObject_callback_resolveSync DontDelete|Function 1 @@ -1088,14 +1089,6 @@ static JSC_DEFINE_CUSTOM_SETTER(setBunObjectMain, (JSC::JSGlobalObject * globalO return BunObject_setter_main(globalObject, encodedValue); } -#define bunObjectReadableStreamToArrayCodeGenerator WebCore::readableStreamReadableStreamToArrayCodeGenerator -#define bunObjectReadableStreamToArrayBufferCodeGenerator WebCore::readableStreamReadableStreamToArrayBufferCodeGenerator -#define bunObjectReadableStreamToBytesCodeGenerator WebCore::readableStreamReadableStreamToBytesCodeGenerator -#define bunObjectReadableStreamToBlobCodeGenerator WebCore::readableStreamReadableStreamToBlobCodeGenerator -#define bunObjectReadableStreamToFormDataCodeGenerator WebCore::readableStreamReadableStreamToFormDataCodeGenerator -#define bunObjectReadableStreamToJSONCodeGenerator WebCore::readableStreamReadableStreamToJSONCodeGenerator -#define bunObjectReadableStreamToTextCodeGenerator WebCore::readableStreamReadableStreamToTextCodeGenerator - // LazyProperty wrappers for stdin/stderr/stdout static JSValue BunObject_lazyPropCb_wrap_stdin(VM& vm, JSObject* bunObject) { @@ -1117,14 +1110,6 @@ static JSValue BunObject_lazyPropCb_wrap_stdout(VM& vm, JSObject* bunObject) #include "BunObject.lut.h" -#undef bunObjectReadableStreamToArrayCodeGenerator -#undef bunObjectReadableStreamToArrayBufferCodeGenerator -#undef bunObjectReadableStreamToBytesCodeGenerator -#undef bunObjectReadableStreamToBlobCodeGenerator -#undef bunObjectReadableStreamToFormDataCodeGenerator -#undef bunObjectReadableStreamToJSONCodeGenerator -#undef bunObjectReadableStreamToTextCodeGenerator - const JSC::ClassInfo JSBunObject::s_info = { "Bun"_s, &Base::s_info, &bunObjectTable, nullptr, CREATE_METHOD_TABLE(JSBunObject) }; static JSValue constructCookieObject(VM& vm, JSObject* bunObject) diff --git a/src/jsc/bindings/BunObject.h b/src/jsc/bindings/BunObject.h index 2cde03a03ab6..725e1a59c609 100644 --- a/src/jsc/bindings/BunObject.h +++ b/src/jsc/bindings/BunObject.h @@ -1,5 +1,7 @@ #pragma once +#include "root.h" + namespace Bun { JSC_DECLARE_HOST_FUNCTION(functionBunPeek); @@ -11,10 +13,14 @@ JSC_DECLARE_HOST_FUNCTION(functionBunNanoseconds); JSC_DECLARE_HOST_FUNCTION(functionPathToFileURL); JSC_DECLARE_HOST_FUNCTION(functionFileURLToPath); -JSC::JSValue constructBunFetchObject(VM& vm, JSObject* bunObject); -JSC::JSObject* createBunObject(VM& vm, JSObject* globalObject); +JSC::JSValue constructBunFetchObject(JSC::VM& vm, JSC::JSObject* bunObject); +JSC::JSObject* createBunObject(JSC::VM& vm, JSC::JSObject* globalObject); + +// `Bun.concatArrayBuffers`: single-allocation concatenation of an array of +// ArrayBuffer/ArrayBufferView values; also used by the Web Streams consumers. +JSC::EncodedJSValue flattenArrayOfBuffersIntoArrayBufferOrUint8Array(JSC::JSGlobalObject*, JSC::JSValue arrayValue, size_t maxLength, bool asUint8Array); -JSC::JSObject* BunShell(JSGlobalObject* globalObject); -JSC::JSValue ShellError(JSGlobalObject* globalObject); +JSC::JSObject* BunShell(JSC::JSGlobalObject* globalObject); +JSC::JSValue ShellError(JSC::JSGlobalObject* globalObject); } diff --git a/src/jsc/bindings/JS2Native.cpp b/src/jsc/bindings/JS2Native.cpp index cd4a94fb3712..93880c9af271 100644 --- a/src/jsc/bindings/JS2Native.cpp +++ b/src/jsc/bindings/JS2Native.cpp @@ -10,10 +10,6 @@ #include "GeneratedJS2Native.h" #include "wtf/Assertions.h" -extern "C" JSC::EncodedJSValue ByteBlob__JSReadableStreamSource__load(JSC::JSGlobalObject* global); -extern "C" JSC::EncodedJSValue FileReader__JSReadableStreamSource__load(JSC::JSGlobalObject* global); -extern "C" JSC::EncodedJSValue ByteStream__JSReadableStreamSource__load(JSC::JSGlobalObject* global); - namespace Bun { namespace JS2Native { diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index adb5ed6ecdda..cdc5f4324651 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -89,14 +89,15 @@ #include "JSBuffer.h" #include "JSBufferList.h" #include "webcore/JSMIMEBindings.h" -#include "JSByteLengthQueuingStrategy.h" +#include "streams/JSByteLengthQueuingStrategy.h" #include "JSCloseEvent.h" #include "JSCommonJSExtensions.h" -#include "JSCountQueuingStrategy.h" +#include "streams/JSCountQueuingStrategy.h" #include "JSCustomEvent.h" #include "JSDOMConvertBase.h" #include "JSDOMConvertUnion.h" #include "JSDOMException.h" +#include "JSDOMGuardedObject.h" #include "JSDOMFile.h" #include "JSDOMFormData.h" #include "JSDOMURL.h" @@ -120,12 +121,13 @@ #include "JSPerformanceMeasure.h" #include "JSPerformanceObserver.h" #include "JSPerformanceObserverEntryList.h" -#include "JSReadableByteStreamController.h" -#include "JSReadableStream.h" -#include "JSReadableStreamBYOBReader.h" -#include "JSReadableStreamBYOBRequest.h" -#include "JSReadableStreamDefaultController.h" -#include "JSReadableStreamDefaultReader.h" +#include "streams/JSReadableByteStreamController.h" +#include "streams/JSReadableStream.h" +#include "streams/JSReadableStreamBYOBReader.h" +#include "streams/JSStreamsRuntime.h" +#include "streams/JSReadableStreamBYOBRequest.h" +#include "streams/JSReadableStreamDefaultController.h" +#include "streams/JSReadableStreamDefaultReader.h" #include "JSSink.h" #include "JSSocketAddressDTO.h" #include "JSReactElement.h" @@ -133,19 +135,19 @@ #include "JSSQLStatement.h" #include "JSStringDecoder.h" #include "JSTextEncoder.h" -#include "JSTextEncoderStream.h" -#include "JSTextDecoderStream.h" -#include "JSTransformStream.h" -#include "JSTransformStreamDefaultController.h" +#include "streams/JSTextEncoderStream.h" +#include "streams/JSTextDecoderStream.h" +#include "streams/JSTransformStream.h" +#include "streams/JSTransformStreamDefaultController.h" #include "JSURLPattern.h" #include "JSURLSearchParams.h" #include "JSWasmStreamingCompiler.h" #include #include "JSWebSocket.h" #include "JSWorker.h" -#include "JSWritableStream.h" -#include "JSWritableStreamDefaultController.h" -#include "JSWritableStreamDefaultWriter.h" +#include "streams/JSWritableStream.h" +#include "streams/JSWritableStreamDefaultController.h" +#include "streams/JSWritableStreamDefaultWriter.h" #include "libusockets.h" #include "ModuleLoader.h" #include "napi_external.h" @@ -158,7 +160,8 @@ #include "Performance.h" #include "ProcessBindingConstants.h" #include "ProcessBindingTTYWrap.h" -#include "ReadableStream.h" +#include "streams/BunStreamConsumers.h" +#include "streams/WebStreamsInternals.h" #include "SerializedScriptValue.h" #include "StructuredClone.h" #include "WebCoreJSBuiltins.h" @@ -1153,15 +1156,6 @@ WebCore::EventTarget& GlobalObject::eventTarget() return globalEventScope; } -JSC_DEFINE_CUSTOM_GETTER(functionLazyLoadStreamPrototypeMap_getter, - (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, - JSC::PropertyName)) -{ - Zig::GlobalObject* thisObject = uncheckedDowncast(lexicalGlobalObject); - return JSC::JSValue::encode( - thisObject->readableStreamNativeMap()); -} - JSC_DEFINE_CUSTOM_GETTER(JSBuffer_getter, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) @@ -1662,8 +1656,6 @@ JSC_DEFINE_HOST_FUNCTION(functionNavigatorGetHardwareConcurrency, (JSC::JSGlobal JSC_DECLARE_HOST_FUNCTION(makeGetterTypeErrorForBuiltins); JSC_DECLARE_HOST_FUNCTION(makeDOMExceptionForBuiltins); -JSC_DECLARE_HOST_FUNCTION(createWritableStreamFromInternal); -JSC_DECLARE_HOST_FUNCTION(getInternalWritableStream); JSC_DECLARE_HOST_FUNCTION(isAbortSignal); JSC_DECLARE_HOST_FUNCTION(jsBunPeekPromiseStatus); JSC_DECLARE_HOST_FUNCTION(jsBunPeekPromiseSettledValue); @@ -1712,28 +1704,6 @@ JSC_DEFINE_HOST_FUNCTION(makeDOMExceptionForBuiltins, (JSGlobalObject * globalOb return JSValue::encode(value); } -JSC_DEFINE_HOST_FUNCTION(getInternalWritableStream, (JSGlobalObject*, CallFrame* callFrame)) -{ - ASSERT(callFrame); - ASSERT(callFrame->argumentCount() == 1); - - auto* writableStream = dynamicDowncast(callFrame->uncheckedArgument(0)); - if (!writableStream) [[unlikely]] - return JSValue::encode(jsUndefined()); - return JSValue::encode(writableStream->wrapped().internalWritableStream()); -} - -JSC_DEFINE_HOST_FUNCTION(createWritableStreamFromInternal, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - ASSERT(callFrame); - ASSERT(callFrame->argumentCount() == 1); - ASSERT(callFrame->uncheckedArgument(0).isObject()); - - auto* jsDOMGlobalObject = uncheckedDowncast(globalObject); - auto internalWritableStream = InternalWritableStream::fromObject(*jsDOMGlobalObject, *callFrame->uncheckedArgument(0).toObject(globalObject)); - return JSValue::encode(toJSNewlyCreated(globalObject, jsDOMGlobalObject, WritableStream::create(WTF::move(internalWritableStream)))); -} - JSC_DEFINE_HOST_FUNCTION(addAbortAlgorithmToSignal, (JSGlobalObject * globalObject, CallFrame* callFrame)) { ASSERT(callFrame); @@ -2431,10 +2401,9 @@ void GlobalObject::finishCreation(VM& vm) init.set(process); }); - m_lazyReadableStreamPrototypeMap.initLater( - [](const JSC::LazyProperty::Initializer& init) { - auto* map = JSC::JSMap::create(init.vm, init.owner->mapStructure()); - init.set(map); + m_streamsRuntime.initLater( + [](const JSC::LazyProperty::Initializer& init) { + init.set(WebCore::JSStreamsRuntime::create(init.vm, static_cast(init.owner))); }); m_requireMap.initLater( @@ -2833,56 +2802,21 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionCheckBufferRead, (JSC::JSGlobalObject * globa } return JSValue::encode(jsUndefined()); } -extern "C" EncodedJSValue Bun__assignStreamIntoResumableSink(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue stream, JSC::EncodedJSValue sink) -{ - Zig::GlobalObject* globalThis = static_cast(globalObject); - return globalThis->assignStreamToResumableSink(JSValue::decode(stream), JSValue::decode(sink)); -} -EncodedJSValue GlobalObject::assignStreamToResumableSink(JSValue stream, JSValue sink) -{ - auto& vm = this->vm(); - JSC::JSFunction* function = this->m_assignStreamToResumableSink.get(); - if (!function) { - function = JSFunction::create(vm, this, static_cast(readableStreamInternalsAssignStreamIntoResumableSinkCodeGenerator(vm)), this); - this->m_assignStreamToResumableSink.set(vm, this, function); - } - - auto callData = JSC::getCallData(function); - JSC::MarkedArgumentBuffer arguments; - arguments.append(stream); - arguments.append(sink); - - WTF::NakedPtr returnedException = nullptr; - - auto result = JSC::profiledCall(this, ProfilingReason::API, function, callData, JSC::jsUndefined(), arguments, returnedException); - if (auto* exception = returnedException.get()) { - return JSC::JSValue::encode(exception); - } - - return JSC::JSValue::encode(result); -} - EncodedJSValue GlobalObject::assignToStream(JSValue stream, JSValue controller) { auto& vm = this->vm(); - JSC::JSFunction* function = this->m_assignToStream.get(); - if (!function) { - function = JSFunction::create(vm, this, static_cast(readableStreamInternalsAssignToStreamCodeGenerator(vm)), this); - this->m_assignToStream.set(vm, this, function); - } - - auto callData = JSC::getCallData(function); - JSC::MarkedArgumentBuffer arguments; - arguments.append(stream); - arguments.append(controller); - - WTF::NakedPtr returnedException = nullptr; - - auto result = JSC::profiledCall(this, ProfilingReason::API, function, callData, JSC::jsUndefined(), arguments, returnedException); - if (auto* exception = returnedException.get()) { + auto* readableStream = dynamicDowncast(stream); + if (!readableStream) [[unlikely]] + return JSC::JSValue::encode(JSC::Exception::create(vm, createTypeError(this, "Expected a ReadableStream"_s))); + // The generated `${Sink}__assignToStream` caller expects any failure returned as the + // encoded Exception cell, never left pending on the VM. + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + JSValue result = Bun::WebStreams::assignToStream(this, readableStream, controller); + if (auto* exception = scope.exception()) [[unlikely]] { + // Hand the Exception cell back to the native caller; a termination stays pending by design. + scope.clearExceptionExceptTermination(); return JSC::JSValue::encode(exception); } - return JSC::JSValue::encode(result); } @@ -2937,11 +2871,6 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) // ----- Private/Static Properties ----- GlobalPropertyInfo staticGlobals[] = { - GlobalPropertyInfo { builtinNames.startDirectStreamPrivateName(), - JSC::JSFunction::create(vm, this, 1, - String(), functionStartDirectStream, ImplementationVisibility::Public), - PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | 0 }, - GlobalPropertyInfo { builtinNames.lazyPrivateName(), JSC::JSFunction::create(vm, this, 0, "@lazy"_s, JS2Native::jsDollarLazy, ImplementationVisibility::Public), PropertyAttribute::ReadOnly | PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | 0 }, @@ -2956,8 +2885,6 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) GlobalPropertyInfo(builtinNames.peekPromiseStatusPrivateName(), JSFunction::create(vm, this, 1, String(), jsBunPeekPromiseStatus, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.peekPromiseSettledValuePrivateName(), JSFunction::create(vm, this, 1, String(), jsBunPeekPromiseSettledValue, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.pokePromiseAsHandledPrivateName(), JSFunction::create(vm, this, 1, String(), jsBunPokePromiseAsHandled, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), - GlobalPropertyInfo(builtinNames.getInternalWritableStreamPrivateName(), JSFunction::create(vm, this, 1, String(), getInternalWritableStream, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), - GlobalPropertyInfo(builtinNames.createWritableStreamFromInternalPrivateName(), JSFunction::create(vm, this, 1, String(), createWritableStreamFromInternal, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.fulfillModuleSyncPrivateName(), JSFunction::create(vm, this, 1, String(), functionFulfillModuleSync, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.esmNamespaceForCjsPrivateName(), JSFunction::create(vm, this, 1, String(), functionEsmNamespaceForCjs, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), GlobalPropertyInfo(builtinNames.esmRegistryDeletePrivateName(), JSFunction::create(vm, this, 1, String(), functionEsmRegistryDelete, ImplementationVisibility::Public), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly), @@ -2979,11 +2906,7 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) // TODO: most/all of these private properties can be made as static globals. // i've noticed doing it as is will work somewhat but getDirect() wont be able to find them - putDirectBuiltinFunction(vm, this, builtinNames.createFIFOPrivateName(), streamInternalsCreateFIFOCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.createEmptyReadableStreamPrivateName(), readableStreamCreateEmptyReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.createUsedReadableStreamPrivateName(), readableStreamCreateUsedReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.createErroredReadableStreamPrivateName(), readableStreamCreateErroredReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.createNativeReadableStreamPrivateName(), readableStreamCreateNativeReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + putDirectBuiltinFunction(vm, this, builtinNames.createFIFOPrivateName(), fifoCreateFIFOCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); // These three are CommonJS-only and never reached on an ESM startup path; install // lazy getters so their source isn't parsed during global object construction. // (See getRequireESMBuiltin / getLoadEsmIntoCjsBuiltin / getInternalRequireBuiltin above.) @@ -3020,7 +2943,6 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) PropertyAttribute::ReadOnly | PropertyAttribute::DontDelete | 0); putDirectCustomAccessor(vm, static_cast(vm.clientData)->builtinNames().BufferPrivateName(), JSC::CustomGetterSetter::create(vm, JSBuffer_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomValue); - putDirectCustomAccessor(vm, builtinNames.lazyStreamPrototypeMapPrivateName(), JSC::CustomGetterSetter::create(vm, functionLazyLoadStreamPrototypeMap_getter, nullptr), PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly | PropertyAttribute::CustomValue); putDirectCustomAccessor(vm, builtinNames.TransformStreamPrivateName(), CustomGetterSetter::create(vm, TransformStream_getter, nullptr), attributesForStructure(static_cast(PropertyAttribute::DontEnum)) | PropertyAttribute::CustomValue); putDirectCustomAccessor(vm, builtinNames.TransformStreamDefaultControllerPrivateName(), CustomGetterSetter::create(vm, TransformStreamDefaultController_getter, nullptr), attributesForStructure(static_cast(PropertyAttribute::DontEnum)) | PropertyAttribute::CustomValue); putDirectCustomAccessor(vm, builtinNames.ReadableByteStreamControllerPrivateName(), CustomGetterSetter::create(vm, ReadableByteStreamController_getter, nullptr), attributesForStructure(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly) | PropertyAttribute::CustomValue); diff --git a/src/jsc/bindings/ZigGlobalObject.h b/src/jsc/bindings/ZigGlobalObject.h index 9bba7344771e..fb4271ed1d0f 100644 --- a/src/jsc/bindings/ZigGlobalObject.h +++ b/src/jsc/bindings/ZigGlobalObject.h @@ -29,6 +29,7 @@ class SubtleCrypto; class EventTarget; class Performance; class JSBuiltinInternalFunctions; +class JSStreamsRuntime; } // namespace WebCore namespace Bun { @@ -272,7 +273,7 @@ class GlobalObject : public Bun::GlobalScope { JSC::JSObject* NodeVMSyntheticModule() const { return m_NodeVMSyntheticModuleClassStructure.constructorInitializedOnMainThread(this); } JSC::JSValue NodeVMSyntheticModulePrototype() const { return m_NodeVMSyntheticModuleClassStructure.prototypeInitializedOnMainThread(this); } - JSC::JSMap* readableStreamNativeMap() const { return m_lazyReadableStreamPrototypeMap.getInitializedOnMainThread(this); } + WebCore::JSStreamsRuntime* streamsRuntime() const { return m_streamsRuntime.getInitializedOnMainThread(this); } JSC::JSMap* requireMap() const { return m_requireMap.getInitializedOnMainThread(this); } // The JSC module loader registry is no longer a JS Map. Use // moduleLoader()->registryEntry(key) / moduleMap() / removeEntry(key) / @@ -361,7 +362,6 @@ class GlobalObject : public Bun::GlobalScope { JSObject* subtleCrypto() { return m_subtleCryptoObject.getInitializedOnMainThread(this); } JSC::EncodedJSValue assignToStream(JSValue stream, JSValue controller); - JSC::EncodedJSValue assignStreamToResumableSink(JSValue stream, JSValue sink); WebCore::EventTarget& eventTarget(); WebCore::ScriptExecutionContext* m_scriptExecutionContext; @@ -483,14 +483,6 @@ class GlobalObject : public Bun::GlobalScope { V(public, Bun::BakeAdditionsToGlobalObject, m_bakeAdditions) \ \ /* TODO: these should use LazyProperty */ \ - V(private, WriteBarrier, m_assignToStream) \ - V(private, WriteBarrier, m_assignStreamToResumableSink) \ - V(public, WriteBarrier, m_readableStreamToArrayBuffer) \ - V(public, WriteBarrier, m_readableStreamToBytes) \ - V(public, WriteBarrier, m_readableStreamToBlob) \ - V(public, WriteBarrier, m_readableStreamToJSON) \ - V(public, WriteBarrier, m_readableStreamToText) \ - V(public, WriteBarrier, m_readableStreamToFormData) \ \ V(public, LazyPropertyOfGlobalObject, m_moduleResolveFilenameFunction) \ V(public, LazyPropertyOfGlobalObject, m_moduleRunMainFunction) \ @@ -601,7 +593,7 @@ class GlobalObject : public Bun::GlobalScope { V(private, LazyPropertyOfGlobalObject, m_utilInspectStylizeColorFunction) \ V(private, LazyPropertyOfGlobalObject, m_utilInspectStylizeNoColorFunction) \ V(private, LazyPropertyOfGlobalObject, m_wasmStreamingConsumeStreamFunction) \ - V(private, LazyPropertyOfGlobalObject, m_lazyReadableStreamPrototypeMap) \ + V(private, LazyPropertyOfGlobalObject, m_streamsRuntime) \ V(private, LazyPropertyOfGlobalObject, m_requireMap) \ V(private, LazyPropertyOfGlobalObject, m_JSArrayBufferControllerPrototype) \ V(private, LazyPropertyOfGlobalObject, m_JSHTTPSResponseControllerPrototype) \ diff --git a/src/jsc/bindings/ZigGlobalObject.lut.txt b/src/jsc/bindings/ZigGlobalObject.lut.txt index 326013b4e89b..5d6354de3ec8 100644 --- a/src/jsc/bindings/ZigGlobalObject.lut.txt +++ b/src/jsc/bindings/ZigGlobalObject.lut.txt @@ -1,95 +1,95 @@ -// In a separate file because processing ZigGlobalObject.cpp takes 15+ seconds - -/* Source for ZigGlobalObject.lut.h -@begin bunGlobalObjectTable - addEventListener jsFunctionAddEventListener Function 2 - alert WebCore__alert Function 1 - atob functionATOB Function 1 - btoa functionBTOA Function 1 - clearImmediate functionClearImmediate Function 1 - clearInterval functionClearInterval Function 1 - clearTimeout functionClearTimeout Function 1 - confirm WebCore__confirm Function 1 - dispatchEvent jsFunctionDispatchEvent Function 1 - fetch constructBunFetchObject PropertyCallback - postMessage jsFunctionPostMessage Function 1 - prompt WebCore__prompt Function 1 - queueMicrotask functionQueueMicrotask Function 1 - removeEventListener jsFunctionRemoveEventListener Function 2 - reportError functionReportError Function 1 - setImmediate functionSetImmediate Function 1 - setInterval functionSetInterval Function 1 - setTimeout functionSetTimeout Function 1 - structuredClone WebCore::jsFunctionStructuredClone Function 2 - - global GlobalObject_getGlobalThis PropertyCallback - - Bun GlobalObject::m_bunObject CellProperty|DontDelete|ReadOnly - File GlobalObject::m_JSDOMFileConstructor CellProperty - crypto GlobalObject::m_cryptoObject CellProperty - navigator GlobalObject::m_navigatorObject CellProperty - performance GlobalObject::m_performanceObject CellProperty - process GlobalObject::m_processObject CellProperty - - Blob GlobalObject::m_JSBlob ClassStructure - Buffer GlobalObject::m_JSBufferClassStructure ClassStructure - BuildError GlobalObject::m_JSBuildMessage ClassStructure - BuildMessage GlobalObject::m_JSBuildMessage ClassStructure - Crypto GlobalObject::m_JSCrypto ClassStructure - HTMLRewriter GlobalObject::m_JSHTMLRewriter ClassStructure - Request GlobalObject::m_JSRequest ClassStructure - ResolveError GlobalObject::m_JSResolveMessage ClassStructure - ResolveMessage GlobalObject::m_JSResolveMessage ClassStructure - Response GlobalObject::m_JSResponse ClassStructure - TextDecoder GlobalObject::m_JSTextDecoder ClassStructure - - AbortController AbortControllerConstructorCallback PropertyCallback - AbortSignal AbortSignalConstructorCallback PropertyCallback - BroadcastChannel BroadcastChannelConstructorCallback PropertyCallback - ByteLengthQueuingStrategy ByteLengthQueuingStrategyConstructorCallback PropertyCallback - CloseEvent CloseEventConstructorCallback PropertyCallback - CompressionStream CompressionStreamConstructorCallback PropertyCallback - CountQueuingStrategy CountQueuingStrategyConstructorCallback PropertyCallback - CryptoKey CryptoKeyConstructorCallback PropertyCallback - CustomEvent CustomEventConstructorCallback PropertyCallback - DecompressionStream DecompressionStreamConstructorCallback PropertyCallback - DOMException DOMExceptionConstructorCallback PropertyCallback - ErrorEvent ErrorEventConstructorCallback PropertyCallback - Event EventConstructorCallback PropertyCallback - EventTarget EventTargetConstructorCallback PropertyCallback - FormData DOMFormDataConstructorCallback PropertyCallback - Headers FetchHeadersConstructorCallback PropertyCallback - MessageChannel MessageChannelConstructorCallback PropertyCallback - MessageEvent MessageEventConstructorCallback PropertyCallback - MessagePort MessagePortConstructorCallback PropertyCallback - Performance PerformanceConstructorCallback PropertyCallback - PerformanceEntry PerformanceEntryConstructorCallback PropertyCallback - PerformanceMark PerformanceMarkConstructorCallback PropertyCallback - PerformanceMeasure PerformanceMeasureConstructorCallback PropertyCallback - PerformanceObserver PerformanceObserverConstructorCallback PropertyCallback - PerformanceObserverEntryList PerformanceObserverEntryListConstructorCallback PropertyCallback - PerformanceResourceTiming PerformanceResourceTimingConstructorCallback PropertyCallback - PerformanceServerTiming PerformanceServerTimingConstructorCallback PropertyCallback - PerformanceTiming PerformanceTimingConstructorCallback PropertyCallback - ReadableByteStreamController ReadableByteStreamControllerConstructorCallback PropertyCallback - ReadableStream ReadableStreamConstructorCallback PropertyCallback - ReadableStreamBYOBReader ReadableStreamBYOBReaderConstructorCallback PropertyCallback - ReadableStreamBYOBRequest ReadableStreamBYOBRequestConstructorCallback PropertyCallback - ReadableStreamDefaultController ReadableStreamDefaultControllerConstructorCallback PropertyCallback - ReadableStreamDefaultReader ReadableStreamDefaultReaderConstructorCallback PropertyCallback - SubtleCrypto SubtleCryptoConstructorCallback PropertyCallback - TextDecoderStream TextDecoderStreamConstructorCallback PropertyCallback - TextEncoder TextEncoderConstructorCallback PropertyCallback - TextEncoderStream TextEncoderStreamConstructorCallback PropertyCallback - TransformStream TransformStreamConstructorCallback PropertyCallback - TransformStreamDefaultController TransformStreamDefaultControllerConstructorCallback PropertyCallback - URL DOMURLConstructorCallback DontEnum|PropertyCallback - URLPattern URLPatternConstructorCallback PropertyCallback - URLSearchParams URLSearchParamsConstructorCallback DontEnum|PropertyCallback - WebSocket WebSocketConstructorCallback PropertyCallback - Worker WorkerConstructorCallback PropertyCallback - WritableStream WritableStreamConstructorCallback PropertyCallback - WritableStreamDefaultController WritableStreamDefaultControllerConstructorCallback PropertyCallback - WritableStreamDefaultWriter WritableStreamDefaultWriterConstructorCallback PropertyCallback -@end -*/ +// In a separate file because processing ZigGlobalObject.cpp takes 15+ seconds + +/* Source for ZigGlobalObject.lut.h +@begin bunGlobalObjectTable + addEventListener jsFunctionAddEventListener Function 2 + alert WebCore__alert Function 1 + atob functionATOB Function 1 + btoa functionBTOA Function 1 + clearImmediate functionClearImmediate Function 1 + clearInterval functionClearInterval Function 1 + clearTimeout functionClearTimeout Function 1 + confirm WebCore__confirm Function 1 + dispatchEvent jsFunctionDispatchEvent Function 1 + fetch constructBunFetchObject PropertyCallback + postMessage jsFunctionPostMessage Function 1 + prompt WebCore__prompt Function 1 + queueMicrotask functionQueueMicrotask Function 1 + removeEventListener jsFunctionRemoveEventListener Function 2 + reportError functionReportError Function 1 + setImmediate functionSetImmediate Function 1 + setInterval functionSetInterval Function 1 + setTimeout functionSetTimeout Function 1 + structuredClone WebCore::jsFunctionStructuredClone Function 2 + + global GlobalObject_getGlobalThis PropertyCallback + + Bun GlobalObject::m_bunObject CellProperty|DontDelete|ReadOnly + File GlobalObject::m_JSDOMFileConstructor CellProperty + crypto GlobalObject::m_cryptoObject CellProperty + navigator GlobalObject::m_navigatorObject CellProperty + performance GlobalObject::m_performanceObject CellProperty + process GlobalObject::m_processObject CellProperty + + Blob GlobalObject::m_JSBlob ClassStructure + Buffer GlobalObject::m_JSBufferClassStructure ClassStructure + BuildError GlobalObject::m_JSBuildMessage ClassStructure + BuildMessage GlobalObject::m_JSBuildMessage ClassStructure + Crypto GlobalObject::m_JSCrypto ClassStructure + HTMLRewriter GlobalObject::m_JSHTMLRewriter ClassStructure + Request GlobalObject::m_JSRequest ClassStructure + ResolveError GlobalObject::m_JSResolveMessage ClassStructure + ResolveMessage GlobalObject::m_JSResolveMessage ClassStructure + Response GlobalObject::m_JSResponse ClassStructure + TextDecoder GlobalObject::m_JSTextDecoder ClassStructure + + AbortController AbortControllerConstructorCallback PropertyCallback + AbortSignal AbortSignalConstructorCallback PropertyCallback + BroadcastChannel BroadcastChannelConstructorCallback PropertyCallback + ByteLengthQueuingStrategy ByteLengthQueuingStrategyConstructorCallback DontEnum|PropertyCallback + CloseEvent CloseEventConstructorCallback PropertyCallback + CompressionStream CompressionStreamConstructorCallback PropertyCallback + CountQueuingStrategy CountQueuingStrategyConstructorCallback DontEnum|PropertyCallback + CryptoKey CryptoKeyConstructorCallback PropertyCallback + CustomEvent CustomEventConstructorCallback PropertyCallback + DecompressionStream DecompressionStreamConstructorCallback PropertyCallback + DOMException DOMExceptionConstructorCallback PropertyCallback + ErrorEvent ErrorEventConstructorCallback PropertyCallback + Event EventConstructorCallback PropertyCallback + EventTarget EventTargetConstructorCallback PropertyCallback + FormData DOMFormDataConstructorCallback PropertyCallback + Headers FetchHeadersConstructorCallback PropertyCallback + MessageChannel MessageChannelConstructorCallback PropertyCallback + MessageEvent MessageEventConstructorCallback PropertyCallback + MessagePort MessagePortConstructorCallback PropertyCallback + Performance PerformanceConstructorCallback PropertyCallback + PerformanceEntry PerformanceEntryConstructorCallback PropertyCallback + PerformanceMark PerformanceMarkConstructorCallback PropertyCallback + PerformanceMeasure PerformanceMeasureConstructorCallback PropertyCallback + PerformanceObserver PerformanceObserverConstructorCallback PropertyCallback + PerformanceObserverEntryList PerformanceObserverEntryListConstructorCallback PropertyCallback + PerformanceResourceTiming PerformanceResourceTimingConstructorCallback PropertyCallback + PerformanceServerTiming PerformanceServerTimingConstructorCallback PropertyCallback + PerformanceTiming PerformanceTimingConstructorCallback PropertyCallback + ReadableByteStreamController ReadableByteStreamControllerConstructorCallback DontEnum|PropertyCallback + ReadableStream ReadableStreamConstructorCallback DontEnum|PropertyCallback + ReadableStreamBYOBReader ReadableStreamBYOBReaderConstructorCallback DontEnum|PropertyCallback + ReadableStreamBYOBRequest ReadableStreamBYOBRequestConstructorCallback DontEnum|PropertyCallback + ReadableStreamDefaultController ReadableStreamDefaultControllerConstructorCallback DontEnum|PropertyCallback + ReadableStreamDefaultReader ReadableStreamDefaultReaderConstructorCallback DontEnum|PropertyCallback + SubtleCrypto SubtleCryptoConstructorCallback PropertyCallback + TextDecoderStream TextDecoderStreamConstructorCallback PropertyCallback + TextEncoder TextEncoderConstructorCallback PropertyCallback + TextEncoderStream TextEncoderStreamConstructorCallback PropertyCallback + TransformStream TransformStreamConstructorCallback DontEnum|PropertyCallback + TransformStreamDefaultController TransformStreamDefaultControllerConstructorCallback DontEnum|PropertyCallback + URL DOMURLConstructorCallback DontEnum|PropertyCallback + URLPattern URLPatternConstructorCallback PropertyCallback + URLSearchParams URLSearchParamsConstructorCallback DontEnum|PropertyCallback + WebSocket WebSocketConstructorCallback PropertyCallback + Worker WorkerConstructorCallback PropertyCallback + WritableStream WritableStreamConstructorCallback DontEnum|PropertyCallback + WritableStreamDefaultController WritableStreamDefaultControllerConstructorCallback DontEnum|PropertyCallback + WritableStreamDefaultWriter WritableStreamDefaultWriterConstructorCallback DontEnum|PropertyCallback +@end +*/ diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 889a499220ac..c48163182c6d 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -153,6 +153,7 @@ #include "JavaScriptCore/CustomGetterSetter.h" #include "ErrorStackFrame.h" +#include "AsyncStackTrace.h" #include "ErrorStackTrace.h" #include "ObjectBindings.h" @@ -2246,164 +2247,6 @@ JSC::EncodedJSValue JSGlobalObject__createOutOfMemoryError(JSC::JSGlobalObject* return JSValue::encode(exception); } -// Walk a promise's reaction chain to find the async generators awaiting it, -// and collect them as async StackFrames. Used when an error is created from -// native code at the top of the event loop (e.g. run_from_js_thread in node_fs.rs) -// where there's no JS call stack, but the promise being rejected has an await -// chain that tells us where the user's code is. -// -// This replicates the minimal chain-walking from JSC's private -// Interpreter::getAsyncStackTrace for the common case (direct await). Promise -// combinators (all/race/any) are not traced through — we stop at them. -static void collectAsyncStackFramesFromPromise(JSC::VM& vm, JSC::JSCell* owner, JSC::JSPromise* promise, WTF::Vector& results, size_t maxStackSize) -{ - if (!JSC::Options::useAsyncStackTrace() || !promise) - return; - - JSC::AssertNoGC assertNoGC; - - auto dynamicCastValue = [](JSC::JSValue v, T** out) -> bool { - if (!v || !v.isCell()) - return false; - *out = dynamicDowncast(v.asCell()); - return *out != nullptr; - }; - - auto unwrapGeneratorFromContext = [&](JSC::JSValue context) -> JSC::JSAsyncFunctionGenerator* { - JSC::InternalFieldTuple* tuple = nullptr; - if (dynamicCastValue(context, &tuple)) - context = tuple->getInternalField(0); - JSC::JSAsyncFunctionGenerator* generator = nullptr; - dynamicCastValue(context, &generator); - return generator; - }; - - // Walk reaction->context → generator. If context is not a generator (e.g. - // thenable-chain from `return promise` without await inside an async - // function), follow reaction->promise() to the next promise in the chain. - // Cap hops to avoid pathological chains. - // - // The pending reaction can be stored two ways: - // - Inline in the JSPromise itself (the common single-await / single-then - // fast path). InternalMicrotask carries the await generator context in - // m_slot; FulfillHandler/RejectHandler carry the result promise in - // payloadCell() and the handler in m_slot. - // - As a heap-allocated JSPromiseReaction list once a second handler is - // attached, headed at payloadCell(). - auto getAwaitingGenerator = [&](JSC::JSPromise* p) -> JSC::JSAsyncFunctionGenerator* { - for (unsigned hops = 0; p && hops < 32; hops++) { - if (p->status() != JSC::JSPromise::Status::Pending) - return nullptr; - switch (p->inlineReactionKind()) { - case JSC::JSPromise::InlineReactionKind::InternalMicrotask: { - if (auto* generator = unwrapGeneratorFromContext(p->inlineReactionContext())) - return generator; - // No generator in the context. For the resolve-with-promise fast - // path (`return promise` without await inside an async function), - // the reaction's cell payload is the outer promise being resolved — - // follow it to the next promise in the chain. Combinator reactions - // store a JSPromiseCombinatorsGlobalContext there, so the downcast - // fails and we stop, as before. - if (auto* next = dynamicDowncast(p->payloadCell())) { - p = next; - continue; - } - return nullptr; - } - case JSC::JSPromise::InlineReactionKind::FulfillHandler: - case JSC::JSPromise::InlineReactionKind::RejectHandler: { - p = p->inlineHandlerResultPromise(); - continue; - } - case JSC::JSPromise::InlineReactionKind::None: - break; - } - auto* reaction = dynamicDowncast(p->payloadCell()); - if (!reaction) - return nullptr; - if (auto* generator = unwrapGeneratorFromContext(JSC::JSPromiseReaction::tryGetContext(reaction))) - return generator; - // No generator in context — follow the thenable chain to the - // promise this reaction resolves/rejects. - if (!dynamicCastValue(reaction->promise(), &p)) - return nullptr; - } - return nullptr; - }; - - auto computeBytecodeIndex = [&](JSC::CodeBlock* codeBlock, JSC::JSAsyncFunctionGenerator* generator) -> JSC::BytecodeIndex { - JSC::BytecodeIndex bytecodeIndex(0); - JSC::JSValue stateValue = generator->internalField(JSC::JSAsyncFunctionGenerator::Field::State).get(); - if (stateValue.isInt32()) { - int32_t state = stateValue.asInt32(); - size_t numberOfJumpTables = codeBlock->numberOfUnlinkedSwitchJumpTables(); - if (state > 0 && numberOfJumpTables > 0) { - size_t lastTableIndex = numberOfJumpTables - 1; - const JSC::UnlinkedSimpleJumpTable& jumpTable = codeBlock->unlinkedSwitchJumpTable(lastTableIndex); - int32_t offset = jumpTable.offsetForValue(state); - if (offset) - bytecodeIndex = JSC::BytecodeIndex(offset); - } - } - return bytecodeIndex; - }; - - auto appendFrame = [&](JSC::JSAsyncFunctionGenerator* generator) { - JSC::JSFunction* asyncFunction = nullptr; - if (!dynamicCastValue(generator->next(), &asyncFunction)) - return; - if (asyncFunction->isHostOrPrivateBuiltinFunction()) - return; - JSC::FunctionExecutable* executable = asyncFunction->jsExecutable(); - if (!executable) - return; - if (JSC::CodeBlock* codeBlock = executable->codeBlockForCall()) { - JSC::BytecodeIndex bytecodeIndex = computeBytecodeIndex(codeBlock, generator); - results.append(JSC::StackFrame(vm, owner, asyncFunction, codeBlock, bytecodeIndex, /* isAsyncFrame */ true)); - } else { - results.append(JSC::StackFrame(vm, owner, asyncFunction, /* isAsyncFrame */ true)); - } - }; - - JSC::JSAsyncFunctionGenerator* gen = getAwaitingGenerator(promise); - while (gen && results.size() < maxStackSize) { - appendFrame(gen); - JSC::JSPromise* returnPromise = nullptr; - if (!dynamicCastValue(gen->context(), &returnPromise)) - break; - gen = getAwaitingGenerator(returnPromise); - } -} - -extern "C" void Bun__attachAsyncStackFromPromise(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue errorValue, JSC::JSPromise* promise) -{ - auto& vm = JSC::getVM(globalObject); - auto* instance = dynamicDowncast(JSC::JSValue::decode(errorValue)); - if (!instance || !promise) - return; - - // Don't overwrite an existing stack trace. User-provided errors (e.g. via - // StreamError.JSValue or Body.ValueError.JSValue) may already have a - // meaningful synchronous stack from where they were created. Also skip if - // .stack was already accessed — setStackFrames after materialization - // would desync m_stackTrace from the cached property. - if (instance->hasMaterializedErrorInfo()) - return; - if (auto* existing = instance->stackTrace(); existing && !existing->isEmpty()) - return; - - size_t limit = globalObject->stackTraceLimit().value_or(10); - if (!limit) - return; - - WTF::Vector frames; - collectAsyncStackFramesFromPromise(vm, instance, promise, frames, limit); - if (frames.isEmpty()) - return; - - instance->setStackFrames(vm, WTF::move(frames)); -} - JSC::EncodedJSValue SystemError__toErrorInstance(const SystemError* arg0, JSC::JSGlobalObject* globalObject) { SystemError err = *arg0; @@ -3170,42 +3013,6 @@ JSC::EncodedJSValue JSC__JSModuleLoader__evaluate(JSC::JSGlobalObject* globalObj } } -[[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue ReadableStream__empty(Zig::GlobalObject* globalObject) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - auto clientData = WebCore::clientData(vm); - auto* function = globalObject->getDirect(vm, clientData->builtinNames().createEmptyReadableStreamPrivateName()).getObject(); - JSValue emptyStream = JSC::call(globalObject, function, JSC::ArgList(), "ReadableStream.create"_s); - RETURN_IF_EXCEPTION(scope, {}); - return JSValue::encode(emptyStream); -} - -[[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue ReadableStream__used(Zig::GlobalObject* globalObject) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - auto clientData = WebCore::clientData(vm); - auto* function = globalObject->getDirect(vm, clientData->builtinNames().createUsedReadableStreamPrivateName()).getObject(); - JSValue usedStream = JSC::call(globalObject, function, JSC::ArgList(), "ReadableStream.create"_s); - RETURN_IF_EXCEPTION(scope, {}); - return JSValue::encode(usedStream); -} - -[[ZIG_EXPORT(zero_is_throw)]] JSC::EncodedJSValue ReadableStream__errored(Zig::GlobalObject* globalObject, JSC::EncodedJSValue encodedReason) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - auto clientData = WebCore::clientData(vm); - auto* function = globalObject->getDirect(vm, clientData->builtinNames().createErroredReadableStreamPrivateName()).getObject(); - JSC::MarkedArgumentBuffer arguments; - arguments.append(JSC::JSValue::decode(encodedReason)); - ASSERT(!arguments.hasOverflowed()); - JSValue erroredStream = JSC::call(globalObject, function, arguments, "ReadableStream.create"_s); - RETURN_IF_EXCEPTION(scope, {}); - return JSValue::encode(erroredStream); -} - JSC::EncodedJSValue JSC__JSValue__createRangeError(const ZigString* message, const ZigString* arg1, JSC::JSGlobalObject* globalObject) { diff --git a/src/jsc/bindings/js_classes.ts b/src/jsc/bindings/js_classes.ts index 989f69cbb4e4..04a78a221967 100644 --- a/src/jsc/bindings/js_classes.ts +++ b/src/jsc/bindings/js_classes.ts @@ -4,9 +4,9 @@ export default [ // source-of-truth impl in src/codegen/generate-classes.ts // result in build/debug/codegen/ZigGeneratedClasses.cpp ["Blob"], - ["ReadableStream", "JSReadableStream.h"], - ["WritableStream", "JSWritableStream.h"], - ["TransformStream", "JSTransformStream.h"], + ["ReadableStream", "streams/JSReadableStream.h"], + ["WritableStream", "streams/JSWritableStream.h"], + ["TransformStream", "streams/JSTransformStream.h"], ["ArrayBuffer"], ["CompressionStream", "JSCompressionStream.h"], ["DecompressionStream", "JSDecompressionStream.h"], diff --git a/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h b/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h index f14c788bd571..e1a3c3b17012 100644 --- a/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/jsc/bindings/webcore/DOMClientIsoSubspaces.h @@ -278,12 +278,39 @@ class DOMClientIsoSubspaces { std::unique_ptr m_clientSubspaceForCountQueuingStrategy; std::unique_ptr m_clientSubspaceForReadableByteStreamController; std::unique_ptr m_clientSubspaceForReadableStream; + std::unique_ptr m_clientSubspaceForReadableStreamConstructor; + std::unique_ptr m_clientSubspaceForReadableStreamDefaultReaderConstructor; + std::unique_ptr m_clientSubspaceForReadableStreamBYOBReaderConstructor; + std::unique_ptr m_clientSubspaceForWritableStreamConstructor; + std::unique_ptr m_clientSubspaceForWritableStreamDefaultWriterConstructor; + std::unique_ptr m_clientSubspaceForTransformStreamConstructor; + std::unique_ptr m_clientSubspaceForByteLengthQueuingStrategyConstructor; + std::unique_ptr m_clientSubspaceForCountQueuingStrategyConstructor; + std::unique_ptr m_clientSubspaceForTextEncoderStreamConstructor; + std::unique_ptr m_clientSubspaceForTextDecoderStreamConstructor; + std::unique_ptr m_clientSubspaceForStreamsRuntime; + std::unique_ptr m_clientSubspaceForStreamPipeToOperation; + std::unique_ptr m_clientSubspaceForReadRequest; + std::unique_ptr m_clientSubspaceForReadIntoRequest; + std::unique_ptr m_clientSubspaceForPullIntoDescriptor; + std::unique_ptr m_clientSubspaceForStreamTeeState; + std::unique_ptr m_clientSubspaceForCrossRealmTransformState; + std::unique_ptr m_clientSubspaceForStreamFromIterableContext; + std::unique_ptr m_clientSubspaceForDirectStreamController; + std::unique_ptr m_clientSubspaceForNativeStreamSourceAdapter; + std::unique_ptr m_clientSubspaceForDirectSinkCloseState; + std::unique_ptr m_clientSubspaceForAsyncIteratorSourceOperation; + std::unique_ptr m_clientSubspaceForReadStreamIntoSinkOperation; + std::unique_ptr m_clientSubspaceForResumableSinkPumpOperation; + std::unique_ptr m_clientSubspaceForBunStandaloneTextSink; + std::unique_ptr m_clientSubspaceForOneShotDirectSink; + std::unique_ptr m_clientSubspaceForReadableStreamIntoArrayOperation; + std::unique_ptr m_clientSubspaceForReadableStreamAsyncIterator; + std::unique_ptr m_clientSubspaceForReadableStreamReaderBase; std::unique_ptr m_clientSubspaceForReadableStreamBYOBReader; std::unique_ptr m_clientSubspaceForReadableStreamBYOBRequest; std::unique_ptr m_clientSubspaceForReadableStreamDefaultController; std::unique_ptr m_clientSubspaceForReadableStreamDefaultReader; - std::unique_ptr m_clientSubspaceForReadableStreamSink; - std::unique_ptr m_clientSubspaceForReadableStreamSource; std::unique_ptr m_clientSubspaceForTransformStream; std::unique_ptr m_clientSubspaceForTransformStreamDefaultController; std::unique_ptr m_clientSubspaceForCompressionStream; @@ -291,7 +318,6 @@ class DOMClientIsoSubspaces { std::unique_ptr m_clientSubspaceForWritableStream; std::unique_ptr m_clientSubspaceForWritableStreamDefaultController; std::unique_ptr m_clientSubspaceForWritableStreamDefaultWriter; - std::unique_ptr m_clientSubspaceForWritableStreamSink; // std::unique_ptr m_clientSubspaceForWebLock; // std::unique_ptr m_clientSubspaceForWebLockManager; // std::unique_ptr m_clientSubspaceForAnalyserNode; diff --git a/src/jsc/bindings/webcore/DOMConstructors.h b/src/jsc/bindings/webcore/DOMConstructors.h index fa626592be4e..36a2fd08cc41 100644 --- a/src/jsc/bindings/webcore/DOMConstructors.h +++ b/src/jsc/bindings/webcore/DOMConstructors.h @@ -194,8 +194,6 @@ enum class DOMConstructorID : uint16_t { ReadableStreamBYOBRequest, ReadableStreamDefaultController, ReadableStreamDefaultReader, - ReadableStreamSink, - ReadableStreamSource, TransformStream, TransformStreamDefaultController, CompressionStream, @@ -203,7 +201,6 @@ enum class DOMConstructorID : uint16_t { WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter, - WritableStreamSink, WebLock, WebLockManager, AnalyserNode, @@ -863,7 +860,7 @@ enum class DOMConstructorID : uint16_t { URLPattern, }; -static constexpr unsigned numberOfDOMConstructorsBase = 848; +static constexpr unsigned numberOfDOMConstructorsBase = 845; static constexpr unsigned bunExtraConstructors = 4; diff --git a/src/jsc/bindings/webcore/DOMIsoSubspaces.h b/src/jsc/bindings/webcore/DOMIsoSubspaces.h index c67afb40065d..91a30452f0f1 100644 --- a/src/jsc/bindings/webcore/DOMIsoSubspaces.h +++ b/src/jsc/bindings/webcore/DOMIsoSubspaces.h @@ -260,12 +260,39 @@ class DOMIsoSubspaces { std::unique_ptr m_subspaceForCountQueuingStrategy; std::unique_ptr m_subspaceForReadableByteStreamController; std::unique_ptr m_subspaceForReadableStream; + std::unique_ptr m_subspaceForReadableStreamConstructor; + std::unique_ptr m_subspaceForReadableStreamDefaultReaderConstructor; + std::unique_ptr m_subspaceForReadableStreamBYOBReaderConstructor; + std::unique_ptr m_subspaceForWritableStreamConstructor; + std::unique_ptr m_subspaceForWritableStreamDefaultWriterConstructor; + std::unique_ptr m_subspaceForTransformStreamConstructor; + std::unique_ptr m_subspaceForByteLengthQueuingStrategyConstructor; + std::unique_ptr m_subspaceForCountQueuingStrategyConstructor; + std::unique_ptr m_subspaceForTextEncoderStreamConstructor; + std::unique_ptr m_subspaceForTextDecoderStreamConstructor; + std::unique_ptr m_subspaceForStreamsRuntime; + std::unique_ptr m_subspaceForStreamPipeToOperation; + std::unique_ptr m_subspaceForReadRequest; + std::unique_ptr m_subspaceForReadIntoRequest; + std::unique_ptr m_subspaceForPullIntoDescriptor; + std::unique_ptr m_subspaceForStreamTeeState; + std::unique_ptr m_subspaceForCrossRealmTransformState; + std::unique_ptr m_subspaceForStreamFromIterableContext; + std::unique_ptr m_subspaceForDirectStreamController; + std::unique_ptr m_subspaceForNativeStreamSourceAdapter; + std::unique_ptr m_subspaceForDirectSinkCloseState; + std::unique_ptr m_subspaceForAsyncIteratorSourceOperation; + std::unique_ptr m_subspaceForReadStreamIntoSinkOperation; + std::unique_ptr m_subspaceForResumableSinkPumpOperation; + std::unique_ptr m_subspaceForBunStandaloneTextSink; + std::unique_ptr m_subspaceForOneShotDirectSink; + std::unique_ptr m_subspaceForReadableStreamIntoArrayOperation; + std::unique_ptr m_subspaceForReadableStreamAsyncIterator; + std::unique_ptr m_subspaceForReadableStreamReaderBase; std::unique_ptr m_subspaceForReadableStreamBYOBReader; std::unique_ptr m_subspaceForReadableStreamBYOBRequest; std::unique_ptr m_subspaceForReadableStreamDefaultController; std::unique_ptr m_subspaceForReadableStreamDefaultReader; - std::unique_ptr m_subspaceForReadableStreamSink; - std::unique_ptr m_subspaceForReadableStreamSource; std::unique_ptr m_subspaceForTransformStream; std::unique_ptr m_subspaceForTransformStreamDefaultController; std::unique_ptr m_subspaceForCompressionStream; @@ -273,7 +300,6 @@ class DOMIsoSubspaces { std::unique_ptr m_subspaceForWritableStream; std::unique_ptr m_subspaceForWritableStreamDefaultController; std::unique_ptr m_subspaceForWritableStreamDefaultWriter; - std::unique_ptr m_subspaceForWritableStreamSink; // std::unique_ptr m_subspaceForWebLock; // std::unique_ptr m_subspaceForWebLockManager; // std::unique_ptr m_subspaceForAnalyserNode; diff --git a/src/jsc/bindings/webcore/InternalWritableStream.cpp b/src/jsc/bindings/webcore/InternalWritableStream.cpp deleted file mode 100644 index 9a788952b052..000000000000 --- a/src/jsc/bindings/webcore/InternalWritableStream.cpp +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright (C) 2020-2021 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CANON INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "config.h" -#include "InternalWritableStream.h" - -#include "Exception.h" -#include "WebCoreJSClientData.h" -#include "WebCoreJSBuiltins.h" - -namespace WebCore { - -static ExceptionOr invokeWritableStreamFunction(JSC::JSGlobalObject& globalObject, const JSC::Identifier& identifier, const JSC::MarkedArgumentBuffer& arguments) -{ - JSC::VM& vm = globalObject.vm(); - JSC::JSLockHolder lock(vm); - - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - - auto function = globalObject.get(&globalObject, identifier); - ASSERT(function.isCallable()); - scope.assertNoExceptionExceptTermination(); - - auto callData = JSC::getCallData(function); - - auto result = call(&globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(scope, Exception { ExistingExceptionError }); - - return result; -} - -ExceptionOr> InternalWritableStream::createFromUnderlyingSink(JSDOMGlobalObject& globalObject, JSC::JSValue underlyingSink, JSC::JSValue strategy) -{ - auto* clientData = static_cast(globalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().writableStreamInternalsBuiltins().createInternalWritableStreamFromUnderlyingSinkPrivateName(); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(underlyingSink); - arguments.append(strategy); - ASSERT(!arguments.hasOverflowed()); - - auto result = invokeWritableStreamFunction(globalObject, privateName, arguments); - if (result.hasException()) [[unlikely]] - return result.releaseException(); - - ASSERT(result.returnValue().isObject()); - return adoptRef(*new InternalWritableStream(globalObject, *result.returnValue().toObject(&globalObject))); -} - -Ref InternalWritableStream::fromObject(JSDOMGlobalObject& globalObject, JSC::JSObject& object) -{ - return adoptRef(*new InternalWritableStream(globalObject, object)); -} - -bool InternalWritableStream::locked() const -{ - auto* globalObject = this->globalObject(); - if (!globalObject) - return false; - - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(globalObject->vm()); - - auto* clientData = static_cast(globalObject->vm().clientData); - auto& privateName = clientData->builtinFunctions().writableStreamInternalsBuiltins().isWritableStreamLockedPrivateName(); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(guardedObject()); - ASSERT(!arguments.hasOverflowed()); - - auto result = invokeWritableStreamFunction(*globalObject, privateName, arguments); - CLEAR_IF_EXCEPTION(scope); - return result.hasException() ? false : result.returnValue().isTrue(); -} - -void InternalWritableStream::lock() -{ - auto* globalObject = this->globalObject(); - if (!globalObject) - return; - - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(globalObject->vm()); - - auto* clientData = static_cast(globalObject->vm().clientData); - auto& privateName = clientData->builtinFunctions().writableStreamInternalsBuiltins().acquireWritableStreamDefaultWriterPrivateName(); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(guardedObject()); - ASSERT(!arguments.hasOverflowed()); - - auto result = invokeWritableStreamFunction(*globalObject, privateName, arguments); - CLEAR_IF_EXCEPTION(scope); -} - -JSC::JSValue InternalWritableStream::abort(JSC::JSGlobalObject& globalObject, JSC::JSValue reason) -{ - auto* clientData = static_cast(globalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().writableStreamInternalsBuiltins().writableStreamAbortForBindingsPrivateName(); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(guardedObject()); - arguments.append(reason); - ASSERT(!arguments.hasOverflowed()); - - auto result = invokeWritableStreamFunction(globalObject, privateName, arguments); - if (result.hasException()) - return {}; - - return result.returnValue(); -} - -JSC::JSValue InternalWritableStream::close(JSC::JSGlobalObject& globalObject) -{ - auto* clientData = static_cast(globalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().writableStreamInternalsBuiltins().writableStreamCloseForBindingsPrivateName(); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(guardedObject()); - ASSERT(!arguments.hasOverflowed()); - - auto result = invokeWritableStreamFunction(globalObject, privateName, arguments); - if (result.hasException()) - return {}; - - return result.returnValue(); -} - -JSC::JSValue InternalWritableStream::getWriter(JSC::JSGlobalObject& globalObject) -{ - auto* clientData = static_cast(globalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().writableStreamInternalsBuiltins().acquireWritableStreamDefaultWriterPrivateName(); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(guardedObject()); - ASSERT(!arguments.hasOverflowed()); - - auto result = invokeWritableStreamFunction(globalObject, privateName, arguments); - if (result.hasException()) - return {}; - - return result.returnValue(); -} - -} diff --git a/src/jsc/bindings/webcore/InternalWritableStream.h b/src/jsc/bindings/webcore/InternalWritableStream.h deleted file mode 100644 index 0ad2c8cd015a..000000000000 --- a/src/jsc/bindings/webcore/InternalWritableStream.h +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2020-2021 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CANON INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include "ExceptionOr.h" -#include "JSDOMGuardedObject.h" -#include - -namespace WebCore { -class InternalWritableStream final : public DOMGuarded { -public: - static ExceptionOr> createFromUnderlyingSink(JSDOMGlobalObject&, JSC::JSValue underlyingSink, JSC::JSValue strategy); - static Ref fromObject(JSDOMGlobalObject&, JSC::JSObject&); - - operator JSC::JSValue() const { return guarded(); } - - bool locked() const; - void lock(); - JSC::JSValue abort(JSC::JSGlobalObject&, JSC::JSValue); - JSC::JSValue close(JSC::JSGlobalObject&); - JSC::JSValue getWriter(JSC::JSGlobalObject&); - -private: - // InternalWritableStream is exclusively owned by WritableStream, which - // is exclusively owned by JSWritableStream. Liveness of the guarded - // internal stream object is driven by JSWritableStream::visitChildren, - // not by the global object's m_guardedObjects set. - InternalWritableStream(JSDOMGlobalObject& globalObject, JSC::JSObject& jsObject) - : DOMGuarded(globalObject, jsObject, DoNotRegisterWithGlobalObjectTag {}) - { - } -}; - -} diff --git a/src/jsc/bindings/webcore/JSByteLengthQueuingStrategy.cpp b/src/jsc/bindings/webcore/JSByteLengthQueuingStrategy.cpp deleted file mode 100644 index 95a87d55d6bc..000000000000 --- a/src/jsc/bindings/webcore/JSByteLengthQueuingStrategy.cpp +++ /dev/null @@ -1,180 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSByteLengthQueuingStrategy.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsByteLengthQueuingStrategyConstructor); - -class JSByteLengthQueuingStrategyPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSByteLengthQueuingStrategyPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSByteLengthQueuingStrategyPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSByteLengthQueuingStrategyPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSByteLengthQueuingStrategyPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSByteLengthQueuingStrategyPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSByteLengthQueuingStrategyPrototype, JSByteLengthQueuingStrategyPrototype::Base); - -using JSByteLengthQueuingStrategyDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSByteLengthQueuingStrategyDOMConstructor::s_info = { "ByteLengthQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSByteLengthQueuingStrategyDOMConstructor) }; - -template<> JSValue JSByteLengthQueuingStrategyDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSByteLengthQueuingStrategyDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "ByteLengthQueuingStrategy"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSByteLengthQueuingStrategy::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSByteLengthQueuingStrategyDOMConstructor::initializeExecutable(VM& vm) -{ - return byteLengthQueuingStrategyInitializeByteLengthQueuingStrategyCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSByteLengthQueuingStrategyPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsByteLengthQueuingStrategyConstructor, 0 } }, - { "highWaterMark"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, byteLengthQueuingStrategyHighWaterMarkCodeGenerator, 0 } }, - { "size"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, byteLengthQueuingStrategySizeCodeGenerator, 0 } } -}; - -const ClassInfo JSByteLengthQueuingStrategyPrototype::s_info = { "ByteLengthQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSByteLengthQueuingStrategyPrototype) }; - -void JSByteLengthQueuingStrategyPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSByteLengthQueuingStrategy::info(), JSByteLengthQueuingStrategyPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSByteLengthQueuingStrategy::s_info = { "ByteLengthQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSByteLengthQueuingStrategy) }; - -JSByteLengthQueuingStrategy::JSByteLengthQueuingStrategy(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSByteLengthQueuingStrategy::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSByteLengthQueuingStrategy::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSByteLengthQueuingStrategyPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSByteLengthQueuingStrategyPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSByteLengthQueuingStrategy::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSByteLengthQueuingStrategy::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSByteLengthQueuingStrategy::destroy(JSC::JSCell* cell) -{ - JSByteLengthQueuingStrategy* thisObject = static_cast(cell); - thisObject->JSByteLengthQueuingStrategy::~JSByteLengthQueuingStrategy(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsByteLengthQueuingStrategyConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSByteLengthQueuingStrategy::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSByteLengthQueuingStrategy::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForByteLengthQueuingStrategy.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForByteLengthQueuingStrategy = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForByteLengthQueuingStrategy.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForByteLengthQueuingStrategy = std::forward(space); }); -} -} diff --git a/src/jsc/bindings/webcore/JSByteLengthQueuingStrategy.h b/src/jsc/bindings/webcore/JSByteLengthQueuingStrategy.h deleted file mode 100644 index 20fb28e915ab..000000000000 --- a/src/jsc/bindings/webcore/JSByteLengthQueuingStrategy.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSByteLengthQueuingStrategy : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSByteLengthQueuingStrategy* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSByteLengthQueuingStrategy* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSByteLengthQueuingStrategy(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSByteLengthQueuingStrategy(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSCountQueuingStrategy.cpp b/src/jsc/bindings/webcore/JSCountQueuingStrategy.cpp deleted file mode 100644 index 6c9ca6691b14..000000000000 --- a/src/jsc/bindings/webcore/JSCountQueuingStrategy.cpp +++ /dev/null @@ -1,181 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSCountQueuingStrategy.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsCountQueuingStrategyConstructor); - -class JSCountQueuingStrategyPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSCountQueuingStrategyPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSCountQueuingStrategyPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSCountQueuingStrategyPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCountQueuingStrategyPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSCountQueuingStrategyPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCountQueuingStrategyPrototype, JSCountQueuingStrategyPrototype::Base); - -using JSCountQueuingStrategyDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSCountQueuingStrategyDOMConstructor::s_info = { "CountQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCountQueuingStrategyDOMConstructor) }; - -template<> JSValue JSCountQueuingStrategyDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSCountQueuingStrategyDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "CountQueuingStrategy"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSCountQueuingStrategy::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSCountQueuingStrategyDOMConstructor::initializeExecutable(VM& vm) -{ - return countQueuingStrategyInitializeCountQueuingStrategyCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSCountQueuingStrategyPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsCountQueuingStrategyConstructor, 0 } }, - { "highWaterMark"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, countQueuingStrategyHighWaterMarkCodeGenerator, 0 } }, - { "size"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, countQueuingStrategySizeCodeGenerator, 0 } } -}; - -const ClassInfo JSCountQueuingStrategyPrototype::s_info = { "CountQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCountQueuingStrategyPrototype) }; - -void JSCountQueuingStrategyPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSCountQueuingStrategy::info(), JSCountQueuingStrategyPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSCountQueuingStrategy::s_info = { "CountQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCountQueuingStrategy) }; - -JSCountQueuingStrategy::JSCountQueuingStrategy(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSCountQueuingStrategy::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSCountQueuingStrategy::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSCountQueuingStrategyPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSCountQueuingStrategyPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSCountQueuingStrategy::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSCountQueuingStrategy::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSCountQueuingStrategy::destroy(JSC::JSCell* cell) -{ - JSCountQueuingStrategy* thisObject = static_cast(cell); - thisObject->JSCountQueuingStrategy::~JSCountQueuingStrategy(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsCountQueuingStrategyConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSCountQueuingStrategy::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSCountQueuingStrategy::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForCountQueuingStrategy.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCountQueuingStrategy = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForCountQueuingStrategy.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForCountQueuingStrategy = std::forward(space); }); -} - -} diff --git a/src/jsc/bindings/webcore/JSCountQueuingStrategy.h b/src/jsc/bindings/webcore/JSCountQueuingStrategy.h deleted file mode 100644 index 0e582615eaf1..000000000000 --- a/src/jsc/bindings/webcore/JSCountQueuingStrategy.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSCountQueuingStrategy : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSCountQueuingStrategy* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSCountQueuingStrategy* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSCountQueuingStrategy(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSCountQueuingStrategy(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp b/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp index 1841c00d783d..e4a4ea5bc8d1 100644 --- a/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp +++ b/src/jsc/bindings/webcore/JSDOMConvertNumbers.cpp @@ -134,7 +134,7 @@ static inline T toSmallerInt(JSGlobalObject& lexicalGlobalObject, JSValue value) case IntegerConversionConfiguration::Normal: break; case IntegerConversionConfiguration::EnforceRange: - return enforceRange(lexicalGlobalObject, x, LimitsTrait::minValue, LimitsTrait::maxValue); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, LimitsTrait::minValue, LimitsTrait::maxValue)); case IntegerConversionConfiguration::Clamp: return std::isnan(x) ? 0 : clampTo(clampRoundEven(x)); } @@ -180,7 +180,7 @@ static inline T toSmallerUInt(JSGlobalObject& lexicalGlobalObject, JSValue value case IntegerConversionConfiguration::Normal: break; case IntegerConversionConfiguration::EnforceRange: - return enforceRange(lexicalGlobalObject, x, 0, LimitsTrait::maxValue); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, 0, LimitsTrait::maxValue)); case IntegerConversionConfiguration::Clamp: return std::isnan(x) ? 0 : clampTo(clampRoundEven(x)); } @@ -265,7 +265,7 @@ template<> int32_t convertToIntegerEnforceRange(JSC::JSGlobalObject& le double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, kMinInt32, kMaxInt32); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, kMinInt32, kMaxInt32)); } template<> uint32_t convertToIntegerEnforceRange(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) @@ -278,7 +278,7 @@ template<> uint32_t convertToIntegerEnforceRange(JSC::JSGlobalObject& double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, 0, kMaxUInt32); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, 0, kMaxUInt32)); } template<> int32_t convertToIntegerClamp(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) @@ -319,7 +319,7 @@ template<> int64_t convertToIntegerEnforceRange(JSC::JSGlobalObject& le double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, -kJSMaxInteger, kJSMaxInteger); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, -kJSMaxInteger, kJSMaxInteger)); } template<> uint64_t convertToIntegerEnforceRange(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) @@ -332,7 +332,7 @@ template<> uint64_t convertToIntegerEnforceRange(JSC::JSGlobalObject& double x = value.toNumber(&lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, 0); - return enforceRange(lexicalGlobalObject, x, 0, kJSMaxInteger); + RELEASE_AND_RETURN(scope, enforceRange(lexicalGlobalObject, x, 0, kJSMaxInteger)); } template<> int64_t convertToIntegerClamp(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) diff --git a/src/jsc/bindings/webcore/JSReadableByteStreamController.cpp b/src/jsc/bindings/webcore/JSReadableByteStreamController.cpp deleted file mode 100644 index 3106e5038f8d..000000000000 --- a/src/jsc/bindings/webcore/JSReadableByteStreamController.cpp +++ /dev/null @@ -1,183 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableByteStreamController.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsReadableByteStreamControllerConstructor); - -class JSReadableByteStreamControllerPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableByteStreamControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableByteStreamControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableByteStreamControllerPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableByteStreamControllerPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableByteStreamControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableByteStreamControllerPrototype, JSReadableByteStreamControllerPrototype::Base); - -using JSReadableByteStreamControllerDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSReadableByteStreamControllerDOMConstructor::s_info = { "ReadableByteStreamController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableByteStreamControllerDOMConstructor) }; - -template<> JSValue JSReadableByteStreamControllerDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSReadableByteStreamControllerDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(3), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "ReadableByteStreamController"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSReadableByteStreamController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSReadableByteStreamControllerDOMConstructor::initializeExecutable(VM& vm) -{ - return readableByteStreamControllerInitializeReadableByteStreamControllerCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSReadableByteStreamControllerPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableByteStreamControllerConstructor, 0 } }, - { "byobRequest"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, readableByteStreamControllerByobRequestCodeGenerator, 0 } }, - { "desiredSize"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, readableByteStreamControllerDesiredSizeCodeGenerator, 0 } }, - { "enqueue"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableByteStreamControllerEnqueueCodeGenerator, 0 } }, - { "close"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableByteStreamControllerCloseCodeGenerator, 0 } }, - { "error"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableByteStreamControllerErrorCodeGenerator, 0 } }, -}; - -const ClassInfo JSReadableByteStreamControllerPrototype::s_info = { "ReadableByteStreamController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableByteStreamControllerPrototype) }; - -void JSReadableByteStreamControllerPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSReadableByteStreamController::info(), JSReadableByteStreamControllerPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSReadableByteStreamController::s_info = { "ReadableByteStreamController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableByteStreamController) }; - -JSReadableByteStreamController::JSReadableByteStreamController(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSReadableByteStreamController::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSReadableByteStreamController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableByteStreamControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableByteStreamControllerPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableByteStreamController::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSReadableByteStreamController::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSReadableByteStreamController::destroy(JSC::JSCell* cell) -{ - JSReadableByteStreamController* thisObject = static_cast(cell); - thisObject->JSReadableByteStreamController::~JSReadableByteStreamController(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsReadableByteStreamControllerConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSReadableByteStreamController::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSReadableByteStreamController::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableByteStreamController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableByteStreamController = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableByteStreamController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableByteStreamController = std::forward(space); }); -} -} diff --git a/src/jsc/bindings/webcore/JSReadableByteStreamController.h b/src/jsc/bindings/webcore/JSReadableByteStreamController.h deleted file mode 100644 index 6fbe0488f53e..000000000000 --- a/src/jsc/bindings/webcore/JSReadableByteStreamController.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSReadableByteStreamController : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSReadableByteStreamController* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSReadableByteStreamController* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableByteStreamController(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSReadableByteStreamController(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStream.cpp b/src/jsc/bindings/webcore/JSReadableStream.cpp deleted file mode 100644 index d1eb55a7192a..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStream.cpp +++ /dev/null @@ -1,315 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableStream.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "ZigGeneratedClasses.h" -#include "JavaScriptCore/BuiltinNames.h" -#include "ZigGlobalObject.h" -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -extern "C" void ReadableStream__incrementCount(void*, int32_t); - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamConstructor); - -class JSReadableStreamPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableStreamPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamPrototype, JSReadableStreamPrototype::Base); - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamProtoFuncText, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - JSReadableStream* thisObject = dynamicDowncast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - throwThisTypeError(*globalObject, scope, "ReadableStream"_s, "text"_s); - return {}; - } - - return ZigGlobalObject__readableStreamToText(defaultGlobalObject(globalObject), JSValue::encode(thisObject)); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamProtoFuncBytes, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - JSReadableStream* thisObject = dynamicDowncast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - throwThisTypeError(*globalObject, scope, "ReadableStream"_s, "bytes"_s); - return {}; - } - - return ZigGlobalObject__readableStreamToBytes(defaultGlobalObject(globalObject), JSValue::encode(thisObject)); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamProtoFuncJSON, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - JSReadableStream* thisObject = dynamicDowncast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - throwThisTypeError(*globalObject, scope, "ReadableStream"_s, "json"_s); - return {}; - } - - return ZigGlobalObject__readableStreamToJSON(defaultGlobalObject(globalObject), JSValue::encode(thisObject)); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamProtoFuncBlob, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - JSReadableStream* thisObject = dynamicDowncast(callFrame->thisValue()); - if (!thisObject) [[unlikely]] { - auto& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - throwThisTypeError(*globalObject, scope, "ReadableStream"_s, "blob"_s); - return {}; - } - - return ZigGlobalObject__readableStreamToBlob(defaultGlobalObject(globalObject), JSValue::encode(thisObject)); -} -using JSReadableStreamDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSReadableStreamDOMConstructor::s_info = { "ReadableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDOMConstructor) }; - -template<> JSValue JSReadableStreamDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSReadableStreamDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "ReadableStream"_s); - 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); -} - -template<> FunctionExecutable* JSReadableStreamDOMConstructor::initializeExecutable(VM& vm) -{ - return readableStreamInitializeReadableStreamCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSReadableStreamPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamConstructor, 0 } }, - { "blob"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamProtoFuncBlob, 0 } }, - { "bytes"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamProtoFuncBytes, 0 } }, - { "cancel"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamCancelCodeGenerator, 0 } }, - { "getReader"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamGetReaderCodeGenerator, 0 } }, - { "json"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamProtoFuncJSON, 0 } }, - { "locked"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, readableStreamLockedCodeGenerator, 0 } }, - { "pipeThrough"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamPipeThroughCodeGenerator, 2 } }, - { "pipeTo"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamPipeToCodeGenerator, 1 } }, - { "tee"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamTeeCodeGenerator, 0 } }, - { "text"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamProtoFuncText, 0 } }, -}; - -const ClassInfo JSReadableStreamPrototype::s_info = { "ReadableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamPrototype) }; - -static JSC_DEFINE_CUSTOM_SETTER(JSReadableStreamPrototype__nativePtrSetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::EncodedJSValue encodedJSValue, JSC::PropertyName)) -{ - JSReadableStream* thisObject = uncheckedDowncast(JSValue::decode(encodedThisValue)); - thisObject->setNativePtr(lexicalGlobalObject->vm(), JSValue::decode(encodedJSValue)); - return true; -} - -static JSC_DEFINE_CUSTOM_GETTER(JSReadableStreamPrototype__nativePtrGetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::PropertyName)) -{ - JSReadableStream* thisObject = uncheckedDowncast(JSValue::decode(encodedThisValue)); - - // Force it to be locked, even though the value is still really there. - if (thisObject->isNativeTypeTransferred()) { - return JSValue::encode(jsNumber(-1)); - } - - return JSValue::encode(thisObject->nativePtr()); -} - -static JSC_DEFINE_CUSTOM_SETTER(JSReadableStreamPrototype__nativeTypeSetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::EncodedJSValue encodedJSValue, JSC::PropertyName)) -{ - JSReadableStream* thisObject = uncheckedDowncast(JSValue::decode(encodedThisValue)); - thisObject->setNativeType(JSValue::decode(encodedJSValue).toInt32(lexicalGlobalObject)); - return true; -} - -static JSC_DEFINE_CUSTOM_GETTER(JSReadableStreamPrototype__nativeTypeGetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::PropertyName)) -{ - JSReadableStream* thisObject = uncheckedDowncast(JSValue::decode(encodedThisValue)); - return JSValue::encode(jsNumber(thisObject->nativeType())); -} - -static JSC_DEFINE_CUSTOM_SETTER(JSReadableStreamPrototype__disturbedSetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::EncodedJSValue encodedJSValue, JSC::PropertyName)) -{ - JSReadableStream* thisObject = uncheckedDowncast(JSValue::decode(encodedThisValue)); - thisObject->setDisturbed(JSValue::decode(encodedJSValue).toBoolean(lexicalGlobalObject)); - return true; -} - -static JSC_DEFINE_CUSTOM_GETTER(JSReadableStreamPrototype__disturbedGetterWrap, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue encodedThisValue, JSC::PropertyName)) -{ - JSReadableStream* thisObject = uncheckedDowncast(JSValue::decode(encodedThisValue)); - return JSValue::encode(jsBoolean(thisObject->disturbed())); -} - -void JSReadableStreamPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - auto clientData = WebCore::clientData(vm); - - this->putDirectCustomAccessor(vm, clientData->builtinNames().bunNativePtrPrivateName(), DOMAttributeGetterSetter::create(vm, JSReadableStreamPrototype__nativePtrGetterWrap, JSReadableStreamPrototype__nativePtrSetterWrap, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete); - this->putDirectCustomAccessor(vm, clientData->builtinNames().bunNativeTypePrivateName(), DOMAttributeGetterSetter::create(vm, JSReadableStreamPrototype__nativeTypeGetterWrap, JSReadableStreamPrototype__nativeTypeSetterWrap, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete); - this->putDirectCustomAccessor(vm, clientData->builtinNames().disturbedPrivateName(), DOMAttributeGetterSetter::create(vm, JSReadableStreamPrototype__disturbedGetterWrap, JSReadableStreamPrototype__disturbedSetterWrap, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | PropertyAttribute::DontDelete); - - reifyStaticProperties(vm, JSReadableStream::info(), JSReadableStreamPrototypeTableValues, *this); - this->putDirectBuiltinFunction(vm, globalObject(), vm.propertyNames->asyncIteratorSymbol, readableStreamLazyAsyncIteratorCodeGenerator(vm), JSC::PropertyAttribute::DontDelete | 0); - this->putDirectBuiltinFunction(vm, globalObject(), vm.propertyNames->builtinNames().valuesPublicName(), readableStreamValuesCodeGenerator(vm), JSC::PropertyAttribute::DontDelete | 0); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSReadableStream::s_info = { "ReadableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStream) }; - -JSReadableStream::JSReadableStream(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSReadableStream::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -void JSReadableStream::setNativePtr(JSC::VM& vm, JSC::JSValue value) -{ - this->m_nativePtr.set(vm, this, value); -} - -JSObject* JSReadableStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableStreamPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSReadableStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSReadableStream::destroy(JSC::JSCell* cell) -{ - JSReadableStream* thisObject = static_cast(cell); - thisObject->JSReadableStream::~JSReadableStream(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSReadableStream::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSReadableStream::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableStream.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStream = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableStream.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStream = std::forward(space); }); -} - -template -void JSReadableStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) -{ - JSReadableStream* stream = uncheckedDowncast(cell); - ASSERT_GC_OBJECT_INHERITS(stream, info()); - Base::visitChildren(stream, visitor); - - visitor.append(stream->m_nativePtr); -} - -DEFINE_VISIT_CHILDREN(JSReadableStream); - -} diff --git a/src/jsc/bindings/webcore/JSReadableStream.h b/src/jsc/bindings/webcore/JSReadableStream.h deleted file mode 100644 index 45c14a9a7b6a..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStream.h +++ /dev/null @@ -1,95 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSReadableStream : public JSDOMObject { - -public: - using Base = JSDOMObject; - static JSReadableStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSReadableStream* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableStream(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - - int nativeType() const { return this->m_nativeType; } - bool disturbed() const { return this->m_disturbed; } - bool isNativeTypeTransferred() const { return this->m_transferred; } - void setTransferred() - { - this->m_transferred = true; - } - JSC::JSValue nativePtr() - { - return this->m_nativePtr.get(); - } - - void setNativePtr(JSC::VM&, JSC::JSValue value); - - void setNativeType(int value) - { - this->m_nativeType = value; - } - - void setDisturbed(bool value) - { - this->m_disturbed = value; - } - - DECLARE_VISIT_CHILDREN; - -protected: - mutable JSC::WriteBarrier m_nativePtr; - int m_nativeType { 0 }; - bool m_disturbed = false; - bool m_transferred = false; - - JSReadableStream(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStreamBYOBReader.cpp b/src/jsc/bindings/webcore/JSReadableStreamBYOBReader.cpp deleted file mode 100644 index 95ff528a5063..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamBYOBReader.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableStreamBYOBReader.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamBYOBReaderConstructor); - -class JSReadableStreamBYOBReaderPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableStreamBYOBReaderPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableStreamBYOBReaderPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamBYOBReaderPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBReaderPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableStreamBYOBReaderPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBReaderPrototype, JSReadableStreamBYOBReaderPrototype::Base); - -using JSReadableStreamBYOBReaderDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSReadableStreamBYOBReaderDOMConstructor::s_info = { "ReadableStreamBYOBReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBReaderDOMConstructor) }; - -template<> JSValue JSReadableStreamBYOBReaderDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSReadableStreamBYOBReaderDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "ReadableStreamBYOBReader"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSReadableStreamBYOBReader::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSReadableStreamBYOBReaderDOMConstructor::initializeExecutable(VM& vm) -{ - return readableStreamBYOBReaderInitializeReadableStreamBYOBReaderCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSReadableStreamBYOBReaderPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamBYOBReaderConstructor, 0 } }, - { "closed"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, readableStreamBYOBReaderClosedCodeGenerator, 0 } }, - { "read"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamBYOBReaderReadCodeGenerator, 0 } }, - { "cancel"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamBYOBReaderCancelCodeGenerator, 0 } }, - { "releaseLock"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamBYOBReaderReleaseLockCodeGenerator, 0 } }, -}; - -const ClassInfo JSReadableStreamBYOBReaderPrototype::s_info = { "ReadableStreamBYOBReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBReaderPrototype) }; - -void JSReadableStreamBYOBReaderPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSReadableStreamBYOBReader::info(), JSReadableStreamBYOBReaderPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSReadableStreamBYOBReader::s_info = { "ReadableStreamBYOBReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBReader) }; - -JSReadableStreamBYOBReader::JSReadableStreamBYOBReader(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSReadableStreamBYOBReader::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSReadableStreamBYOBReader::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableStreamBYOBReaderPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableStreamBYOBReaderPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableStreamBYOBReader::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSReadableStreamBYOBReader::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSReadableStreamBYOBReader::destroy(JSC::JSCell* cell) -{ - JSReadableStreamBYOBReader* thisObject = static_cast(cell); - thisObject->JSReadableStreamBYOBReader::~JSReadableStreamBYOBReader(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamBYOBReaderConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSReadableStreamBYOBReader::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSReadableStreamBYOBReader::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamBYOBReader.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamBYOBReader = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableStreamBYOBReader.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamBYOBReader = std::forward(space); }); -} -} diff --git a/src/jsc/bindings/webcore/JSReadableStreamBYOBReader.h b/src/jsc/bindings/webcore/JSReadableStreamBYOBReader.h deleted file mode 100644 index b206a3beed12..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamBYOBReader.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSReadableStreamBYOBReader : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSReadableStreamBYOBReader* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSReadableStreamBYOBReader* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableStreamBYOBReader(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSReadableStreamBYOBReader(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStreamBYOBRequest.cpp b/src/jsc/bindings/webcore/JSReadableStreamBYOBRequest.cpp deleted file mode 100644 index 5b30d1fd7ea5..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamBYOBRequest.cpp +++ /dev/null @@ -1,181 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableStreamBYOBRequest.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamBYOBRequestConstructor); - -class JSReadableStreamBYOBRequestPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableStreamBYOBRequestPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableStreamBYOBRequestPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamBYOBRequestPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBRequestPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableStreamBYOBRequestPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBRequestPrototype, JSReadableStreamBYOBRequestPrototype::Base); - -using JSReadableStreamBYOBRequestDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSReadableStreamBYOBRequestDOMConstructor::s_info = { "ReadableStreamBYOBRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBRequestDOMConstructor) }; - -template<> JSValue JSReadableStreamBYOBRequestDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSReadableStreamBYOBRequestDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(2), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "ReadableStreamBYOBRequest"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSReadableStreamBYOBRequest::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSReadableStreamBYOBRequestDOMConstructor::initializeExecutable(VM& vm) -{ - return readableStreamBYOBRequestInitializeReadableStreamBYOBRequestCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSReadableStreamBYOBRequestPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamBYOBRequestConstructor, 0 } }, - { "view"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, readableStreamBYOBRequestViewCodeGenerator, 0 } }, - { "respond"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamBYOBRequestRespondCodeGenerator, 0 } }, - { "respondWithNewView"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamBYOBRequestRespondWithNewViewCodeGenerator, 0 } }, -}; - -const ClassInfo JSReadableStreamBYOBRequestPrototype::s_info = { "ReadableStreamBYOBRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBRequestPrototype) }; - -void JSReadableStreamBYOBRequestPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSReadableStreamBYOBRequest::info(), JSReadableStreamBYOBRequestPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSReadableStreamBYOBRequest::s_info = { "ReadableStreamBYOBRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBRequest) }; - -JSReadableStreamBYOBRequest::JSReadableStreamBYOBRequest(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSReadableStreamBYOBRequest::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSReadableStreamBYOBRequest::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableStreamBYOBRequestPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableStreamBYOBRequestPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableStreamBYOBRequest::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSReadableStreamBYOBRequest::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSReadableStreamBYOBRequest::destroy(JSC::JSCell* cell) -{ - JSReadableStreamBYOBRequest* thisObject = static_cast(cell); - thisObject->JSReadableStreamBYOBRequest::~JSReadableStreamBYOBRequest(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamBYOBRequestConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSReadableStreamBYOBRequest::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSReadableStreamBYOBRequest::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamBYOBRequest.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamBYOBRequest = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableStreamBYOBRequest.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamBYOBRequest = std::forward(space); }); -} -} diff --git a/src/jsc/bindings/webcore/JSReadableStreamBYOBRequest.h b/src/jsc/bindings/webcore/JSReadableStreamBYOBRequest.h deleted file mode 100644 index 94bb293b442f..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamBYOBRequest.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSReadableStreamBYOBRequest : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSReadableStreamBYOBRequest* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSReadableStreamBYOBRequest* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableStreamBYOBRequest(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSReadableStreamBYOBRequest(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStreamDefaultController.cpp b/src/jsc/bindings/webcore/JSReadableStreamDefaultController.cpp deleted file mode 100644 index 339f4e7efbcc..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamDefaultController.cpp +++ /dev/null @@ -1,186 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableStreamDefaultController.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamDefaultControllerConstructor); - -class JSReadableStreamDefaultControllerPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableStreamDefaultControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableStreamDefaultControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamDefaultControllerPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultControllerPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableStreamDefaultControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultControllerPrototype, JSReadableStreamDefaultControllerPrototype::Base); - -using JSReadableStreamDefaultControllerDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSReadableStreamDefaultControllerDOMConstructor::s_info = { "ReadableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultControllerDOMConstructor) }; - -template<> JSValue JSReadableStreamDefaultControllerDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSReadableStreamDefaultControllerDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(4), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "ReadableStreamDefaultController"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSReadableStreamDefaultController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSReadableStreamDefaultControllerDOMConstructor::initializeExecutable(VM& vm) -{ - return readableStreamDefaultControllerInitializeReadableStreamDefaultControllerCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSReadableStreamDefaultControllerPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamDefaultControllerConstructor, 0 } }, - { "desiredSize"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, readableStreamDefaultControllerDesiredSizeCodeGenerator, 0 } }, - { "enqueue"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamDefaultControllerEnqueueCodeGenerator, 0 } }, - { "close"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamDefaultControllerCloseCodeGenerator, 0 } }, - { "error"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamDefaultControllerErrorCodeGenerator, 0 } }, -}; - -const ClassInfo JSReadableStreamDefaultControllerPrototype::s_info = { "ReadableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultControllerPrototype) }; - -void JSReadableStreamDefaultControllerPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSReadableStreamDefaultController::info(), JSReadableStreamDefaultControllerPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); - - auto clientData = WebCore::clientData(vm); - this->putDirect(vm, clientData->builtinNames().sinkPublicName(), jsUndefined(), JSC::PropertyAttribute::DontDelete | 0); -} - -const ClassInfo JSReadableStreamDefaultController::s_info = { "ReadableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultController) }; - -JSReadableStreamDefaultController::JSReadableStreamDefaultController(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSReadableStreamDefaultController::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSReadableStreamDefaultController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableStreamDefaultControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableStreamDefaultControllerPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableStreamDefaultController::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSReadableStreamDefaultController::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSReadableStreamDefaultController::destroy(JSC::JSCell* cell) -{ - JSReadableStreamDefaultController* thisObject = static_cast(cell); - thisObject->JSReadableStreamDefaultController::~JSReadableStreamDefaultController(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamDefaultControllerConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSReadableStreamDefaultController::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSReadableStreamDefaultController::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamDefaultController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamDefaultController = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableStreamDefaultController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamDefaultController = std::forward(space); }); -} - -} diff --git a/src/jsc/bindings/webcore/JSReadableStreamDefaultController.h b/src/jsc/bindings/webcore/JSReadableStreamDefaultController.h deleted file mode 100644 index 4279e712f1e5..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamDefaultController.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSReadableStreamDefaultController : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSReadableStreamDefaultController* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSReadableStreamDefaultController* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableStreamDefaultController(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSReadableStreamDefaultController(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStreamDefaultReader.cpp b/src/jsc/bindings/webcore/JSReadableStreamDefaultReader.cpp deleted file mode 100644 index a6041eba0623..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamDefaultReader.cpp +++ /dev/null @@ -1,186 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableStreamDefaultReader.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamDefaultReaderConstructor); - -class JSReadableStreamDefaultReaderPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableStreamDefaultReaderPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableStreamDefaultReaderPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamDefaultReaderPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultReaderPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableStreamDefaultReaderPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultReaderPrototype, JSReadableStreamDefaultReaderPrototype::Base); - -using JSReadableStreamDefaultReaderDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSReadableStreamDefaultReaderDOMConstructor::s_info = { "ReadableStreamDefaultReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultReaderDOMConstructor) }; - -template<> JSValue JSReadableStreamDefaultReaderDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSReadableStreamDefaultReaderDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "ReadableStreamDefaultReader"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSReadableStreamDefaultReader::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSReadableStreamDefaultReaderDOMConstructor::initializeExecutable(VM& vm) -{ - return readableStreamDefaultReaderInitializeReadableStreamDefaultReaderCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSReadableStreamDefaultReaderPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamDefaultReaderConstructor, 0 } }, - { "closed"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, readableStreamDefaultReaderClosedCodeGenerator, 0 } }, - { "read"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamDefaultReaderReadCodeGenerator, 0 } }, - { "readMany"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamDefaultReaderReadManyCodeGenerator, 0 } }, - { "cancel"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamDefaultReaderCancelCodeGenerator, 0 } }, - { "releaseLock"_s, static_cast(JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, readableStreamDefaultReaderReleaseLockCodeGenerator, 0 } }, -}; - -const ClassInfo JSReadableStreamDefaultReaderPrototype::s_info = { "ReadableStreamDefaultReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultReaderPrototype) }; - -void JSReadableStreamDefaultReaderPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSReadableStreamDefaultReader::info(), JSReadableStreamDefaultReaderPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); - // As suggested by https://github.com/tc39/proposal-explicit-resource-management#relation-to-dom-apis - // putDirectWithoutTransition(vm, vm.propertyNames->disposeSymbol, get(globalObject(), PropertyName(Identifier::fromString(vm, "releaseLock"_s))), JSC::PropertyAttribute::DontEnum | 0); -} - -const ClassInfo JSReadableStreamDefaultReader::s_info = { "ReadableStreamDefaultReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultReader) }; - -JSReadableStreamDefaultReader::JSReadableStreamDefaultReader(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSReadableStreamDefaultReader::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSReadableStreamDefaultReader::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableStreamDefaultReaderPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableStreamDefaultReaderPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableStreamDefaultReader::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSReadableStreamDefaultReader::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSReadableStreamDefaultReader::destroy(JSC::JSCell* cell) -{ - JSReadableStreamDefaultReader* thisObject = static_cast(cell); - thisObject->JSReadableStreamDefaultReader::~JSReadableStreamDefaultReader(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamDefaultReaderConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSReadableStreamDefaultReader::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSReadableStreamDefaultReader::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamDefaultReader.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamDefaultReader = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableStreamDefaultReader.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamDefaultReader = std::forward(space); }); -} - -} diff --git a/src/jsc/bindings/webcore/JSReadableStreamDefaultReader.h b/src/jsc/bindings/webcore/JSReadableStreamDefaultReader.h deleted file mode 100644 index 4178cf2be986..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamDefaultReader.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSReadableStreamDefaultReader : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSReadableStreamDefaultReader* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSReadableStreamDefaultReader* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableStreamDefaultReader(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSReadableStreamDefaultReader(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStreamSink.cpp b/src/jsc/bindings/webcore/JSReadableStreamSink.cpp deleted file mode 100644 index f16957ea9fe8..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamSink.cpp +++ /dev/null @@ -1,245 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableStreamSink.h" - -#include "ActiveDOMObject.h" -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "IDLTypes.h" -#include "JSDOMBinding.h" -#include "JSDOMConvertBase.h" -#include "JSDOMConvertBufferSource.h" -#include "JSDOMConvertStrings.h" -#include "JSDOMConvertUnion.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "ScriptExecutionContext.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace WebCore { -using namespace JSC; - -// Functions - -static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamSinkPrototypeFunction_enqueue); -static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamSinkPrototypeFunction_close); -static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamSinkPrototypeFunction_error); - -class JSReadableStreamSinkPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableStreamSinkPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableStreamSinkPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamSinkPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamSinkPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableStreamSinkPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamSinkPrototype, JSReadableStreamSinkPrototype::Base); - -/* Hash table for prototype */ - -static const HashTableValue JSReadableStreamSinkPrototypeTableValues[] = { - { "enqueue"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamSinkPrototypeFunction_enqueue, 1 } }, - { "close"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamSinkPrototypeFunction_close, 0 } }, - { "error"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamSinkPrototypeFunction_error, 1 } }, -}; - -const ClassInfo JSReadableStreamSinkPrototype::s_info = { "ReadableStreamSink"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamSinkPrototype) }; - -void JSReadableStreamSinkPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSReadableStreamSink::info(), JSReadableStreamSinkPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSReadableStreamSink::s_info = { "ReadableStreamSink"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamSink) }; - -JSReadableStreamSink::JSReadableStreamSink(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) - : JSDOMWrapper(structure, globalObject, WTF::move(impl)) -{ -} - -void JSReadableStreamSink::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); - - // static_assert(!std::is_base_of::value, "Interface is not marked as [ActiveDOMObject] even though implementation class subclasses ActiveDOMObject."); -} - -JSObject* JSReadableStreamSink::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableStreamSinkPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableStreamSinkPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableStreamSink::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -void JSReadableStreamSink::destroy(JSC::JSCell* cell) -{ - JSReadableStreamSink* thisObject = static_cast(cell); - thisObject->JSReadableStreamSink::~JSReadableStreamSink(); -} - -static inline JSC::EncodedJSValue jsReadableStreamSinkPrototypeFunction_enqueueBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); - if (callFrame->argumentCount() < 1) [[unlikely]] - return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); - EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); - auto chunk = convert>(*lexicalGlobalObject, argument0.value()); - RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.enqueue(WTF::move(chunk)); }))); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamSinkPrototypeFunction_enqueue, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "enqueue"); -} - -static inline JSC::EncodedJSValue jsReadableStreamSinkPrototypeFunction_closeBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); - RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.close(); }))); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamSinkPrototypeFunction_close, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "close"); -} - -static inline JSC::EncodedJSValue jsReadableStreamSinkPrototypeFunction_errorBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); - if (callFrame->argumentCount() < 1) [[unlikely]] - return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); - EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); - auto message = convert(*lexicalGlobalObject, argument0.value()); - RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.error(WTF::move(message)); }))); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamSinkPrototypeFunction_error, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "error"); -} - -JSC::GCClient::IsoSubspace* JSReadableStreamSink::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamSink.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamSink = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableStreamSink.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamSink = std::forward(space); }); -} - -void JSReadableStreamSink::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) -{ - auto* thisObject = uncheckedDowncast(cell); - analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); - if (thisObject->scriptExecutionContext()) - analyzer.setLabelForCell(cell, makeString("url "_s, thisObject->scriptExecutionContext()->url().string())); - Base::analyzeHeap(cell, analyzer); -} - -bool JSReadableStreamSinkOwner::isReachableFromOpaqueRoots(JSC::Handle handle, void*, AbstractSlotVisitor& visitor, ASCIILiteral* reason) -{ - UNUSED_PARAM(handle); - UNUSED_PARAM(visitor); - UNUSED_PARAM(reason); - return false; -} - -void JSReadableStreamSinkOwner::finalize(JSC::Handle handle, void* context) -{ - auto* jsReadableStreamSink = static_cast(handle.slot()->asCell()); - auto& world = *static_cast(context); - uncacheWrapper(world, &jsReadableStreamSink->wrapped(), jsReadableStreamSink); -} - -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) -{ - return createWrapper(globalObject, WTF::move(impl)); -} - -JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, ReadableStreamSink& impl) -{ - return wrap(lexicalGlobalObject, globalObject, impl); -} - -ReadableStreamSink* JSReadableStreamSink::toWrapped(JSC::VM&, JSC::JSValue value) -{ - if (auto* wrapper = dynamicDowncast(value)) - return &wrapper->wrapped(); - return nullptr; -} - -} diff --git a/src/jsc/bindings/webcore/JSReadableStreamSink.h b/src/jsc/bindings/webcore/JSReadableStreamSink.h deleted file mode 100644 index e029d9ac31fc..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamSink.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" -#include "ReadableStreamSink.h" -#include - -namespace WebCore { - -class JSReadableStreamSink : public JSDOMWrapper { -public: - using Base = JSDOMWrapper; - static JSReadableStreamSink* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) - { - JSReadableStreamSink* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableStreamSink(structure, *globalObject, WTF::move(impl)); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static ReadableStreamSink* toWrapped(JSC::VM&, JSC::JSValue); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); - -protected: - JSReadableStreamSink(JSC::Structure*, JSDOMGlobalObject&, Ref&&); - - void finishCreation(JSC::VM&); -}; - -class JSReadableStreamSinkOwner final : public JSC::WeakHandleOwner { -public: - bool isReachableFromOpaqueRoots(JSC::Handle, void* context, JSC::AbstractSlotVisitor&, ASCIILiteral*) final; - void finalize(JSC::Handle, void* context) final; -}; - -inline JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, ReadableStreamSink*) -{ - static NeverDestroyed owner; - return &owner.get(); -} - -inline void* wrapperKey(ReadableStreamSink* wrappableObject) -{ - return wrappableObject; -} - -JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, ReadableStreamSink&); -inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, ReadableStreamSink* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&&); -inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, RefPtr&& impl) { return impl ? toJSNewlyCreated(lexicalGlobalObject, globalObject, impl.releaseNonNull()) : JSC::jsNull(); } - -template<> struct JSDOMWrapperConverterTraits { - using WrapperClass = JSReadableStreamSink; - using ToWrappedReturnType = ReadableStreamSink*; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStreamSource.cpp b/src/jsc/bindings/webcore/JSReadableStreamSource.cpp deleted file mode 100644 index 9b8695a6678d..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamSource.cpp +++ /dev/null @@ -1,270 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSReadableStreamSource.h" - -#include "ActiveDOMObject.h" -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "IDLTypes.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMConvertAny.h" -#include "JSDOMConvertBase.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMOperation.h" -#include "JSDOMOperationReturningPromise.h" -#include "JSDOMWrapperCache.h" -#include "ScriptExecutionContext.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace WebCore { -using namespace JSC; - -// Functions - -static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamSourcePrototypeFunction_start); -static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamSourcePrototypeFunction_pull); -static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamSourcePrototypeFunction_cancel); - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamSource_controller); - -class JSReadableStreamSourcePrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSReadableStreamSourcePrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSReadableStreamSourcePrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamSourcePrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamSourcePrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSReadableStreamSourcePrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamSourcePrototype, JSReadableStreamSourcePrototype::Base); - -/* Hash table for prototype */ - -static const HashTableValue JSReadableStreamSourcePrototypeTableValues[] = { - { "controller"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamSource_controller, 0 } }, - { "start"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamSourcePrototypeFunction_start, 1 } }, - { "pull"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamSourcePrototypeFunction_pull, 1 } }, - { "cancel"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamSourcePrototypeFunction_cancel, 1 } }, -}; - -const ClassInfo JSReadableStreamSourcePrototype::s_info = { "ReadableStreamSource"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamSourcePrototype) }; - -void JSReadableStreamSourcePrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - // -- BUN ADDITION -- - auto clientData = WebCore::clientData(vm); - this->putDirect(vm, clientData->builtinNames().bunNativePtrPrivateName(), jsNumber(0), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - this->putDirect(vm, clientData->builtinNames().bunNativeTypePrivateName(), jsNumber(0), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete | 0); - // -- BUN ADDITION -- - - reifyStaticProperties(vm, JSReadableStreamSource::info(), JSReadableStreamSourcePrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSReadableStreamSource::s_info = { "ReadableStreamSource"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamSource) }; - -JSReadableStreamSource::JSReadableStreamSource(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) - : JSDOMWrapper(structure, globalObject, WTF::move(impl)) -{ -} - -void JSReadableStreamSource::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); - - // static_assert(!std::is_base_of::value, "Interface is not marked as [ActiveDOMObject] even though implementation class subclasses ActiveDOMObject."); -} - -JSObject* JSReadableStreamSource::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSReadableStreamSourcePrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSReadableStreamSourcePrototype::create(vm, &globalObject, structure); -} - -JSObject* JSReadableStreamSource::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -void JSReadableStreamSource::destroy(JSC::JSCell* cell) -{ - JSReadableStreamSource* thisObject = static_cast(cell); - thisObject->JSReadableStreamSource::~JSReadableStreamSource(); -} - -static inline JSValue jsReadableStreamSource_controllerGetter(JSGlobalObject& lexicalGlobalObject, JSReadableStreamSource& thisObject) -{ - UNUSED_PARAM(lexicalGlobalObject); - return thisObject.controller(lexicalGlobalObject); -} - -JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamSource_controller, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName)) -{ - return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); -} - -static inline JSC::EncodedJSValue jsReadableStreamSourcePrototypeFunction_startBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperationReturningPromise::ClassParameter castedThis, Ref&& promise) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - RELEASE_AND_RETURN(throwScope, (JSValue::encode(castedThis->start(*lexicalGlobalObject, *callFrame, WTF::move(promise))))); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamSourcePrototypeFunction_start, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperationReturningPromise::call(*lexicalGlobalObject, *callFrame, "start"); -} - -static inline JSC::EncodedJSValue jsReadableStreamSourcePrototypeFunction_pullBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperationReturningPromise::ClassParameter castedThis, Ref&& promise) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - RELEASE_AND_RETURN(throwScope, (JSValue::encode(castedThis->pull(*lexicalGlobalObject, *callFrame, WTF::move(promise))))); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamSourcePrototypeFunction_pull, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperationReturningPromise::call(*lexicalGlobalObject, *callFrame, "pull"); -} - -static inline JSC::EncodedJSValue jsReadableStreamSourcePrototypeFunction_cancelBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); - if (callFrame->argumentCount() < 1) [[unlikely]] - return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); - EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); - auto reason = convert(*lexicalGlobalObject, argument0.value()); - RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.cancel(WTF::move(reason)); }))); -} - -JSC_DEFINE_HOST_FUNCTION(jsReadableStreamSourcePrototypeFunction_cancel, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "cancel"); -} - -JSC::GCClient::IsoSubspace* JSReadableStreamSource::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamSource.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamSource = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForReadableStreamSource.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamSource = std::forward(space); }); -} - -template -void JSReadableStreamSource::visitChildrenImpl(JSCell* cell, Visitor& visitor) -{ - auto* thisObject = uncheckedDowncast(cell); - ASSERT_GC_OBJECT_INHERITS(thisObject, info()); - Base::visitChildren(thisObject, visitor); - visitor.append(thisObject->m_controller); -} - -DEFINE_VISIT_CHILDREN(JSReadableStreamSource); - -void JSReadableStreamSource::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) -{ - auto* thisObject = uncheckedDowncast(cell); - analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); - if (thisObject->scriptExecutionContext()) - analyzer.setLabelForCell(cell, makeString("url "_s, thisObject->scriptExecutionContext()->url().string())); - Base::analyzeHeap(cell, analyzer); -} - -bool JSReadableStreamSourceOwner::isReachableFromOpaqueRoots(JSC::Handle handle, void*, AbstractSlotVisitor& visitor, ASCIILiteral* reason) -{ - UNUSED_PARAM(handle); - UNUSED_PARAM(visitor); - UNUSED_PARAM(reason); - return false; -} - -void JSReadableStreamSourceOwner::finalize(JSC::Handle handle, void* context) -{ - auto* jsReadableStreamSource = static_cast(handle.slot()->asCell()); - auto& world = *static_cast(context); - uncacheWrapper(world, &jsReadableStreamSource->wrapped(), jsReadableStreamSource); -} - -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) -{ - return createWrapper(globalObject, WTF::move(impl)); -} - -JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, ReadableStreamSource& impl) -{ - return wrap(lexicalGlobalObject, globalObject, impl); -} - -ReadableStreamSource* JSReadableStreamSource::toWrapped(JSC::VM&, JSC::JSValue value) -{ - if (auto* wrapper = dynamicDowncast(value)) - return &wrapper->wrapped(); - return nullptr; -} - -} diff --git a/src/jsc/bindings/webcore/JSReadableStreamSource.h b/src/jsc/bindings/webcore/JSReadableStreamSource.h deleted file mode 100644 index eb26e8018b99..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamSource.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" -#include "ReadableStreamSource.h" -#include - -namespace WebCore { - -class JSReadableStreamSource : public JSDOMWrapper { -public: - using Base = JSDOMWrapper; - static JSReadableStreamSource* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) - { - JSReadableStreamSource* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSReadableStreamSource(structure, *globalObject, WTF::move(impl)); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static ReadableStreamSource* toWrapped(JSC::VM&, JSC::JSValue); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - mutable JSC::WriteBarrier m_controller; - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - DECLARE_VISIT_CHILDREN; - - static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); - - // Custom attributes - JSC::JSValue controller(JSC::JSGlobalObject&) const; - - // Custom functions - JSC::JSValue start(JSC::JSGlobalObject&, JSC::CallFrame&, Ref&&); - JSC::JSValue pull(JSC::JSGlobalObject&, JSC::CallFrame&, Ref&&); - -protected: - JSReadableStreamSource(JSC::Structure*, JSDOMGlobalObject&, Ref&&); - - void finishCreation(JSC::VM&); -}; - -class JSReadableStreamSourceOwner final : public JSC::WeakHandleOwner { -public: - bool isReachableFromOpaqueRoots(JSC::Handle, void* context, JSC::AbstractSlotVisitor&, ASCIILiteral*) final; - void finalize(JSC::Handle, void* context) final; -}; - -inline JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, ReadableStreamSource*) -{ - static NeverDestroyed owner; - return &owner.get(); -} - -inline void* wrapperKey(ReadableStreamSource* wrappableObject) -{ - return wrappableObject; -} - -JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, ReadableStreamSource&); -inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, ReadableStreamSource* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&&); -inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, RefPtr&& impl) { return impl ? toJSNewlyCreated(lexicalGlobalObject, globalObject, impl.releaseNonNull()) : JSC::jsNull(); } - -template<> struct JSDOMWrapperConverterTraits { - using WrapperClass = JSReadableStreamSource; - using ToWrappedReturnType = ReadableStreamSource*; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSReadableStreamSourceCustom.cpp b/src/jsc/bindings/webcore/JSReadableStreamSourceCustom.cpp deleted file mode 100644 index beffb62ea640..000000000000 --- a/src/jsc/bindings/webcore/JSReadableStreamSourceCustom.cpp +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2016 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted, provided that the following conditions - * are required to be met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of Canon Inc. nor the names of - * its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "config.h" -#include "JSReadableStreamSource.h" - -#include "JSDOMPromiseDeferred.h" - -namespace WebCore { -using namespace JSC; - -JSValue JSReadableStreamSource::start(JSGlobalObject& lexicalGlobalObject, CallFrame& callFrame, Ref&& promise) -{ - VM& vm = lexicalGlobalObject.vm(); - - // FIXME: Why is it ok to ASSERT the argument count here? - ASSERT(callFrame.argumentCount()); - JSReadableStreamDefaultController* controller = dynamicDowncast(callFrame.uncheckedArgument(0)); - ASSERT(controller); - - m_controller.set(vm, this, controller); - - wrapped().start(ReadableStreamDefaultController(controller), WTF::move(promise)); - - return jsUndefined(); -} - -JSValue JSReadableStreamSource::pull(JSGlobalObject&, CallFrame&, Ref&& promise) -{ - wrapped().pull(WTF::move(promise)); - return jsUndefined(); -} - -JSValue JSReadableStreamSource::controller(JSGlobalObject&) const -{ - ASSERT_NOT_REACHED(); - return jsUndefined(); -} - -} diff --git a/src/jsc/bindings/webcore/JSTextDecoderStream.cpp b/src/jsc/bindings/webcore/JSTextDecoderStream.cpp deleted file mode 100644 index 9e9b5b8b3633..000000000000 --- a/src/jsc/bindings/webcore/JSTextDecoderStream.cpp +++ /dev/null @@ -1,172 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSTextDecoderStream.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMWrapperCache.h" -// #include "TextDecoderStreamBuiltins.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsTextDecoderStreamConstructor); - -class JSTextDecoderStreamPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSTextDecoderStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSTextDecoderStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTextDecoderStreamPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextDecoderStreamPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSTextDecoderStreamPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextDecoderStreamPrototype, JSTextDecoderStreamPrototype::Base); - -using JSTextDecoderStreamDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSTextDecoderStreamDOMConstructor::s_info = { "TextDecoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextDecoderStreamDOMConstructor) }; - -template<> JSValue JSTextDecoderStreamDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSTextDecoderStreamDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "TextDecoderStream"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSTextDecoderStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSTextDecoderStreamDOMConstructor::initializeExecutable(VM& vm) -{ - return textDecoderStreamInitializeTextDecoderStreamCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSTextDecoderStreamPrototypeTableValues[] = { - { "constructor"_s, static_cast(PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextDecoderStreamConstructor, 0 } }, - { "encoding"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textDecoderStreamEncodingCodeGenerator, 0 } }, - { "fatal"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textDecoderStreamFatalCodeGenerator, 0 } }, - { "ignoreBOM"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textDecoderStreamIgnoreBOMCodeGenerator, 0 } }, - { "readable"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textDecoderStreamReadableCodeGenerator, 0 } }, - { "writable"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textDecoderStreamWritableCodeGenerator, 0 } }, -}; - -const ClassInfo JSTextDecoderStreamPrototype::s_info = { "TextDecoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextDecoderStreamPrototype) }; - -void JSTextDecoderStreamPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSTextDecoderStream::info(), JSTextDecoderStreamPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSTextDecoderStream::s_info = { "TextDecoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextDecoderStream) }; - -JSTextDecoderStream::JSTextDecoderStream(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -JSObject* JSTextDecoderStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSTextDecoderStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSTextDecoderStreamPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSTextDecoderStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSTextDecoderStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSTextDecoderStream::destroy(JSC::JSCell* cell) -{ - JSTextDecoderStream* thisObject = static_cast(cell); - thisObject->JSTextDecoderStream::~JSTextDecoderStream(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsTextDecoderStreamConstructor, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSTextDecoderStream::getConstructor(vm, prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSTextDecoderStream::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, [](auto& spaces) { return spaces.m_clientSubspaceForTextDecoderStream.get(); }, [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTextDecoderStream = std::forward(space); }, [](auto& spaces) { return spaces.m_subspaceForTextDecoderStream.get(); }, [](auto& spaces, auto&& space) { spaces.m_subspaceForTextDecoderStream = std::forward(space); }); -} - -} diff --git a/src/jsc/bindings/webcore/JSTextDecoderStream.h b/src/jsc/bindings/webcore/JSTextDecoderStream.h deleted file mode 100644 index 34de4e992ac0..000000000000 --- a/src/jsc/bindings/webcore/JSTextDecoderStream.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSTextDecoderStream : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSTextDecoderStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - auto& vm = JSC::getVM(globalObject); - JSTextDecoderStream* ptr = new (NotNull, JSC::allocateCell(vm)) JSTextDecoderStream(structure, *globalObject); - ptr->finishCreation(vm); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSTextDecoderStream(JSC::Structure*, JSDOMGlobalObject&); - - DECLARE_DEFAULT_FINISH_CREATION; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSTextEncoderStream.cpp b/src/jsc/bindings/webcore/JSTextEncoderStream.cpp deleted file mode 100644 index b26c7bd99850..000000000000 --- a/src/jsc/bindings/webcore/JSTextEncoderStream.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSTextEncoderStream.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMWrapperCache.h" -// #include "TextEncoderStreamBuiltins.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsTextEncoderStreamConstructor); - -class JSTextEncoderStreamPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSTextEncoderStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSTextEncoderStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTextEncoderStreamPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextEncoderStreamPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSTextEncoderStreamPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextEncoderStreamPrototype, JSTextEncoderStreamPrototype::Base); - -using JSTextEncoderStreamDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSTextEncoderStreamDOMConstructor::s_info = { "TextEncoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextEncoderStreamDOMConstructor) }; - -template<> JSValue JSTextEncoderStreamDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSTextEncoderStreamDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "TextEncoderStream"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSTextEncoderStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSTextEncoderStreamDOMConstructor::initializeExecutable(VM& vm) -{ - return textEncoderStreamInitializeTextEncoderStreamCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSTextEncoderStreamPrototypeTableValues[] = { - { "constructor"_s, static_cast(PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextEncoderStreamConstructor, 0 } }, - { "encoding"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textEncoderStreamEncodingCodeGenerator, 0 } }, - { "readable"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textEncoderStreamReadableCodeGenerator, 0 } }, - { "writable"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin, NoIntrinsic, { HashTableValue::BuiltinAccessorType, textEncoderStreamWritableCodeGenerator, 0 } }, -}; - -const ClassInfo JSTextEncoderStreamPrototype::s_info = { "TextEncoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextEncoderStreamPrototype) }; - -void JSTextEncoderStreamPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSTextEncoderStream::info(), JSTextEncoderStreamPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSTextEncoderStream::s_info = { "TextEncoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextEncoderStream) }; - -JSTextEncoderStream::JSTextEncoderStream(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -JSObject* JSTextEncoderStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSTextEncoderStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSTextEncoderStreamPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSTextEncoderStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSTextEncoderStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSTextEncoderStream::destroy(JSC::JSCell* cell) -{ - JSTextEncoderStream* thisObject = static_cast(cell); - thisObject->JSTextEncoderStream::~JSTextEncoderStream(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsTextEncoderStreamConstructor, (JSGlobalObject * lexicalGlobalObject, EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSTextEncoderStream::getConstructor(vm, prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSTextEncoderStream::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, [](auto& spaces) { return spaces.m_clientSubspaceForTextEncoderStream.get(); }, [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTextEncoderStream = std::forward(space); }, [](auto& spaces) { return spaces.m_subspaceForTextEncoderStream.get(); }, [](auto& spaces, auto&& space) { spaces.m_subspaceForTextEncoderStream = std::forward(space); }); -} - -} diff --git a/src/jsc/bindings/webcore/JSTextEncoderStream.h b/src/jsc/bindings/webcore/JSTextEncoderStream.h deleted file mode 100644 index 3ad0efb3d194..000000000000 --- a/src/jsc/bindings/webcore/JSTextEncoderStream.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSTextEncoderStream : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSTextEncoderStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - auto& vm = JSC::getVM(globalObject); - JSTextEncoderStream* ptr = new (NotNull, JSC::allocateCell(vm)) JSTextEncoderStream(structure, *globalObject); - ptr->finishCreation(vm); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSTextEncoderStream(JSC::Structure*, JSDOMGlobalObject&); - - DECLARE_DEFAULT_FINISH_CREATION; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSTransformStream.cpp b/src/jsc/bindings/webcore/JSTransformStream.cpp deleted file mode 100644 index 7a995341d8be..000000000000 --- a/src/jsc/bindings/webcore/JSTransformStream.cpp +++ /dev/null @@ -1,178 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSTransformStream.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsTransformStreamConstructor); - -class JSTransformStreamPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSTransformStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSTransformStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTransformStreamPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSTransformStreamPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamPrototype, JSTransformStreamPrototype::Base); - -using JSTransformStreamDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSTransformStreamDOMConstructor::s_info = { "TransformStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamDOMConstructor) }; - -template<> JSValue JSTransformStreamDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSTransformStreamDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "TransformStream"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSTransformStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSTransformStreamDOMConstructor::initializeExecutable(VM& vm) -{ - return transformStreamInitializeTransformStreamCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSTransformStreamPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTransformStreamConstructor, 0 } }, - { "readable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, transformStreamReadableCodeGenerator, 0 } }, - { "writable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, transformStreamWritableCodeGenerator, 0 } }, -}; - -const ClassInfo JSTransformStreamPrototype::s_info = { "TransformStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamPrototype) }; - -void JSTransformStreamPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSTransformStream::info(), JSTransformStreamPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSTransformStream::s_info = { "TransformStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStream) }; - -JSTransformStream::JSTransformStream(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSTransformStream::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSTransformStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSTransformStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSTransformStreamPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSTransformStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSTransformStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSTransformStream::destroy(JSC::JSCell* cell) -{ - JSTransformStream* thisObject = static_cast(cell); - thisObject->JSTransformStream::~JSTransformStream(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsTransformStreamConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSTransformStream::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSTransformStream::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForTransformStream.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTransformStream = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForTransformStream.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForTransformStream = std::forward(space); }); -} - -} diff --git a/src/jsc/bindings/webcore/JSTransformStream.h b/src/jsc/bindings/webcore/JSTransformStream.h deleted file mode 100644 index a68e7da4761d..000000000000 --- a/src/jsc/bindings/webcore/JSTransformStream.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSTransformStream : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSTransformStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSTransformStream* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSTransformStream(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSTransformStream(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSTransformStreamDefaultController.cpp b/src/jsc/bindings/webcore/JSTransformStreamDefaultController.cpp deleted file mode 100644 index 6aba145012e9..000000000000 --- a/src/jsc/bindings/webcore/JSTransformStreamDefaultController.cpp +++ /dev/null @@ -1,182 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSTransformStreamDefaultController.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsTransformStreamDefaultControllerConstructor); - -class JSTransformStreamDefaultControllerPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSTransformStreamDefaultControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSTransformStreamDefaultControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTransformStreamDefaultControllerPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamDefaultControllerPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSTransformStreamDefaultControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamDefaultControllerPrototype, JSTransformStreamDefaultControllerPrototype::Base); - -using JSTransformStreamDefaultControllerDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSTransformStreamDefaultControllerDOMConstructor::s_info = { "TransformStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamDefaultControllerDOMConstructor) }; - -template<> JSValue JSTransformStreamDefaultControllerDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSTransformStreamDefaultControllerDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "TransformStreamDefaultController"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSTransformStreamDefaultController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSTransformStreamDefaultControllerDOMConstructor::initializeExecutable(VM& vm) -{ - return transformStreamDefaultControllerInitializeTransformStreamDefaultControllerCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSTransformStreamDefaultControllerPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTransformStreamDefaultControllerConstructor, 0 } }, - { "desiredSize"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, transformStreamDefaultControllerDesiredSizeCodeGenerator, 0 } }, - { "enqueue"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, transformStreamDefaultControllerEnqueueCodeGenerator, 0 } }, - { "error"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, transformStreamDefaultControllerErrorCodeGenerator, 0 } }, - { "terminate"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, transformStreamDefaultControllerTerminateCodeGenerator, 0 } }, -}; - -const ClassInfo JSTransformStreamDefaultControllerPrototype::s_info = { "TransformStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamDefaultControllerPrototype) }; - -void JSTransformStreamDefaultControllerPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSTransformStreamDefaultController::info(), JSTransformStreamDefaultControllerPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSTransformStreamDefaultController::s_info = { "TransformStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamDefaultController) }; - -JSTransformStreamDefaultController::JSTransformStreamDefaultController(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSTransformStreamDefaultController::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSTransformStreamDefaultController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSTransformStreamDefaultControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSTransformStreamDefaultControllerPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSTransformStreamDefaultController::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSTransformStreamDefaultController::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSTransformStreamDefaultController::destroy(JSC::JSCell* cell) -{ - JSTransformStreamDefaultController* thisObject = static_cast(cell); - thisObject->JSTransformStreamDefaultController::~JSTransformStreamDefaultController(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsTransformStreamDefaultControllerConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSTransformStreamDefaultController::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSTransformStreamDefaultController::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForTransformStreamDefaultController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTransformStreamDefaultController = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForTransformStreamDefaultController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForTransformStreamDefaultController = std::forward(space); }); -} -} diff --git a/src/jsc/bindings/webcore/JSTransformStreamDefaultController.h b/src/jsc/bindings/webcore/JSTransformStreamDefaultController.h deleted file mode 100644 index 9fe4c0568fe3..000000000000 --- a/src/jsc/bindings/webcore/JSTransformStreamDefaultController.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSTransformStreamDefaultController : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSTransformStreamDefaultController* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSTransformStreamDefaultController* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSTransformStreamDefaultController(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSTransformStreamDefaultController(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSWritableStream.cpp b/src/jsc/bindings/webcore/JSWritableStream.cpp deleted file mode 100644 index 1ed046e0067e..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStream.cpp +++ /dev/null @@ -1,347 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSWritableStream.h" - -#include "ActiveDOMObject.h" -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMConstructor.h" -#include "JSDOMConvertBoolean.h" -#include "JSDOMConvertInterface.h" -#include "JSDOMConvertObject.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMOperationReturningPromise.h" -#include "JSDOMWrapperCache.h" -#include "ScriptExecutionContext.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace WebCore { -using namespace JSC; - -// Functions - -static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_abort); -static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_close); -static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_getWriter); - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamConstructor); -static JSC_DECLARE_CUSTOM_GETTER(jsWritableStream_locked); - -class JSWritableStreamPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSWritableStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSWritableStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSWritableStreamPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamPrototype, JSWritableStreamPrototype::Base); - -using JSWritableStreamDOMConstructor = JSDOMConstructor; - -template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWritableStreamDOMConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* castedThis = uncheckedDowncast(callFrame->jsCallee()); - ASSERT(castedThis); - EnsureStillAliveScope argument0 = callFrame->argument(0); - auto underlyingSink = argument0.value().isUndefined() ? std::optional::ReturnType>() : std::optional::ReturnType>(convert(*lexicalGlobalObject, argument0.value())); - RETURN_IF_EXCEPTION(throwScope, {}); - EnsureStillAliveScope argument1 = callFrame->argument(1); - auto strategy = argument1.value().isUndefined() ? std::optional::ReturnType>() : std::optional::ReturnType>(convert(*lexicalGlobalObject, argument1.value())); - RETURN_IF_EXCEPTION(throwScope, {}); - auto object = WritableStream::create(*castedThis->globalObject(), WTF::move(underlyingSink), WTF::move(strategy)); - if constexpr (IsExceptionOr) - RETURN_IF_EXCEPTION(throwScope, {}); - static_assert(TypeOrExceptionOrUnderlyingType::isRef); - auto jsValue = toJSNewlyCreated>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, WTF::move(object)); - if constexpr (IsExceptionOr) - RETURN_IF_EXCEPTION(throwScope, {}); - setSubclassStructureIfNeeded(lexicalGlobalObject, callFrame, asObject(jsValue)); - RETURN_IF_EXCEPTION(throwScope, {}); - return JSValue::encode(jsValue); -} -JSC_ANNOTATE_HOST_FUNCTION(JSWritableStreamDOMConstructorConstruct, JSWritableStreamDOMConstructor::construct); - -template<> const ClassInfo JSWritableStreamDOMConstructor::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDOMConstructor) }; - -template<> JSValue JSWritableStreamDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSWritableStreamDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "WritableStream"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSWritableStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -/* Hash table for prototype */ - -static const HashTableValue JSWritableStreamPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamConstructor, 0 } }, - { "locked"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStream_locked, 0 } }, - { "abort"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_abort, 0 } }, - { "close"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_close, 0 } }, - { "getWriter"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_getWriter, 0 } }, -}; - -const ClassInfo JSWritableStreamPrototype::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamPrototype) }; - -void JSWritableStreamPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSWritableStream::info(), JSWritableStreamPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSWritableStream::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStream) }; - -JSWritableStream::JSWritableStream(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) - : JSDOMWrapper(structure, globalObject, WTF::move(impl)) -{ -} - -void JSWritableStream::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); - - // static_assert(!std::is_base_of::value, "Interface is not marked as [ActiveDOMObject] even though implementation class subclasses ActiveDOMObject."); -} - -JSObject* JSWritableStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSWritableStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSWritableStreamPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSWritableStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSWritableStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSWritableStream::destroy(JSC::JSCell* cell) -{ - JSWritableStream* thisObject = static_cast(cell); - thisObject->JSWritableStream::~JSWritableStream(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSWritableStream::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -static inline JSValue jsWritableStream_lockedGetter(JSGlobalObject& lexicalGlobalObject, JSWritableStream& thisObject) -{ - auto& vm = JSC::getVM(&lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto& impl = thisObject.wrapped(); - RELEASE_AND_RETURN(throwScope, (toJS(lexicalGlobalObject, throwScope, impl.locked()))); -} - -JSC_DEFINE_CUSTOM_GETTER(jsWritableStream_locked, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName attributeName)) -{ - return IDLAttribute::get(*lexicalGlobalObject, thisValue, attributeName); -} - -static inline JSC::EncodedJSValue jsWritableStreamPrototypeFunction_abortBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperationReturningPromise::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - RELEASE_AND_RETURN(throwScope, (JSValue::encode(castedThis->abort(*lexicalGlobalObject, *callFrame)))); -} - -JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_abort, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperationReturningPromise::callReturningOwnPromise(*lexicalGlobalObject, *callFrame, "abort"); -} - -static inline JSC::EncodedJSValue jsWritableStreamPrototypeFunction_closeBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperationReturningPromise::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - RELEASE_AND_RETURN(throwScope, (JSValue::encode(castedThis->close(*lexicalGlobalObject, *callFrame)))); -} - -JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_close, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperationReturningPromise::callReturningOwnPromise(*lexicalGlobalObject, *callFrame, "close"); -} - -static inline JSC::EncodedJSValue jsWritableStreamPrototypeFunction_getWriterBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - RELEASE_AND_RETURN(throwScope, (JSValue::encode(castedThis->getWriter(*lexicalGlobalObject, *callFrame)))); -} - -JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_getWriter, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "getWriter"); -} - -JSC::GCClient::IsoSubspace* JSWritableStream::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForWritableStream.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStream = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForWritableStream.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStream = std::forward(space); }); -} - -template -void JSWritableStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) -{ - auto* thisObject = uncheckedDowncast(cell); - ASSERT_GC_OBJECT_INHERITS(thisObject, info()); - Base::visitChildren(thisObject, visitor); - thisObject->visitAdditionalChildrenInGCThread(visitor); -} - -DEFINE_VISIT_CHILDREN(JSWritableStream); - -template -void JSWritableStream::visitOutputConstraints(JSCell* cell, Visitor& visitor) -{ - auto* thisObject = uncheckedDowncast(cell); - ASSERT_GC_OBJECT_INHERITS(thisObject, info()); - Base::visitOutputConstraints(thisObject, visitor); - thisObject->visitAdditionalChildrenInGCThread(visitor); -} - -template void JSWritableStream::visitOutputConstraints(JSCell*, AbstractSlotVisitor&); -template void JSWritableStream::visitOutputConstraints(JSCell*, SlotVisitor&); - -template -void JSWritableStream::visitAdditionalChildrenInGCThread(Visitor& visitor) -{ - // InternalWritableStream opts out of the global object's m_guardedObjects - // root set (see InternalWritableStream ctor), so the JS wrapper is - // responsible for keeping the internal stream object reachable. - wrapped().internalWritableStream().visitAggregate(visitor); -} - -DEFINE_VISIT_ADDITIONAL_CHILDREN_IN_GC_THREAD(JSWritableStream); - -void JSWritableStream::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) -{ - auto* thisObject = uncheckedDowncast(cell); - analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); - if (thisObject->scriptExecutionContext()) - analyzer.setLabelForCell(cell, makeString("url "_s, thisObject->scriptExecutionContext()->url().string())); - Base::analyzeHeap(cell, analyzer); -} - -bool JSWritableStreamOwner::isReachableFromOpaqueRoots(JSC::Handle handle, void*, AbstractSlotVisitor& visitor, ASCIILiteral* reason) -{ - UNUSED_PARAM(handle); - UNUSED_PARAM(visitor); - UNUSED_PARAM(reason); - return false; -} - -void JSWritableStreamOwner::finalize(JSC::Handle handle, void* context) -{ - auto* jsWritableStream = static_cast(handle.slot()->asCell()); - auto& world = *static_cast(context); - uncacheWrapper(world, &jsWritableStream->wrapped(), jsWritableStream); -} - -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) -{ - return createWrapper(globalObject, WTF::move(impl)); -} - -JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, WritableStream& impl) -{ - return wrap(lexicalGlobalObject, globalObject, impl); -} - -WritableStream* JSWritableStream::toWrapped(JSC::VM&, JSC::JSValue value) -{ - if (auto* wrapper = dynamicDowncast(value)) - return &wrapper->wrapped(); - return nullptr; -} - -} diff --git a/src/jsc/bindings/webcore/JSWritableStream.h b/src/jsc/bindings/webcore/JSWritableStream.h deleted file mode 100644 index c7b42f68cb20..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStream.h +++ /dev/null @@ -1,103 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" -#include "WritableStream.h" -#include - -namespace WebCore { - -class JSWritableStream : public JSDOMWrapper { -public: - using Base = JSDOMWrapper; - static JSWritableStream* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) - { - JSWritableStream* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSWritableStream(structure, *globalObject, WTF::move(impl)); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static WritableStream* toWrapped(JSC::VM&, JSC::JSValue); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - DECLARE_VISIT_CHILDREN; - template void visitAdditionalChildrenInGCThread(Visitor&); - - template static void visitOutputConstraints(JSCell*, Visitor&); - static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); - - // Custom functions - JSC::JSValue abort(JSC::JSGlobalObject&, JSC::CallFrame&); - JSC::JSValue close(JSC::JSGlobalObject&, JSC::CallFrame&); - JSC::JSValue getWriter(JSC::JSGlobalObject&, JSC::CallFrame&); - -protected: - JSWritableStream(JSC::Structure*, JSDOMGlobalObject&, Ref&&); - - void finishCreation(JSC::VM&); -}; - -class JSWritableStreamOwner final : public JSC::WeakHandleOwner { -public: - bool isReachableFromOpaqueRoots(JSC::Handle, void* context, JSC::AbstractSlotVisitor&, ASCIILiteral*) final; - void finalize(JSC::Handle, void* context) final; -}; - -inline JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, WritableStream*) -{ - static NeverDestroyed owner; - return &owner.get(); -} - -inline void* wrapperKey(WritableStream* wrappableObject) -{ - return wrappableObject; -} - -JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, WritableStream&); -inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, WritableStream* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&&); -inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, RefPtr&& impl) { return impl ? toJSNewlyCreated(lexicalGlobalObject, globalObject, impl.releaseNonNull()) : JSC::jsNull(); } - -template<> struct JSDOMWrapperConverterTraits { - using WrapperClass = JSWritableStream; - using ToWrappedReturnType = WritableStream*; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSWritableStreamDefaultController.cpp b/src/jsc/bindings/webcore/JSWritableStreamDefaultController.cpp deleted file mode 100644 index 64acfd382741..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStreamDefaultController.cpp +++ /dev/null @@ -1,179 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSWritableStreamDefaultController.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultControllerConstructor); - -class JSWritableStreamDefaultControllerPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSWritableStreamDefaultControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSWritableStreamDefaultControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamDefaultControllerPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultControllerPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSWritableStreamDefaultControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultControllerPrototype, JSWritableStreamDefaultControllerPrototype::Base); - -using JSWritableStreamDefaultControllerDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSWritableStreamDefaultControllerDOMConstructor::s_info = { "WritableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultControllerDOMConstructor) }; - -template<> JSValue JSWritableStreamDefaultControllerDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSWritableStreamDefaultControllerDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "WritableStreamDefaultController"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSWritableStreamDefaultController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSWritableStreamDefaultControllerDOMConstructor::initializeExecutable(VM& vm) -{ - return writableStreamDefaultControllerInitializeWritableStreamDefaultControllerCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSWritableStreamDefaultControllerPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultControllerConstructor, 0 } }, - { "error"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, writableStreamDefaultControllerErrorCodeGenerator, 0 } }, -}; - -const ClassInfo JSWritableStreamDefaultControllerPrototype::s_info = { "WritableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultControllerPrototype) }; - -void JSWritableStreamDefaultControllerPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSWritableStreamDefaultController::info(), JSWritableStreamDefaultControllerPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSWritableStreamDefaultController::s_info = { "WritableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultController) }; - -JSWritableStreamDefaultController::JSWritableStreamDefaultController(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSWritableStreamDefaultController::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSWritableStreamDefaultController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSWritableStreamDefaultControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSWritableStreamDefaultControllerPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSWritableStreamDefaultController::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSWritableStreamDefaultController::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSWritableStreamDefaultController::destroy(JSC::JSCell* cell) -{ - JSWritableStreamDefaultController* thisObject = static_cast(cell); - thisObject->JSWritableStreamDefaultController::~JSWritableStreamDefaultController(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultControllerConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSWritableStreamDefaultController::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSWritableStreamDefaultController::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForWritableStreamDefaultController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStreamDefaultController = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForWritableStreamDefaultController.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStreamDefaultController = std::forward(space); }); -} - -} diff --git a/src/jsc/bindings/webcore/JSWritableStreamDefaultController.h b/src/jsc/bindings/webcore/JSWritableStreamDefaultController.h deleted file mode 100644 index c695f439b948..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStreamDefaultController.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSWritableStreamDefaultController : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSWritableStreamDefaultController* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSWritableStreamDefaultController* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSWritableStreamDefaultController(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSWritableStreamDefaultController(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSWritableStreamDefaultWriter.cpp b/src/jsc/bindings/webcore/JSWritableStreamDefaultWriter.cpp deleted file mode 100644 index b656be73c50e..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStreamDefaultWriter.cpp +++ /dev/null @@ -1,185 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSWritableStreamDefaultWriter.h" - -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "JSDOMAttribute.h" -#include "JSDOMBinding.h" -#include "JSDOMBuiltinConstructor.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObjectInlines.h" -#include "JSDOMOperation.h" -#include "JSDOMWrapperCache.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include "WebCoreJSBuiltins.h" - -namespace WebCore { -using namespace JSC; - -// Functions - -// Attributes - -static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultWriterConstructor); - -class JSWritableStreamDefaultWriterPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSWritableStreamDefaultWriterPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSWritableStreamDefaultWriterPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamDefaultWriterPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultWriterPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSWritableStreamDefaultWriterPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultWriterPrototype, JSWritableStreamDefaultWriterPrototype::Base); - -using JSWritableStreamDefaultWriterDOMConstructor = JSDOMBuiltinConstructor; - -template<> const ClassInfo JSWritableStreamDefaultWriterDOMConstructor::s_info = { "WritableStreamDefaultWriter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultWriterDOMConstructor) }; - -template<> JSValue JSWritableStreamDefaultWriterDOMConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) -{ - UNUSED_PARAM(vm); - return globalObject.functionPrototype(); -} - -template<> void JSWritableStreamDefaultWriterDOMConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) -{ - putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - JSString* nameString = jsNontrivialString(vm, "WritableStreamDefaultWriter"_s); - m_originalName.set(vm, this, nameString); - putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); - putDirect(vm, vm.propertyNames->prototype, JSWritableStreamDefaultWriter::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); -} - -template<> FunctionExecutable* JSWritableStreamDefaultWriterDOMConstructor::initializeExecutable(VM& vm) -{ - return writableStreamDefaultWriterInitializeWritableStreamDefaultWriterCodeGenerator(vm); -} - -/* Hash table for prototype */ - -static const HashTableValue JSWritableStreamDefaultWriterPrototypeTableValues[] = { - { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultWriterConstructor, 0 } }, - { "closed"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, writableStreamDefaultWriterClosedCodeGenerator, 0 } }, - { "desiredSize"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, writableStreamDefaultWriterDesiredSizeCodeGenerator, 0 } }, - { "ready"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::Accessor | JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinAccessorType, writableStreamDefaultWriterReadyCodeGenerator, 0 } }, - { "abort"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, writableStreamDefaultWriterAbortCodeGenerator, 0 } }, - { "close"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, writableStreamDefaultWriterCloseCodeGenerator, 0 } }, - { "releaseLock"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, writableStreamDefaultWriterReleaseLockCodeGenerator, 0 } }, - { "write"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, writableStreamDefaultWriterWriteCodeGenerator, 0 } }, -}; - -const ClassInfo JSWritableStreamDefaultWriterPrototype::s_info = { "WritableStreamDefaultWriter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultWriterPrototype) }; - -void JSWritableStreamDefaultWriterPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSWritableStreamDefaultWriter::info(), JSWritableStreamDefaultWriterPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSWritableStreamDefaultWriter::s_info = { "WritableStreamDefaultWriter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultWriter) }; - -JSWritableStreamDefaultWriter::JSWritableStreamDefaultWriter(Structure* structure, JSDOMGlobalObject& globalObject) - : JSDOMObject(structure, globalObject) -{ -} - -void JSWritableStreamDefaultWriter::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); -} - -JSObject* JSWritableStreamDefaultWriter::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSWritableStreamDefaultWriterPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSWritableStreamDefaultWriterPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSWritableStreamDefaultWriter::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -JSValue JSWritableStreamDefaultWriter::getConstructor(VM& vm, const JSGlobalObject* globalObject) -{ - return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); -} - -void JSWritableStreamDefaultWriter::destroy(JSC::JSCell* cell) -{ - JSWritableStreamDefaultWriter* thisObject = static_cast(cell); - thisObject->JSWritableStreamDefaultWriter::~JSWritableStreamDefaultWriter(); -} - -JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultWriterConstructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); - if (!prototype) [[unlikely]] - return throwVMTypeError(lexicalGlobalObject, throwScope); - return JSValue::encode(JSWritableStreamDefaultWriter::getConstructor(JSC::getVM(lexicalGlobalObject), prototype->globalObject())); -} - -JSC::GCClient::IsoSubspace* JSWritableStreamDefaultWriter::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForWritableStreamDefaultWriter.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStreamDefaultWriter = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForWritableStreamDefaultWriter.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStreamDefaultWriter = std::forward(space); }); -} -} diff --git a/src/jsc/bindings/webcore/JSWritableStreamDefaultWriter.h b/src/jsc/bindings/webcore/JSWritableStreamDefaultWriter.h deleted file mode 100644 index 3434df33a2e0..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStreamDefaultWriter.h +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" - -namespace WebCore { - -class JSWritableStreamDefaultWriter : public JSDOMObject { -public: - using Base = JSDOMObject; - static JSWritableStreamDefaultWriter* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject) - { - JSWritableStreamDefaultWriter* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSWritableStreamDefaultWriter(structure, *globalObject); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - -protected: - JSWritableStreamDefaultWriter(JSC::Structure*, JSDOMGlobalObject&); - - void finishCreation(JSC::VM&); -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/JSWritableStreamSink.cpp b/src/jsc/bindings/webcore/JSWritableStreamSink.cpp deleted file mode 100644 index 3b158cb6952f..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStreamSink.cpp +++ /dev/null @@ -1,250 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#include "config.h" -#include "JSWritableStreamSink.h" - -#include "ActiveDOMObject.h" -#include "DOMPromiseProxy.h" -#include "ExtendedDOMClientIsoSubspaces.h" -#include "ExtendedDOMIsoSubspaces.h" -#include "IDLTypes.h" -#include "JSDOMBinding.h" -#include "JSDOMConvertAny.h" -#include "JSDOMConvertBase.h" -#include "JSDOMConvertPromise.h" -#include "JSDOMConvertStrings.h" -#include "JSDOMExceptionHandling.h" -#include "JSDOMGlobalObject.h" -#include "JSDOMOperation.h" -#include "JSDOMOperationReturningPromise.h" -#include "JSDOMWrapperCache.h" -#include "ScriptExecutionContext.h" -#include "WebCoreJSClientData.h" -#include -#include -#include -#include -#include -#include -#include -#include - -namespace WebCore { -using namespace JSC; - -// Functions - -static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamSinkPrototypeFunction_write); -static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamSinkPrototypeFunction_close); -static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamSinkPrototypeFunction_error); - -class JSWritableStreamSinkPrototype final : public JSC::JSNonFinalObject { -public: - using Base = JSC::JSNonFinalObject; - static JSWritableStreamSinkPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) - { - JSWritableStreamSinkPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamSinkPrototype(vm, globalObject, structure); - ptr->finishCreation(vm); - return ptr; - } - - DECLARE_INFO; - template - static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamSinkPrototype, Base); - return &vm.plainObjectSpace(); - } - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); - } - -private: - JSWritableStreamSinkPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) - : JSC::JSNonFinalObject(vm, structure) - { - } - - void finishCreation(JSC::VM&); -}; -STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamSinkPrototype, JSWritableStreamSinkPrototype::Base); - -/* Hash table for prototype */ - -static const HashTableValue JSWritableStreamSinkPrototypeTableValues[] = { - { "write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamSinkPrototypeFunction_write, 1 } }, - { "close"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamSinkPrototypeFunction_close, 0 } }, - { "error"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamSinkPrototypeFunction_error, 1 } }, -}; - -const ClassInfo JSWritableStreamSinkPrototype::s_info = { "WritableStreamSink"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamSinkPrototype) }; - -void JSWritableStreamSinkPrototype::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - reifyStaticProperties(vm, JSWritableStreamSink::info(), JSWritableStreamSinkPrototypeTableValues, *this); - JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); -} - -const ClassInfo JSWritableStreamSink::s_info = { "WritableStreamSink"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamSink) }; - -JSWritableStreamSink::JSWritableStreamSink(Structure* structure, JSDOMGlobalObject& globalObject, Ref&& impl) - : JSDOMWrapper(structure, globalObject, WTF::move(impl)) -{ -} - -void JSWritableStreamSink::finishCreation(VM& vm) -{ - Base::finishCreation(vm); - ASSERT(inherits(info())); - - // static_assert(!std::is_base_of::value, "Interface is not marked as [ActiveDOMObject] even though implementation class subclasses ActiveDOMObject."); -} - -JSObject* JSWritableStreamSink::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - auto* structure = JSWritableStreamSinkPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); - structure->setMayBePrototype(true); - return JSWritableStreamSinkPrototype::create(vm, &globalObject, structure); -} - -JSObject* JSWritableStreamSink::prototype(VM& vm, JSDOMGlobalObject& globalObject) -{ - return getDOMPrototype(vm, globalObject); -} - -void JSWritableStreamSink::destroy(JSC::JSCell* cell) -{ - JSWritableStreamSink* thisObject = static_cast(cell); - thisObject->JSWritableStreamSink::~JSWritableStreamSink(); -} - -static inline JSC::EncodedJSValue jsWritableStreamSinkPrototypeFunction_writeBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperationReturningPromise::ClassParameter castedThis, Ref&& promise) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); - if (callFrame->argumentCount() < 1) [[unlikely]] - return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); - auto* context = uncheckedDowncast(lexicalGlobalObject)->scriptExecutionContext(); - if (!context) [[unlikely]] - return JSValue::encode(jsUndefined()); - EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); - auto value = convert(*lexicalGlobalObject, argument0.value()); - RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS>(*lexicalGlobalObject, *castedThis->globalObject(), throwScope, [&]() -> decltype(auto) { return impl.write(*context, WTF::move(value), WTF::move(promise)); }))); -} - -JSC_DEFINE_HOST_FUNCTION(jsWritableStreamSinkPrototypeFunction_write, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperationReturningPromise::call(*lexicalGlobalObject, *callFrame, "write"); -} - -static inline JSC::EncodedJSValue jsWritableStreamSinkPrototypeFunction_closeBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); - RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.close(); }))); -} - -JSC_DEFINE_HOST_FUNCTION(jsWritableStreamSinkPrototypeFunction_close, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "close"); -} - -static inline JSC::EncodedJSValue jsWritableStreamSinkPrototypeFunction_errorBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation::ClassParameter castedThis) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - UNUSED_PARAM(throwScope); - UNUSED_PARAM(callFrame); - auto& impl = castedThis->wrapped(); - if (callFrame->argumentCount() < 1) [[unlikely]] - return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject)); - EnsureStillAliveScope argument0 = callFrame->uncheckedArgument(0); - auto message = convert(*lexicalGlobalObject, argument0.value()); - RETURN_IF_EXCEPTION(throwScope, {}); - RELEASE_AND_RETURN(throwScope, JSValue::encode(toJS(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.error(WTF::move(message)); }))); -} - -JSC_DEFINE_HOST_FUNCTION(jsWritableStreamSinkPrototypeFunction_error, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - return IDLOperation::call(*lexicalGlobalObject, *callFrame, "error"); -} - -JSC::GCClient::IsoSubspace* JSWritableStreamSink::subspaceForImpl(JSC::VM& vm) -{ - return WebCore::subspaceForImpl( - vm, - [](auto& spaces) { return spaces.m_clientSubspaceForWritableStreamSink.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStreamSink = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForWritableStreamSink.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStreamSink = std::forward(space); }); -} - -void JSWritableStreamSink::analyzeHeap(JSCell* cell, HeapAnalyzer& analyzer) -{ - auto* thisObject = uncheckedDowncast(cell); - analyzer.setWrappedObjectForCell(cell, &thisObject->wrapped()); - if (thisObject->scriptExecutionContext()) - analyzer.setLabelForCell(cell, makeString("url "_s, thisObject->scriptExecutionContext()->url().string())); - Base::analyzeHeap(cell, analyzer); -} - -bool JSWritableStreamSinkOwner::isReachableFromOpaqueRoots(JSC::Handle handle, void*, AbstractSlotVisitor& visitor, ASCIILiteral* reason) -{ - UNUSED_PARAM(handle); - UNUSED_PARAM(visitor); - UNUSED_PARAM(reason); - return false; -} - -void JSWritableStreamSinkOwner::finalize(JSC::Handle handle, void* context) -{ - auto* jsWritableStreamSink = static_cast(handle.slot()->asCell()); - auto& world = *static_cast(context); - uncacheWrapper(world, &jsWritableStreamSink->wrapped(), jsWritableStreamSink); -} - -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObject, Ref&& impl) -{ - return createWrapper(globalObject, WTF::move(impl)); -} - -JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, WritableStreamSink& impl) -{ - return wrap(lexicalGlobalObject, globalObject, impl); -} - -WritableStreamSink* JSWritableStreamSink::toWrapped(JSC::VM&, JSC::JSValue value) -{ - if (auto* wrapper = dynamicDowncast(value)) - return &wrapper->wrapped(); - return nullptr; -} - -} diff --git a/src/jsc/bindings/webcore/JSWritableStreamSink.h b/src/jsc/bindings/webcore/JSWritableStreamSink.h deleted file mode 100644 index ec98d4f23e0b..000000000000 --- a/src/jsc/bindings/webcore/JSWritableStreamSink.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - This file is part of the WebKit open source project. - This file has been generated by generate-bindings.pl. DO NOT MODIFY! - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Library General Public - License as published by the Free Software Foundation; either - version 2 of the License, or (at your option) any later version. - - This library is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Library General Public License for more details. - - You should have received a copy of the GNU Library General Public License - along with this library; see the file COPYING.LIB. If not, write to - the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, - Boston, MA 02110-1301, USA. -*/ - -#pragma once - -#include "JSDOMWrapper.h" -#include "WritableStreamSink.h" -#include - -namespace WebCore { - -class JSWritableStreamSink : public JSDOMWrapper { -public: - using Base = JSDOMWrapper; - static JSWritableStreamSink* create(JSC::Structure* structure, JSDOMGlobalObject* globalObject, Ref&& impl) - { - JSWritableStreamSink* ptr = new (NotNull, JSC::allocateCell(globalObject->vm())) JSWritableStreamSink(structure, *globalObject, WTF::move(impl)); - ptr->finishCreation(globalObject->vm()); - return ptr; - } - - static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); - static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); - static WritableStreamSink* toWrapped(JSC::VM&, JSC::JSValue); - static void destroy(JSC::JSCell*); - - DECLARE_INFO; - - static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) - { - return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray); - } - - template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) - { - if constexpr (mode == JSC::SubspaceAccess::Concurrently) - return nullptr; - return subspaceForImpl(vm); - } - static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM& vm); - static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); - -protected: - JSWritableStreamSink(JSC::Structure*, JSDOMGlobalObject&, Ref&&); - - void finishCreation(JSC::VM&); -}; - -class JSWritableStreamSinkOwner final : public JSC::WeakHandleOwner { -public: - bool isReachableFromOpaqueRoots(JSC::Handle, void* context, JSC::AbstractSlotVisitor&, ASCIILiteral*) final; - void finalize(JSC::Handle, void* context) final; -}; - -inline JSC::WeakHandleOwner* wrapperOwner(DOMWrapperWorld&, WritableStreamSink*) -{ - static NeverDestroyed owner; - return &owner.get(); -} - -inline void* wrapperKey(WritableStreamSink* wrappableObject) -{ - return wrappableObject; -} - -JSC::JSValue toJS(JSC::JSGlobalObject*, JSDOMGlobalObject*, WritableStreamSink&); -inline JSC::JSValue toJS(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, WritableStreamSink* impl) { return impl ? toJS(lexicalGlobalObject, globalObject, *impl) : JSC::jsNull(); } -JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&&); -inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject* lexicalGlobalObject, JSDOMGlobalObject* globalObject, RefPtr&& impl) { return impl ? toJSNewlyCreated(lexicalGlobalObject, globalObject, impl.releaseNonNull()) : JSC::jsNull(); } - -template<> struct JSDOMWrapperConverterTraits { - using WrapperClass = JSWritableStreamSink; - using ToWrappedReturnType = WritableStreamSink*; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/ReadableStream.cpp b/src/jsc/bindings/webcore/ReadableStream.cpp deleted file mode 100644 index cd04170c4b14..000000000000 --- a/src/jsc/bindings/webcore/ReadableStream.cpp +++ /dev/null @@ -1,727 +0,0 @@ -/* - * Copyright (C) 2017-2021 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CANON INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "root.h" - -#include "config.h" -#include "ReadableStream.h" - -#include "Exception.h" -#include "ExceptionCode.h" -#include "JSDOMConvertSequences.h" -#include "JSReadableStreamSink.h" -#include "JSReadableStreamSource.h" -#include "WebCoreJSClientData.h" -#include "WebCoreJSBuiltins.h" -#include "ZigGlobalObject.h" -#include "ZigGeneratedClasses.h" -#include "helpers.h" -#include "BunClientData.h" -#include "IDLTypes.h" -#include "BunIDLConvert.h" -#include -#include -#include -#include -#include - -namespace WebCore { -using namespace JSC; - -static inline ExceptionOr invokeConstructor(JSC::JSGlobalObject& lexicalGlobalObject, const JSC::Identifier& identifier, const Function& buildArguments) -{ - VM& vm = lexicalGlobalObject.vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto& globalObject = *uncheckedDowncast(&lexicalGlobalObject); - - auto constructorValue = globalObject.get(&lexicalGlobalObject, identifier); - EXCEPTION_ASSERT(!scope.exception() || vm.hasPendingTerminationException()); - RETURN_IF_EXCEPTION(scope, Exception { ExistingExceptionError }); - auto constructor = JSC::asObject(constructorValue); - - auto constructData = JSC::getConstructData(constructor); - ASSERT(constructData.type != CallData::Type::None); - - MarkedArgumentBuffer args; - buildArguments(args, lexicalGlobalObject, globalObject); - ASSERT(!args.hasOverflowed()); - - JSObject* object = JSC::construct(&lexicalGlobalObject, constructor, constructData, args); - EXCEPTION_ASSERT(!!scope.exception() == !object); - EXCEPTION_ASSERT(!scope.exception() || vm.hasPendingTerminationException()); - RETURN_IF_EXCEPTION(scope, Exception { ExistingExceptionError }); - - return object; -} - -ExceptionOr> ReadableStream::create(JSC::JSGlobalObject& lexicalGlobalObject, RefPtr&& source) -{ - auto& builtinNames = WebCore::builtinNames(lexicalGlobalObject.vm()); - - auto objectOrException = invokeConstructor(lexicalGlobalObject, builtinNames.ReadableStreamPrivateName(), [&source](auto& args, auto& lexicalGlobalObject, auto& globalObject) { - args.append(source ? toJSNewlyCreated(&lexicalGlobalObject, &globalObject, source.releaseNonNull()) : JSC::jsUndefined()); - }); - - if (objectOrException.hasException()) - return objectOrException.releaseException(); - - return create(*uncheckedDowncast(&lexicalGlobalObject), *uncheckedDowncast(objectOrException.releaseReturnValue())); -} - -ExceptionOr> ReadableStream::create(JSC::JSGlobalObject& lexicalGlobalObject, RefPtr&& source, JSC::JSValue nativePtr) -{ - auto& builtinNames = WebCore::builtinNames(lexicalGlobalObject.vm()); - RELEASE_ASSERT(source != nullptr); - - auto objectOrException = invokeConstructor(lexicalGlobalObject, builtinNames.ReadableStreamPrivateName(), [&source, nativePtr](auto& args, auto& lexicalGlobalObject, auto& globalObject) { - auto sourceStream = toJSNewlyCreated(&lexicalGlobalObject, &globalObject, source.releaseNonNull()); - auto tag = WebCore::clientData(lexicalGlobalObject.vm())->builtinNames().bunNativePtrPrivateName(); - sourceStream.getObject()->putDirect(lexicalGlobalObject.vm(), tag, nativePtr, JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum); - args.append(sourceStream); - }); - - if (objectOrException.hasException()) - return objectOrException.releaseException(); - - return create(*uncheckedDowncast(&lexicalGlobalObject), *uncheckedDowncast(objectOrException.releaseReturnValue())); -} - -static inline std::optional invokeReadableStreamFunction(JSC::JSGlobalObject& lexicalGlobalObject, const JSC::Identifier& identifier, JSC::JSValue thisValue, const JSC::MarkedArgumentBuffer& arguments) -{ - JSC::VM& vm = lexicalGlobalObject.vm(); - JSC::JSLockHolder lock(vm); - - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - auto function = lexicalGlobalObject.get(&lexicalGlobalObject, identifier); - RETURN_IF_EXCEPTION(scope, {}); - ASSERT(function.isCallable()); - - auto callData = JSC::getCallData(function); - auto result = call(&lexicalGlobalObject, function, callData, thisValue, arguments); - EXCEPTION_ASSERT(!scope.exception() || vm.hasPendingTerminationException()); - RETURN_IF_EXCEPTION(scope, {}); - return result; -} - -// readableStreamCancel can return a rejected promise (Promise.reject(storedError) -// for an already-errored stream) or a pending promise that rejects later (when a -// still-readable stream's underlyingSource.cancel() rejects asynchronously). -// Native teardown call sites discard the result, so mark it handled to avoid -// surfacing the error as an unhandled rejection. isHandledFlag is sticky and read -// at rejection time, so marking a pending promise is correct. Matches -// $markPromiseAsHandled, which the JS callers of stream.cancel() use unconditionally. -static inline void markCancelResultHandled(std::optional result) -{ - if (!result) - return; - if (auto* promise = dynamicDowncast(*result)) - promise->markAsHandled(); -} - -void ReadableStream::pipeTo(ReadableStreamSink& sink) -{ - auto& lexicalGlobalObject = *m_globalObject; - auto* clientData = static_cast(lexicalGlobalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamPipeToPrivateName(); - - MarkedArgumentBuffer arguments; - arguments.append(readableStream()); - arguments.append(toJS(&lexicalGlobalObject, m_globalObject.get(), sink)); - ASSERT(!arguments.hasOverflowed()); - invokeReadableStreamFunction(lexicalGlobalObject, privateName, JSC::jsUndefined(), arguments); -} - -std::optional, Ref>> ReadableStream::tee() -{ - auto& lexicalGlobalObject = *m_globalObject; - auto* clientData = static_cast(lexicalGlobalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamTeePrivateName(); - - MarkedArgumentBuffer arguments; - arguments.append(readableStream()); - arguments.append(JSC::jsBoolean(true)); - ASSERT(!arguments.hasOverflowed()); - auto returnedValue = invokeReadableStreamFunction(lexicalGlobalObject, privateName, JSC::jsUndefined(), arguments); - if (!returnedValue) - return {}; - - auto results = Detail::SequenceConverter>::convert(lexicalGlobalObject, *returnedValue); - - ASSERT(results.size() == 2); - return std::make_pair(results[0].releaseNonNull(), results[1].releaseNonNull()); -} - -void ReadableStream::lock() -{ - auto& builtinNames = WebCore::builtinNames(m_globalObject->vm()); - auto result = invokeConstructor(*m_globalObject, builtinNames.ReadableStreamDefaultReaderPrivateName(), [this](auto& args, auto&, auto&) { - args.append(readableStream()); - }); -} - -void ReadableStream::cancel(const Exception& exception) -{ - auto& lexicalGlobalObject = *m_globalObject; - auto* clientData = static_cast(lexicalGlobalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamCancelPrivateName(); - - auto& vm = lexicalGlobalObject.vm(); - JSC::JSLockHolder lock(vm); - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - auto value = createDOMException(&lexicalGlobalObject, exception.code(), exception.message()); - if (scope.exception()) [[unlikely]] { - ASSERT(vm.hasPendingTerminationException()); - return; - } - - MarkedArgumentBuffer arguments; - arguments.append(readableStream()); - arguments.append(value); - ASSERT(!arguments.hasOverflowed()); - markCancelResultHandled(invokeReadableStreamFunction(lexicalGlobalObject, privateName, JSC::jsUndefined(), arguments)); -} - -void ReadableStream::cancel(WebCore::JSDOMGlobalObject& globalObject, JSReadableStream* readableStream, const Exception& exception) -{ - auto* clientData = static_cast(globalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamCancelPrivateName(); - - auto& vm = globalObject.vm(); - JSC::JSLockHolder lock(vm); - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - auto value = createDOMException(&globalObject, exception.code(), exception.message()); - if (scope.exception()) [[unlikely]] { - ASSERT(vm.hasPendingTerminationException()); - return; - } - - MarkedArgumentBuffer arguments; - arguments.append(readableStream); - arguments.append(value); - ASSERT(!arguments.hasOverflowed()); - markCancelResultHandled(invokeReadableStreamFunction(globalObject, privateName, JSC::jsUndefined(), arguments)); -} - -static inline bool checkReadableStream(JSDOMGlobalObject& globalObject, JSReadableStream* readableStream, JSC::JSValue function) -{ - auto& lexicalGlobalObject = globalObject; - - ASSERT(function); - JSC::MarkedArgumentBuffer arguments; - arguments.append(readableStream); - ASSERT(!arguments.hasOverflowed()); - - auto& vm = lexicalGlobalObject.vm(); - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - auto callData = JSC::getCallData(function); - ASSERT(callData.type != JSC::CallData::Type::None); - - auto result = call(&lexicalGlobalObject, function, callData, JSC::jsUndefined(), arguments); - EXCEPTION_ASSERT(!scope.exception() || vm.hasPendingTerminationException()); - - return result.isTrue() || scope.exception(); -} - -bool ReadableStream::isLocked() const -{ - return isLocked(globalObject(), readableStream()); -} - -bool ReadableStream::isLocked(JSGlobalObject* globalObject, JSReadableStream* readableStream) -{ - // Mirror isReadableStreamLocked() in ReadableStreamInternals.ts. The builtins - // store a reader object or an empty {} sentinel in the $reader slot, never the - // literal `true`, so a `.isTrue()` check never matches. A stream is locked when - // $reader holds a value, or once the native reader has been detached ($bunNativePtr - // set to -1 by ReadableStream__detach). - auto& vm = globalObject->vm(); - auto clientData = WebCore::clientData(vm); - auto& privateName = clientData->builtinNames().readerPrivateName(); - JSValue reader = readableStream->getDirect(vm, privateName); - if (!reader.isEmpty() && !reader.isUndefinedOrNull()) - return true; - - JSValue nativePtr = readableStream->nativePtr(); - return nativePtr.isInt32() && nativePtr.asInt32() == -1; -} - -bool ReadableStream::isDisturbed(JSGlobalObject* globalObject, JSReadableStream* readableStream) -{ - return readableStream->disturbed(); -} - -bool ReadableStream::isDisturbed() const -{ - return readableStream()->disturbed(); -} - -JSC_DEFINE_HOST_FUNCTION(jsFunctionTransferToNativeReadableStream, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(lexicalGlobalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - auto* readableStream = dynamicDowncast(callFrame->argument(0)); - readableStream->setTransferred(); - readableStream->setDisturbed(true); - return JSValue::encode(jsUndefined()); -} - -} // namespace WebCore - -using namespace JSC; -using namespace WebCore; - -extern "C" bool ReadableStream__tee(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject, JSC::EncodedJSValue* possibleReadableStream1, JSC::EncodedJSValue* possibleReadableStream2) -{ - auto* readableStream = dynamicDowncast(JSC::JSValue::decode(possibleReadableStream)); - if (!readableStream) [[unlikely]] - return false; - - auto lexicalGlobalObject = globalObject; - auto& vm = JSC::getVM(lexicalGlobalObject); - auto* clientData = static_cast(vm.clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamTeePrivateName(); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto invokeReadableStreamFunction = [](JSC::JSGlobalObject* lexicalGlobalObject, const JSC::Identifier& identifier, JSC::JSValue thisValue, const JSC::MarkedArgumentBuffer& arguments) -> std::optional { - JSC::VM& vm = lexicalGlobalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - JSC::JSLockHolder lock(vm); - - auto function = lexicalGlobalObject->get(lexicalGlobalObject, identifier); - scope.assertNoExceptionExceptTermination(); - if (scope.exception()) [[unlikely]] - return {}; - ASSERT(function.isCallable()); - - auto callData = JSC::getCallData(function); - auto result = JSC::call(lexicalGlobalObject, function, callData, thisValue, arguments); - // readableStreamTee throws a catchable TypeError when the stream is already - // locked (reachable from Request/Response.clone()). Propagate it; reporting - // it as uncaught here would clear it and set a nonzero exit code. - RETURN_IF_EXCEPTION(scope, {}); - return result; - }; - - MarkedArgumentBuffer arguments; - arguments.append(readableStream); - arguments.append(JSC::jsBoolean(true)); - ASSERT(!arguments.hasOverflowed()); - auto returnedValue = invokeReadableStreamFunction(lexicalGlobalObject, privateName, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(scope, false); - if (!returnedValue) return false; - - auto results = convert>>(*lexicalGlobalObject, *returnedValue); - RETURN_IF_EXCEPTION(scope, false); - - *possibleReadableStream1 = JSValue::encode(results[0]); - *possibleReadableStream2 = JSValue::encode(results[1]); - return true; -} - -extern "C" void ReadableStream__cancel(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject) -{ - auto* readableStream = dynamicDowncast(JSC::JSValue::decode(possibleReadableStream)); - if (!readableStream) [[unlikely]] - return; - - // Only cancel a stream that has a real reader. Direct streams store an empty - // {} sentinel in $reader (see $readDirectStream) while native code consumes - // them; routing that through readableStreamCancel is wrong (their teardown is - // owned by the controller close/detach path) and drops the native source's - // last reference mid-consumption. A real reader holds an ownerReadableStream - // back-pointer; the sentinel does not. - auto& vm = globalObject->vm(); - auto& builtinNames = WebCore::builtinNames(vm); - JSValue reader = readableStream->getDirect(vm, builtinNames.readerPrivateName()); - if (reader.isEmpty() || !reader.isObject()) - return; - JSObject* readerObject = asObject(reader); - if (!readerObject->getDirect(vm, builtinNames.ownerReadableStreamPrivateName())) - return; - - WebCore::Exception exception { Bun::AbortError }; - WebCore::ReadableStream::cancel(*globalObject, readableStream, exception); -} - -// Like ReadableStream__cancel but forwards an arbitrary JS reason verbatim to -// the stream's cancel algorithm instead of synthesizing a DOMException. Used -// by fetch() to honor AbortSignal.reason when cancelling a request body. -extern "C" void ReadableStream__cancelWithReason(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject, JSC::EncodedJSValue encodedReason) -{ - auto* readableStream = dynamicDowncast(JSC::JSValue::decode(possibleReadableStream)); - if (!readableStream) [[unlikely]] - return; - - auto& vm = globalObject->vm(); - auto* clientData = static_cast(vm.clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamCancelPrivateName(); - - JSC::JSLockHolder lock(vm); - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - MarkedArgumentBuffer arguments; - arguments.append(readableStream); - arguments.append(JSC::JSValue::decode(encodedReason)); - ASSERT(!arguments.hasOverflowed()); - markCancelResultHandled(invokeReadableStreamFunction(*globalObject, privateName, JSC::jsUndefined(), arguments)); -} - -extern "C" void ReadableStream__detach(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject) -{ - auto value = JSC::JSValue::decode(possibleReadableStream); - if (value.isEmpty() || !value.isCell()) - return; - - auto* readableStream = static_cast(value.asCell()); - if (!readableStream) [[unlikely]] - return; - readableStream->setNativePtr(globalObject->vm(), jsNumber(-1)); - readableStream->setNativeType(0); - readableStream->setDisturbed(true); -} - -extern "C" bool ReadableStream__isDisturbed(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject) -{ - ASSERT(globalObject); - return WebCore::ReadableStream::isDisturbed(globalObject, dynamicDowncast(JSC::JSValue::decode(possibleReadableStream))); -} - -extern "C" bool ReadableStream__isLocked(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject) -{ - ASSERT(globalObject); - WebCore::JSReadableStream* stream = dynamicDowncast(JSValue::decode(possibleReadableStream)); - return stream != nullptr && WebCore::ReadableStream::isLocked(globalObject, stream); -} - -extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JSC::EncodedJSValue* possibleReadableStream, void** ptr) -{ - ASSERT(globalObject); - JSC::JSObject* object = JSValue::decode(*possibleReadableStream).getObject(); - if (!object) { - *ptr = nullptr; - return -1; - } - - auto& vm = JSC::getVM(globalObject); - - if (!object->inherits()) { - auto throwScope = DECLARE_THROW_SCOPE(vm); - JSValue target = object; - JSValue fn = JSValue(); - auto* function = dynamicDowncast(object); - if (function && !function->isHostFunction() && function->jsExecutable() && function->jsExecutable()->isAsyncGenerator()) { - fn = object; - target = jsUndefined(); - } else { - auto iterable = object->getIfPropertyExists(globalObject, vm.propertyNames->asyncIteratorSymbol); - RETURN_IF_EXCEPTION(throwScope, {}); - if (iterable && iterable.isCallable()) { - fn = iterable; - } - } - - if (throwScope.exception()) [[unlikely]] { - *ptr = nullptr; - return -1; - } - - if (fn.isEmpty()) { - *ptr = nullptr; - return -1; - } - - auto* createIterator = globalObject->builtinInternalFunctions().readableStreamInternals().m_readableStreamFromAsyncIteratorFunction.get(); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(target); - arguments.append(fn); - - JSC::JSValue result = profiledCall(globalObject, JSC::ProfilingReason::API, createIterator, JSC::getCallData(createIterator), JSC::jsUndefined(), arguments); - - if (throwScope.exception()) [[unlikely]] { - return -1; - } - - if (!result.isObject()) { - *ptr = nullptr; - return -1; - } - - object = result.getObject(); - - ASSERT(object->inherits()); - *possibleReadableStream = JSValue::encode(object); - *ptr = nullptr; - ensureStillAliveHere(object); - return 0; - } - - auto* readableStream = uncheckedDowncast(object); - - JSValue nativePtrHandle = readableStream->nativePtr(); - if (nativePtrHandle.isEmpty() || !nativePtrHandle.isCell()) { - *ptr = nullptr; - return 0; - } - - JSCell* cell = nativePtrHandle.asCell(); - - if (auto* casted = dynamicDowncast(cell)) { - *ptr = casted->wrapped(); - return 1; - } - - if (auto* casted = dynamicDowncast(cell)) { - *ptr = casted->wrapped(); - return 2; - } - - if (auto* casted = dynamicDowncast(cell)) { - *ptr = casted->wrapped(); - return 4; - } - - return 0; -} - -extern "C" JSC::EncodedJSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalObject* globalObject, JSC::EncodedJSValue nativePtr) -{ - auto& vm = JSC::getVM(globalObject); - auto scope = DECLARE_THROW_SCOPE(vm); - - auto& builtinNames = WebCore::builtinNames(vm); - - auto function = globalObject->getDirect(vm, builtinNames.createNativeReadableStreamPrivateName()).getObject(); - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(nativePtr)); - - auto callData = JSC::getCallData(function); - auto result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(scope, {}); - return JSValue::encode(result); -} - -static inline JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBufferBody(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue) -{ - auto& vm = JSC::getVM(globalObject); - - auto throwScope = DECLARE_THROW_SCOPE(vm); - - auto* function = globalObject->m_readableStreamToArrayBuffer.get(); - if (!function) { - function = JSFunction::create(vm, globalObject, static_cast(readableStreamReadableStreamToArrayBufferCodeGenerator(vm)), globalObject); - globalObject->m_readableStreamToArrayBuffer.set(vm, globalObject, function); - } - - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(readableStreamValue)); - - auto callData = JSC::getCallData(function); - JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(throwScope, {}); - - JSC::JSObject* object = result.getObject(); - - if (!result || result.isUndefinedOrNull()) [[unlikely]] - return JSValue::encode(result); - - if (!object) [[unlikely]] { - throwTypeError(globalObject, throwScope, "Expected object"_s); - return {}; - } - - JSC::JSPromise* promise = dynamicDowncast(object); - if (!promise) [[unlikely]] { - throwTypeError(globalObject, throwScope, "Expected promise"_s); - return {}; - } - - RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(promise)); -} - -extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBuffer(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue) -{ - return ZigGlobalObject__readableStreamToArrayBufferBody(static_cast(globalObject), readableStreamValue); -} - -extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBytes(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue) -{ - auto& vm = JSC::getVM(globalObject); - - auto throwScope = DECLARE_THROW_SCOPE(vm); - - auto* function = globalObject->m_readableStreamToBytes.get(); - if (!function) { - function = JSFunction::create(vm, globalObject, static_cast(readableStreamReadableStreamToBytesCodeGenerator(vm)), globalObject); - globalObject->m_readableStreamToBytes.set(vm, globalObject, function); - } - - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(readableStreamValue)); - - auto callData = JSC::getCallData(function); - JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(throwScope, {}); - - JSC::JSObject* object = result.getObject(); - - if (!result || result.isUndefinedOrNull()) [[unlikely]] - return JSValue::encode(result); - - if (!object) [[unlikely]] { - throwTypeError(globalObject, throwScope, "Expected object"_s); - return {}; - } - - JSC::JSPromise* promise = dynamicDowncast(object); - if (!promise) [[unlikely]] { - throwTypeError(globalObject, throwScope, "Expected promise"_s); - return {}; - } - - RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(promise)); -} - -extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToText(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue) -{ - auto& vm = JSC::getVM(globalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - JSC::JSFunction* function = nullptr; - if (auto readableStreamToText = globalObject->m_readableStreamToText.get()) { - function = readableStreamToText; - } else { - function = JSFunction::create(vm, globalObject, static_cast(readableStreamReadableStreamToTextCodeGenerator(vm)), globalObject); - - globalObject->m_readableStreamToText.set(vm, globalObject, function); - } - - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(readableStreamValue)); - - auto callData = JSC::getCallData(function); - JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(throwScope, {}); - return JSC::JSValue::encode(result); -} - -extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToFormData(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue, JSC::EncodedJSValue contentTypeValue) -{ - auto& vm = JSC::getVM(globalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - JSC::JSFunction* function = nullptr; - if (auto readableStreamToFormData = globalObject->m_readableStreamToFormData.get()) { - function = readableStreamToFormData; - } else { - function = JSFunction::create(vm, globalObject, static_cast(readableStreamReadableStreamToFormDataCodeGenerator(vm)), globalObject); - - globalObject->m_readableStreamToFormData.set(vm, globalObject, function); - } - - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(readableStreamValue)); - arguments.append(JSValue::decode(contentTypeValue)); - - auto callData = JSC::getCallData(function); - JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(throwScope, {}); - return JSC::JSValue::encode(result); -} - -extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToJSON(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue) -{ - auto& vm = JSC::getVM(globalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - JSC::JSFunction* function = nullptr; - if (auto readableStreamToJSON = globalObject->m_readableStreamToJSON.get()) { - function = readableStreamToJSON; - } else { - function = JSFunction::create(vm, globalObject, static_cast(readableStreamReadableStreamToJSONCodeGenerator(vm)), globalObject); - - globalObject->m_readableStreamToJSON.set(vm, globalObject, function); - } - - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(readableStreamValue)); - - auto callData = JSC::getCallData(function); - JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(throwScope, {}); - return JSC::JSValue::encode(result); -} - -extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC::EncodedJSValue readableStreamValue) -{ - auto& vm = JSC::getVM(globalObject); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - JSC::JSFunction* function = nullptr; - if (auto readableStreamToBlob = globalObject->m_readableStreamToBlob.get()) { - function = readableStreamToBlob; - } else { - function = JSFunction::create(vm, globalObject, static_cast(readableStreamReadableStreamToBlobCodeGenerator(vm)), globalObject); - - globalObject->m_readableStreamToBlob.set(vm, globalObject, function); - } - - JSC::MarkedArgumentBuffer arguments = JSC::MarkedArgumentBuffer(); - arguments.append(JSValue::decode(readableStreamValue)); - - auto callData = JSC::getCallData(function); - JSValue result = call(globalObject, function, callData, JSC::jsUndefined(), arguments); - RETURN_IF_EXCEPTION(throwScope, {}); - return JSC::JSValue::encode(result); -} - -JSC_DEFINE_HOST_FUNCTION(functionReadableStreamToArrayBuffer, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - - if (callFrame->argumentCount() < 1) [[unlikely]] { - auto throwScope = DECLARE_THROW_SCOPE(vm); - throwTypeError(globalObject, throwScope, "Expected at least one argument"_s); - return {}; - } - - auto readableStreamValue = callFrame->uncheckedArgument(0); - return ZigGlobalObject__readableStreamToArrayBufferBody(static_cast(globalObject), JSValue::encode(readableStreamValue)); -} - -JSC_DEFINE_HOST_FUNCTION(functionReadableStreamToBytes, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) -{ - auto& vm = JSC::getVM(globalObject); - - if (callFrame->argumentCount() < 1) [[unlikely]] { - auto throwScope = DECLARE_THROW_SCOPE(vm); - throwTypeError(globalObject, throwScope, "Expected at least one argument"_s); - return {}; - } - - auto readableStreamValue = callFrame->uncheckedArgument(0); - return ZigGlobalObject__readableStreamToBytes(static_cast(globalObject), JSValue::encode(readableStreamValue)); -} diff --git a/src/jsc/bindings/webcore/ReadableStream.h b/src/jsc/bindings/webcore/ReadableStream.h deleted file mode 100644 index c5c2b4a7699e..000000000000 --- a/src/jsc/bindings/webcore/ReadableStream.h +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2017-2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CANON INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include "ExceptionOr.h" -#include "JSDOMBinding.h" -#include "JSDOMConvert.h" -#include "JSDOMGuardedObject.h" -#include "JSReadableStream.h" - -namespace WebCore { - -class ReadableStreamSink; -class ReadableStreamSource; - -class ReadableStream final : public DOMGuarded { -public: - static Ref create(JSDOMGlobalObject& globalObject, JSReadableStream& readableStream) { return adoptRef(*new ReadableStream(globalObject, readableStream)); } - - static ExceptionOr> create(JSC::JSGlobalObject&, RefPtr&&); - static ExceptionOr> create(JSC::JSGlobalObject& lexicalGlobalObject, RefPtr&& source, JSC::JSValue nativePtr); - - WEBCORE_EXPORT static bool isDisturbed(JSC::JSGlobalObject*, JSReadableStream*); - WEBCORE_EXPORT static bool isLocked(JSC::JSGlobalObject*, JSReadableStream*); - WEBCORE_EXPORT static void cancel(WebCore::JSDOMGlobalObject& globalObject, JSReadableStream*, const WebCore::Exception& exception); - - std::optional, Ref>> tee(); - - void cancel(const Exception&); - void lock(); - void pipeTo(ReadableStreamSink&); - bool isLocked() const; - bool isDisturbed() const; - - JSReadableStream* readableStream() const - { - return guarded(); - } - - ReadableStream(JSDOMGlobalObject& globalObject, JSReadableStream& readableStream) - : DOMGuarded(globalObject, readableStream) - { - } -}; - -struct JSReadableStreamWrapperConverter { - static RefPtr toWrapped(JSC::JSGlobalObject& lexicalGlobalObject, JSC::JSValue value) - { - auto* globalObject = dynamicDowncast(&lexicalGlobalObject); - if (!globalObject) - return nullptr; - - auto* readableStream = dynamicDowncast(value); - if (!readableStream) - return nullptr; - - return ReadableStream::create(*globalObject, *readableStream); - } -}; - -template<> struct JSDOMWrapperConverterTraits { - using WrapperClass = JSReadableStreamWrapperConverter; - using ToWrappedReturnType = RefPtr; - static constexpr bool needsState = true; -}; - -inline JSC::JSValue toJS(JSC::JSGlobalObject*, JSC::JSGlobalObject*, ReadableStream* stream) -{ - return stream ? stream->readableStream() : JSC::jsUndefined(); -} - -inline JSC::JSValue toJS(JSC::JSGlobalObject*, JSC::JSGlobalObject*, ReadableStream& stream) -{ - return stream.readableStream(); -} - -inline JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject*, Ref&& stream) -{ - return stream->readableStream(); -} - -JSC_DECLARE_HOST_FUNCTION(jsFunctionTransferToNativeReadableStream); - -} diff --git a/src/jsc/bindings/webcore/ReadableStreamDefaultController.cpp b/src/jsc/bindings/webcore/ReadableStreamDefaultController.cpp deleted file mode 100644 index 9b3712616b4d..000000000000 --- a/src/jsc/bindings/webcore/ReadableStreamDefaultController.cpp +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (C) 2016 Canon Inc. - * Copyright (C) 2016-2021 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted, provided that the following conditions - * are required to be met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of Canon Inc. nor the names of - * its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "config.h" -#include "ReadableStreamDefaultController.h" - -#include "WebCoreJSClientData.h" -#include "WebCoreJSBuiltins.h" -#include -#include -#include -#include -#include - -namespace WebCore { - -static bool invokeReadableStreamDefaultControllerFunction(JSC::JSGlobalObject& lexicalGlobalObject, const JSC::Identifier& identifier, const JSC::MarkedArgumentBuffer& arguments) -{ - JSC::VM& vm = lexicalGlobalObject.vm(); - JSC::JSLockHolder lock(vm); - - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - auto function = lexicalGlobalObject.get(&lexicalGlobalObject, identifier); - - EXCEPTION_ASSERT(!scope.exception() || vm.hasPendingTerminationException()); - RETURN_IF_EXCEPTION(scope, false); - - ASSERT(function.isCallable()); - - auto callData = JSC::getCallData(function); - call(&lexicalGlobalObject, function, callData, JSC::jsUndefined(), arguments); - EXCEPTION_ASSERT(!scope.exception() || vm.hasPendingTerminationException()); - return !scope.exception(); -} - -void ReadableStreamDefaultController::close() -{ - JSC::MarkedArgumentBuffer arguments; - arguments.append(&jsController()); - - auto* clientData = static_cast(globalObject().vm().clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamDefaultControllerClosePrivateName(); - - invokeReadableStreamDefaultControllerFunction(globalObject(), privateName, arguments); -} - -void ReadableStreamDefaultController::error(const Exception& exception) -{ - JSC::JSGlobalObject& lexicalGlobalObject = this->globalObject(); - auto& vm = lexicalGlobalObject.vm(); - JSC::JSLockHolder lock(vm); - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - auto value = createDOMException(&lexicalGlobalObject, exception.code(), exception.message()); - - if (scope.exception()) [[unlikely]] { - ASSERT(vm.hasPendingTerminationException()); - return; - } - - JSC::MarkedArgumentBuffer arguments; - arguments.append(&jsController()); - arguments.append(value); - - auto* clientData = static_cast(vm.clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamDefaultControllerErrorPrivateName(); - - invokeReadableStreamDefaultControllerFunction(globalObject(), privateName, arguments); -} - -void ReadableStreamDefaultController::error(JSC::JSValue error) -{ - JSC::JSGlobalObject& lexicalGlobalObject = this->globalObject(); - auto& vm = lexicalGlobalObject.vm(); - JSC::JSLockHolder lock(vm); - auto scope = DECLARE_THROW_SCOPE(vm); - auto value = JSC::Exception::create(vm, error); - - if (scope.exception()) [[unlikely]] { - ASSERT(vm.hasPendingTerminationException()); - return; - } - - JSC::MarkedArgumentBuffer arguments; - arguments.append(&jsController()); - arguments.append(value); - - auto* clientData = static_cast(vm.clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamDefaultControllerErrorPrivateName(); - - invokeReadableStreamDefaultControllerFunction(globalObject(), privateName, arguments); -} - -bool ReadableStreamDefaultController::enqueue(JSC::JSValue value) -{ - JSC::JSGlobalObject& lexicalGlobalObject = this->globalObject(); - auto& vm = lexicalGlobalObject.vm(); - JSC::JSLockHolder lock(vm); - - JSC::MarkedArgumentBuffer arguments; - arguments.append(&jsController()); - arguments.append(value); - - auto* clientData = static_cast(lexicalGlobalObject.vm().clientData); - auto& privateName = clientData->builtinFunctions().readableStreamInternalsBuiltins().readableStreamDefaultControllerEnqueuePrivateName(); - - return invokeReadableStreamDefaultControllerFunction(globalObject(), privateName, arguments); -} - -bool ReadableStreamDefaultController::enqueue(RefPtr&& buffer) -{ - if (!buffer) { - error(Exception { OutOfMemoryError }); - return false; - } - - JSC::JSGlobalObject& lexicalGlobalObject = this->globalObject(); - auto& vm = lexicalGlobalObject.vm(); - JSC::JSLockHolder lock(vm); - auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); - auto length = buffer->byteLength(); - auto value = JSC::JSUint8Array::create(&lexicalGlobalObject, lexicalGlobalObject.typedArrayStructureWithTypedArrayType(), WTF::move(buffer), 0, length); - - EXCEPTION_ASSERT(!scope.exception() || vm.hasPendingTerminationException()); - RETURN_IF_EXCEPTION(scope, false); - - return enqueue(value); -} - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/ReadableStreamDefaultController.h b/src/jsc/bindings/webcore/ReadableStreamDefaultController.h deleted file mode 100644 index bb3bc9409ac4..000000000000 --- a/src/jsc/bindings/webcore/ReadableStreamDefaultController.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2016 Canon Inc. - * Copyright (C) 2017 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted, provided that the following conditions - * are required to be met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of Canon Inc. nor the names of - * its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include "JSDOMConvertBufferSource.h" -#include "JSReadableStreamDefaultController.h" -#include -#include -#include - -namespace WebCore { - -class ReadableStreamSource; - -class ReadableStreamDefaultController { -public: - explicit ReadableStreamDefaultController(JSReadableStreamDefaultController* controller) - : m_jsController(controller) - { - } - - bool enqueue(RefPtr&&); - bool enqueue(JSC::JSValue); - void error(const Exception&); - void error(JSC::JSValue error); - void close(); - JSDOMGlobalObject& globalObject() const; - JSReadableStreamDefaultController& jsController() const; - // The owner of ReadableStreamDefaultController is responsible to keep uncollected the JSReadableStreamDefaultController. - JSReadableStreamDefaultController* m_jsController { nullptr }; - -private: -}; - -inline JSReadableStreamDefaultController& ReadableStreamDefaultController::jsController() const -{ - ASSERT(m_jsController); - return *m_jsController; -} - -inline JSDOMGlobalObject& ReadableStreamDefaultController::globalObject() const -{ - ASSERT(m_jsController); - ASSERT(m_jsController->globalObject()); - return *static_cast(m_jsController->globalObject()); -} - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/ReadableStreamSink.cpp b/src/jsc/bindings/webcore/ReadableStreamSink.cpp deleted file mode 100644 index 67078d6ded55..000000000000 --- a/src/jsc/bindings/webcore/ReadableStreamSink.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2017 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "config.h" -#include "ReadableStreamSink.h" - -#include "BufferSource.h" -#include "DOMException.h" -#include "ReadableStream.h" - -namespace WebCore { - -ReadableStreamToSharedBufferSink::ReadableStreamToSharedBufferSink(Callback&& callback) - : m_callback { WTF::move(callback) } -{ -} - -void ReadableStreamToSharedBufferSink::pipeFrom(ReadableStream& stream) -{ - stream.pipeTo(*this); -} - -void ReadableStreamToSharedBufferSink::enqueue(const BufferSource& buffer) -{ - if (!buffer.length()) - return; - - if (m_callback) { - std::span chunk { buffer.data(), buffer.length() }; - m_callback(&chunk); - } -} - -void ReadableStreamToSharedBufferSink::close() -{ - if (m_callback) - m_callback(nullptr); -} - -void ReadableStreamToSharedBufferSink::error(String&& message) -{ - if (auto callback = WTF::move(m_callback)) - callback(Exception { TypeError, WTF::move(message) }); -} - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/ReadableStreamSink.h b/src/jsc/bindings/webcore/ReadableStreamSink.h deleted file mode 100644 index 68dfefb001ff..000000000000 --- a/src/jsc/bindings/webcore/ReadableStreamSink.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2017 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include "ExceptionOr.h" -#include -#include -#include - -namespace WebCore { - -class BufferSource; -class ReadableStream; - -class ReadableStreamSink : public RefCounted { -public: - virtual ~ReadableStreamSink() = default; - - virtual void enqueue(const BufferSource&) = 0; - virtual void close() = 0; - virtual void error(String&&) = 0; -}; - -class ReadableStreamToSharedBufferSink final : public ReadableStreamSink { -public: - using Callback = Function*>&&)>; - static Ref create(Callback&& callback) { return adoptRef(*new ReadableStreamToSharedBufferSink(WTF::move(callback))); } - void pipeFrom(ReadableStream&); - void clearCallback() { m_callback = {}; } - -private: - explicit ReadableStreamToSharedBufferSink(Callback&&); - - void enqueue(const BufferSource&) final; - void close() final; - void error(String&&) final; - - Callback m_callback; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/ReadableStreamSource.cpp b/src/jsc/bindings/webcore/ReadableStreamSource.cpp deleted file mode 100644 index dfae6d6055be..000000000000 --- a/src/jsc/bindings/webcore/ReadableStreamSource.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2017 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "config.h" -#include "ReadableStreamSource.h" -namespace WebCore { - -ReadableStreamSource::~ReadableStreamSource() = default; - -void ReadableStreamSource::start(ReadableStreamDefaultController&& controller, DOMPromiseDeferred&& promise) -{ - ASSERT(!m_promise); - m_promise = makeUnique>(WTF::move(promise)); - m_controller = WTF::move(controller); - - setActive(); - doStart(); -} - -void ReadableStreamSource::pull(DOMPromiseDeferred&& promise) -{ - ASSERT(!m_promise); - ASSERT(m_controller); - - m_promise = makeUnique>(WTF::move(promise)); - - setActive(); - doPull(); -} - -void ReadableStreamSource::startFinished() -{ - ASSERT(m_promise); - m_promise->resolve(); - m_promise = nullptr; - setInactive(); -} - -void ReadableStreamSource::pullFinished() -{ - ASSERT(m_promise); - m_promise->resolve(); - m_promise = nullptr; - setInactive(); -} - -void ReadableStreamSource::cancel(JSC::JSValue) -{ - clean(); - doCancel(); -} - -void ReadableStreamSource::clean() -{ - if (m_promise) { - m_promise = nullptr; - setInactive(); - } -} - -void ReadableStreamSource::error(JSC::JSValue value) -{ - if (m_promise) { - m_promise->reject(value, RejectAsHandled::Yes); - m_promise = nullptr; - setInactive(); - } else { - controller().error(value); - } -} - -void SimpleReadableStreamSource::doCancel() -{ - m_isCancelled = true; -} - -void SimpleReadableStreamSource::close() -{ - if (!m_isCancelled) - controller().close(); -} - -void SimpleReadableStreamSource::enqueue(JSC::JSValue value) -{ - if (!m_isCancelled) - controller().enqueue(value); -} - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/ReadableStreamSource.h b/src/jsc/bindings/webcore/ReadableStreamSource.h deleted file mode 100644 index 0886ab4234e0..000000000000 --- a/src/jsc/bindings/webcore/ReadableStreamSource.h +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright (C) 2016 Canon Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted, provided that the following conditions - * are required to be met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of Canon Inc. nor the names of - * its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include "JSDOMPromiseDeferred.h" -#include "ReadableStreamDefaultController.h" -#include - -namespace WebCore { - -class ReadableStreamSource : public RefCounted { -public: - virtual ~ReadableStreamSource(); - - void start(ReadableStreamDefaultController&&, DOMPromiseDeferred&&); - void pull(DOMPromiseDeferred&&); - void cancel(JSC::JSValue); - void error(JSC::JSValue error); - - bool hasController() const { return !!m_controller; } - - bool isPulling() const { return !!m_promise; } - -protected: - ReadableStreamDefaultController& controller() { return m_controller.value(); } - const ReadableStreamDefaultController& controller() const { return m_controller.value(); } - - void startFinished(); - void pullFinished(); - void cancelFinished(); - void clean(); - - virtual void setActive() = 0; - virtual void setInactive() = 0; - - virtual void doStart() = 0; - virtual void doPull() = 0; - virtual void doCancel() = 0; - - std::unique_ptr> m_promise; - -private: - std::optional m_controller; -}; - -class SimpleReadableStreamSource - : public ReadableStreamSource, - public CanMakeWeakPtr { -public: - static Ref create() { return adoptRef(*new SimpleReadableStreamSource); } - - void close(); - void enqueue(JSC::JSValue); - -private: - SimpleReadableStreamSource() = default; - - // ReadableStreamSource - void setActive() final {} - void setInactive() final {} - void doStart() final {} - void doPull() final {} - void doCancel() final; - - bool m_isCancelled { false }; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/WritableStream.cpp b/src/jsc/bindings/webcore/WritableStream.cpp deleted file mode 100644 index a4b027943ce7..000000000000 --- a/src/jsc/bindings/webcore/WritableStream.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (C) 2021 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#include "config.h" -#include "WritableStream.h" - -#include "JSWritableStream.h" -#include "JSWritableStreamSink.h" - -namespace WebCore { - -ExceptionOr> WritableStream::create(JSC::JSGlobalObject& globalObject, std::optional>&& underlyingSink, std::optional>&& strategy) -{ - JSC::JSValue underlyingSinkValue = JSC::jsUndefined(); - if (underlyingSink) - underlyingSinkValue = underlyingSink->get(); - - JSC::JSValue strategyValue = JSC::jsUndefined(); - if (strategy) - strategyValue = strategy->get(); - - return create(globalObject, underlyingSinkValue, strategyValue); -} - -ExceptionOr> WritableStream::create(JSC::JSGlobalObject& globalObject, JSC::JSValue underlyingSink, JSC::JSValue strategy) -{ - auto result = InternalWritableStream::createFromUnderlyingSink(*uncheckedDowncast(&globalObject), underlyingSink, strategy); - if (result.hasException()) - return result.releaseException(); - - return adoptRef(*new WritableStream(result.releaseReturnValue())); -} - -ExceptionOr> WritableStream::create(JSDOMGlobalObject& globalObject, Ref&& sink) -{ - return create(globalObject, toJSNewlyCreated(&globalObject, &globalObject, WTF::move(sink)), JSC::jsUndefined()); -} - -Ref WritableStream::create(Ref&& internalWritableStream) -{ - return adoptRef(*new WritableStream(WTF::move(internalWritableStream))); -} - -WritableStream::WritableStream(Ref&& internalWritableStream) - : m_internalWritableStream(WTF::move(internalWritableStream)) -{ -} - -JSC::JSValue JSWritableStream::abort(JSC::JSGlobalObject& globalObject, JSC::CallFrame& callFrame) -{ - return wrapped().internalWritableStream().abort(globalObject, callFrame.argument(0)); -} - -JSC::JSValue JSWritableStream::close(JSC::JSGlobalObject& globalObject, JSC::CallFrame&) -{ - return wrapped().internalWritableStream().close(globalObject); -} - -JSC::JSValue JSWritableStream::getWriter(JSC::JSGlobalObject& globalObject, JSC::CallFrame&) -{ - return wrapped().internalWritableStream().getWriter(globalObject); -} - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/WritableStream.h b/src/jsc/bindings/webcore/WritableStream.h deleted file mode 100644 index 63a4b33778c0..000000000000 --- a/src/jsc/bindings/webcore/WritableStream.h +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2021 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR - * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include "root.h" - -#include "InternalWritableStream.h" -#include -#include - -namespace WebCore { - -class InternalWritableStream; -class WritableStreamSink; - -class WritableStream : public RefCounted { -public: - static ExceptionOr> create(JSC::JSGlobalObject&, std::optional>&&, std::optional>&&); - static ExceptionOr> create(JSDOMGlobalObject&, Ref&&); - static Ref create(Ref&&); - - ~WritableStream() = default; - - void lock() { m_internalWritableStream->lock(); } - bool locked() const { return m_internalWritableStream->locked(); } - - InternalWritableStream& internalWritableStream() { return m_internalWritableStream.get(); } - -private: - static ExceptionOr> create(JSC::JSGlobalObject&, JSC::JSValue, JSC::JSValue); - explicit WritableStream(Ref&&); - - Ref m_internalWritableStream; -}; - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/WritableStream.idl b/src/jsc/bindings/webcore/WritableStream.idl deleted file mode 100644 index cd32d17f0280..000000000000 --- a/src/jsc/bindings/webcore/WritableStream.idl +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2015 Canon Inc. - * Copyright (C) 2015 Igalia S.L. - * Copyright (C) 2020-2021 Apple Inc. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted, provided that the following conditions - * are required to be met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of Canon Inc. nor the names of - * its contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY CANON INC. AND ITS CONTRIBUTORS "AS IS" AND ANY - * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL CANON INC. AND ITS CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -[ - Exposed=*, - PrivateIdentifier, - PublicIdentifier, - SkipVTableValidation -] interface WritableStream { - // FIXME: Tighten parameter matching - [CallWith=CurrentGlobalObject] constructor(optional object underlyingSink, optional object strategy); - - readonly attribute boolean locked; - - [Custom, ReturnsOwnPromise] Promise abort(optional any reason); - [Custom, ReturnsOwnPromise] Promise close(); - [Custom] WritableStreamDefaultWriter getWriter(); -}; diff --git a/src/jsc/bindings/webcore/WritableStreamSink.h b/src/jsc/bindings/webcore/WritableStreamSink.h deleted file mode 100644 index 2b20494fff5c..000000000000 --- a/src/jsc/bindings/webcore/WritableStreamSink.h +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright (C) 2020 Apple Inc. All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - */ - -#pragma once - -#include "JSDOMPromiseDeferred.h" -#include -#include - -namespace JSC { -class JSValue; -} - -namespace WebCore { - -class WritableStreamSink : public RefCounted { -public: - virtual ~WritableStreamSink() = default; - - virtual void write(ScriptExecutionContext&, JSC::JSValue, DOMPromiseDeferred&&) = 0; - virtual void close() = 0; - virtual void error(String&&) = 0; -}; - -class SimpleWritableStreamSink : public WritableStreamSink { -public: - using WriteCallback = Function(ScriptExecutionContext&, JSC::JSValue)>; - static Ref create(WriteCallback&& writeCallback) { return adoptRef(*new SimpleWritableStreamSink(WTF::move(writeCallback))); } - -private: - explicit SimpleWritableStreamSink(WriteCallback&&); - - void write(ScriptExecutionContext&, JSC::JSValue, DOMPromiseDeferred&&) final; - void close() final {} - void error(String&&) final {} - - WriteCallback m_writeCallback; -}; - -inline SimpleWritableStreamSink::SimpleWritableStreamSink(WriteCallback&& writeCallback) - : m_writeCallback(WTF::move(writeCallback)) -{ -} - -inline void SimpleWritableStreamSink::write(ScriptExecutionContext& context, JSC::JSValue value, DOMPromiseDeferred&& promise) -{ - promise.settle(m_writeCallback(context, value)); -} - -} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp b/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp new file mode 100644 index 000000000000..5ff33530dc44 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/BunAsyncIterableSource.cpp @@ -0,0 +1,586 @@ +// Bun's async-iterable body extension: an async iterator (or async generator function) +// becomes a DIRECT ReadableStream whose pull drives `iter.next(controller)`; writes obey the +// sink's backpressure protocol and cancellation is forwarded to the iterator. +#include "config.h" +#include "JSAsyncIteratorSourceOperation.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStream.h" +#include "JSStreamsRuntime.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +const ClassInfo JSAsyncIteratorSourceOperation::s_info = { "AsyncIteratorSourceOperation"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSAsyncIteratorSourceOperation) }; + +JSAsyncIteratorSourceOperation::JSAsyncIteratorSourceOperation(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSAsyncIteratorSourceOperation::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSAsyncIteratorSourceOperation* JSAsyncIteratorSourceOperation::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSAsyncIteratorSourceOperation(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSAsyncIteratorSourceOperation::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSAsyncIteratorSourceOperation::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForAsyncIteratorSourceOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForAsyncIteratorSourceOperation = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForAsyncIteratorSourceOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForAsyncIteratorSourceOperation = std::forward(space); }); +} + +template +void JSAsyncIteratorSourceOperation::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_iterator); + visitor.append(thisObject->m_controller); + visitor.append(thisObject->m_pullPromise); +} + +DEFINE_VISIT_CHILDREN(JSAsyncIteratorSourceOperation); + +static void driveAsyncIterator(JSGlobalObject*, JSAsyncIteratorSourceOperation*); +static void asyncIterReturnIteratorAndSettle(JSGlobalObject*, JSAsyncIteratorSourceOperation*); +static void asyncIterFinishWithError(JSGlobalObject*, JSAsyncIteratorSourceOperation*, JSValue error); + +// invokeOptionalMethod returns the EMPTY value when the method is not callable; the empty +// value reports isCell(), so it must never reach a downcast. +static JSPromise* asPromise(JSValue value) +{ + if (!value || !value.isCell()) + return nullptr; + return dynamicDowncast(value); +} + +static void settlePullPromiseResolved(JSGlobalObject* globalObject, JSAsyncIteratorSourceOperation* op) +{ + auto& vm = getVM(globalObject); + op->m_done = true; + op->m_running = false; + if (auto* pullPromise = op->m_pullPromise.get()) { + op->m_pullPromise.clear(); + pullPromise->fulfill(vm, jsUndefined()); + } +} + +static void settlePullPromiseRejected(JSGlobalObject* globalObject, JSAsyncIteratorSourceOperation* op, JSValue error) +{ + auto& vm = getVM(globalObject); + op->m_done = true; + op->m_running = false; + if (auto* pullPromise = op->m_pullPromise.get()) { + op->m_pullPromise.clear(); + pullPromise->reject(vm, error); + } +} + +// The success tail: controller.end(), then iterator.return(), then resolve the pull promise. +static void asyncIterFinishSuccess(JSGlobalObject* globalObject, JSAsyncIteratorSourceOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + + JSValue endResult; + if (JSObject* controller = op->m_controller.get()) { + MarkedArgumentBuffer noArgs; + endResult = invokeOptionalMethod(globalObject, controller, WebCore::builtinNames(vm).endPublicName(), noArgs); + if (scope.exception()) [[unlikely]] { + JSValue error = takeAbruptCompletion(globalObject, scope); + asyncIterFinishWithError(globalObject, op, error ? error : jsUndefined()); + return; + } + } + if (auto* endPromise = asPromise(endResult)) { + endPromise->performPromiseThenWithContext(vm, globalObject, runtime->onAsyncIterableSourceEndFulfilled(), runtime->onAsyncIterableSourceErrored(), jsUndefined(), op); + return; + } + asyncIterReturnIteratorAndSettle(globalObject, op); +} + +// iterator.return() (so a generator's `finally` runs), then resolve the pull promise. +static void asyncIterReturnIteratorAndSettle(JSGlobalObject* globalObject, JSAsyncIteratorSourceOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + JSObject* iterator = op->m_iterator.get(); + op->m_iterator.clear(); + if (!iterator) { + settlePullPromiseResolved(globalObject, op); + return; + } + MarkedArgumentBuffer noArgs; + JSValue returned = invokeOptionalMethod(globalObject, iterator, vm.propertyNames->returnKeyword, noArgs); + if (scope.exception()) [[unlikely]] { + // The iterator's own cleanup failure is subsumed: the stream already ended. + scope.clearExceptionExceptTermination(); + settlePullPromiseResolved(globalObject, op); + return; + } + if (auto* returnPromise = asPromise(returned)) { + markPromiseAsHandled(vm, returnPromise); + returnPromise->performPromiseThenWithContext(vm, globalObject, runtime->onAsyncIterableSourceCleanupSettled(), runtime->onAsyncIterableSourceCleanupSettled(), jsUndefined(), op); + return; + } + settlePullPromiseResolved(globalObject, op); +} + +// Error tail: an already-gone consumer (ERR_INVALID_THIS) returns the iterator quietly; +// otherwise notify it via iterator.throw(error) and settle once that settles. +static void asyncIterFinishWithError(JSGlobalObject* globalObject, JSAsyncIteratorSourceOperation* op, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + + if (errorCodeIs(globalObject, error, "ERR_INVALID_THIS"_s)) { + asyncIterReturnIteratorAndSettle(globalObject, op); + return; + } + + bool swallowByCode = errorCodeIs(globalObject, error, "ERR_INVALID_STATE"_s); + + JSObject* iterator = op->m_iterator.get(); + op->m_iterator.clear(); + JSValue thrown; + if (iterator) { + MarkedArgumentBuffer args; + args.append(error); + thrown = invokeOptionalMethod(globalObject, iterator, vm.propertyNames->throwKeyword, args); + if (scope.exception()) [[unlikely]] { + // The iterator's own cleanup failure is subsumed by the original error. + scope.clearExceptionExceptTermination(); + thrown = {}; + } + } + // The cancelled check happens when the settle runs: a cancellation arriving while + // iterator.throw() is pending must still suppress the rejection. + if (auto* thrownPromise = asPromise(thrown)) { + markPromiseAsHandled(vm, thrownPromise); + auto* context = JSC::InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), op, error); + auto* handler = swallowByCode ? runtime->onAsyncIterableSourceErrorSwallowed() : runtime->onAsyncIterableSourceErrorRethrow(); + thrownPromise->performPromiseThenWithContext(vm, globalObject, handler, handler, jsUndefined(), context); + return; + } + if (swallowByCode || op->m_cancelled) { + settlePullPromiseResolved(globalObject, op); + return; + } + settlePullPromiseRejected(globalObject, op, error); +} + +enum class NextStep : uint8_t { + ContinueLoop, + Suspended, + Finished, +}; + +// One iteration result: write the value (a final `return v` is still written), honor the +// sink's backpressure protocol (`wrote < 0` -> await flush(true)), then finish when done. +static NextStep asyncIterHandleNextResult(JSGlobalObject* globalObject, JSAsyncIteratorSourceOperation* op, JSValue result) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + + JSValue doneValue = jsUndefined(); + JSValue value = jsUndefined(); + if (!result.isObject()) { + // Matches awaiting a malformed iterator: iteration results must be objects. + JSObject* error = createTypeError(globalObject, "Async iterator result is not an object"_s); + asyncIterFinishWithError(globalObject, op, error); + return NextStep::Finished; + } + { + doneValue = result.get(globalObject, vm.propertyNames->done); + if (scope.exception()) [[unlikely]] + goto abrupt; + value = result.get(globalObject, vm.propertyNames->value); + if (scope.exception()) [[unlikely]] + goto abrupt; + } + + if (doneValue.toBoolean(globalObject)) + op->m_iteratorDone = true; + + // The done/value getters run user JS that can cancel the stream. + if (op->m_cancelled) { + asyncIterReturnIteratorAndSettle(globalObject, op); + return NextStep::Finished; + } + + if (!value.isUndefinedOrNull()) { + JSObject* controller = op->m_controller.get(); + if (!controller) { + asyncIterFinishSuccess(globalObject, op); + return NextStep::Finished; + } + MarkedArgumentBuffer writeArgs; + writeArgs.append(value); + JSValue wrote = invokeOptionalMethod(globalObject, controller, WebCore::builtinNames(vm).writePublicName(), writeArgs); + if (scope.exception()) [[unlikely]] + goto abrupt; + if (wrote && wrote.isNumber() && wrote.asNumber() < 0) { + // The HTTP sink reports backpressure with a negative return: wait for the drain. + MarkedArgumentBuffer flushArgs; + flushArgs.append(jsBoolean(true)); + JSValue flushed = invokeOptionalMethod(globalObject, controller, builtinNames(vm).flushPublicName(), flushArgs); + if (scope.exception()) [[unlikely]] + goto abrupt; + JSPromise* flushPromise = asPromise(flushed); + if (!flushPromise) { + flushPromise = promiseResolvedWith(globalObject, flushed ? flushed : jsUndefined()); + if (scope.exception()) [[unlikely]] + goto abrupt; + } + flushPromise->performPromiseThenWithContext(vm, globalObject, runtime->onAsyncIterableSourceFlushFulfilled(), runtime->onAsyncIterableSourceErrored(), jsUndefined(), op); + return NextStep::Suspended; + } + if (auto* wrotePromise = asPromise(wrote)) + markPromiseAsHandled(vm, wrotePromise); + } + + if (op->m_iteratorDone) { + asyncIterFinishSuccess(globalObject, op); + return NextStep::Finished; + } + return NextStep::ContinueLoop; + +abrupt: + JSValue error = takeAbruptCompletion(globalObject, scope); + asyncIterFinishWithError(globalObject, op, error ? error : jsUndefined()); + return NextStep::Finished; +} + +// The pump loop. Synchronously-fulfilled next() results are consumed in place (writes batch +// within the tick); a pending one suspends the loop on its reactions. A non-promise result +// (including foreign thenables) is normalized through promise resolution, like `await`. +static void driveAsyncIterator(JSGlobalObject* globalObject, JSAsyncIteratorSourceOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + + while (true) { + if (op->m_done || op->m_iteratorDone) { + op->m_running = false; + return; + } + if (op->m_cancelled) { + asyncIterReturnIteratorAndSettle(globalObject, op); + return; + } + JSObject* iterator = op->m_iterator.get(); + if (!iterator) { + settlePullPromiseResolved(globalObject, op); + return; + } + MarkedArgumentBuffer nextArgs; + nextArgs.append(op->m_controller ? JSValue(op->m_controller.get()) : jsUndefined()); + JSValue nextResult; + { + JSValue nextFunction = iterator->get(globalObject, vm.propertyNames->next); + if (!scope.exception()) [[likely]] { + if (op->m_cancelled) { + // A `next` getter cancelled the stream; do not resume the iterator. + asyncIterReturnIteratorAndSettle(globalObject, op); + return; + } + nextResult = JSC::call(globalObject, nextFunction, iterator, nextArgs, "iterator.next is not a function"_s); + } + if (scope.exception()) [[unlikely]] { + JSValue error = takeAbruptCompletion(globalObject, scope); + asyncIterFinishWithError(globalObject, op, error ? error : jsUndefined()); + return; + } + } + if (op->m_cancelled) { + asyncIterReturnIteratorAndSettle(globalObject, op); + return; + } + JSPromise* nextPromise = asPromise(nextResult); + if (!nextPromise) { + // `await` semantics: adopt thenables; plain results become fulfilled promises. + nextPromise = promiseResolvedWith(globalObject, nextResult); + if (scope.exception()) [[unlikely]] { + JSValue error = takeAbruptCompletion(globalObject, scope); + asyncIterFinishWithError(globalObject, op, error ? error : jsUndefined()); + return; + } + } + auto status = nextPromise->status(); + if (status == JSPromise::Status::Fulfilled) { + if (asyncIterHandleNextResult(globalObject, op, nextPromise->result()) != NextStep::ContinueLoop) + return; + continue; + } + if (status == JSPromise::Status::Rejected) { + markPromiseAsHandled(vm, nextPromise); + asyncIterFinishWithError(globalObject, op, nextPromise->result()); + return; + } + nextPromise->performPromiseThenWithContext(vm, globalObject, runtime->onAsyncIterableSourceNextFulfilled(), runtime->onAsyncIterableSourceErrored(), jsUndefined(), op); + return; + } +} + +// -- [reaction-convention] handlers: (value, contextCell) -- + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIterableSourceNextFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(1)); + if (op->m_done) { + op->m_running = false; + return JSValue::encode(jsUndefined()); + } + if (op->m_cancelled) { + asyncIterReturnIteratorAndSettle(globalObject, op); + return JSValue::encode(jsUndefined()); + } + if (asyncIterHandleNextResult(globalObject, op, callFrame->argument(0)) == NextStep::ContinueLoop) + driveAsyncIterator(globalObject, op); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIterableSourceFlushFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(1)); + if (op->m_done) + return JSValue::encode(jsUndefined()); + if (op->m_cancelled) { + asyncIterReturnIteratorAndSettle(globalObject, op); + return JSValue::encode(jsUndefined()); + } + // The drained write may have been the iterator's final value. + if (op->m_iteratorDone) + asyncIterFinishSuccess(globalObject, op); + else + driveAsyncIterator(globalObject, op); + return JSValue::encode(jsUndefined()); +} + +// Any rejection feeding the loop (next(), flush(true), end()) takes the error path. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIterableSourceErrored, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(1)); + if (op->m_done) + return JSValue::encode(jsUndefined()); + asyncIterFinishWithError(globalObject, op, callFrame->argument(0)); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIterableSourceEndFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(1)); + asyncIterReturnIteratorAndSettle(globalObject, op); + return JSValue::encode(jsUndefined()); +} + +// Registered as both reactions of iterator.return()'s promise: the stream already ended. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIterableSourceCleanupSettled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(1)); + settlePullPromiseResolved(globalObject, op); + return JSValue::encode(jsUndefined()); +} + +// context = InternalFieldTuple{op, originalError}; iterator.throw(error) settled. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIterableSourceErrorRethrow, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* tuple = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* op = uncheckedDowncast(tuple->getInternalField(0)); + if (op->m_cancelled) { + settlePullPromiseResolved(globalObject, op); + return JSValue::encode(jsUndefined()); + } + settlePullPromiseRejected(globalObject, op, tuple->getInternalField(1)); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIterableSourceErrorSwallowed, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* tuple = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* op = uncheckedDowncast(tuple->getInternalField(0)); + settlePullPromiseResolved(globalObject, op); + return JSValue::encode(jsUndefined()); +} + +// -- [bound-convention] direct-source methods: (opCell, ...callArgs) -- + +// pull(controller): one drive of the iterator runs at a time; every pull while it runs gets +// the same promise. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundAsyncIterableSourcePull, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(0)); + if (op->m_done || op->m_cancelled) + return JSValue::encode(jsUndefined()); + if (JSObject* controller = callFrame->argument(1).getObject()) + op->m_controller.set(vm, op, controller); + if (op->m_running) { + if (auto* pullPromise = op->m_pullPromise.get()) + return JSValue::encode(pullPromise); + return JSValue::encode(jsUndefined()); + } + auto* pullPromise = JSPromise::create(vm, globalObject->promiseStructure()); + op->m_pullPromise.set(vm, op, pullPromise); + op->m_running = true; + driveAsyncIterator(globalObject, op); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(pullPromise); +} + +// cancel(reason): reason ? iterator.throw(reason) : iterator.return(); the result is +// returned so the stream's cancel promise chains onto it. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundAsyncIterableSourceCancel, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(0)); + op->m_cancelled = true; + JSObject* iterator = op->m_iterator.get(); + op->m_iterator.clear(); + // The pump is abandoned: whatever awaited pull() resolves, like the old converter. + settlePullPromiseResolved(globalObject, op); + if (!iterator) + return JSValue::encode(jsUndefined()); + JSValue reason = callFrame->argument(1); + MarkedArgumentBuffer args; + JSValue result; + // Truthiness, not definedness: an absent/falsy reason means a graceful return(), never + // an injected throw (which would surface as an uncatchable rejection). + if (reason.toBoolean(globalObject)) { + args.append(reason); + result = invokeOptionalMethod(globalObject, iterator, vm.propertyNames->throwKeyword, args); + } else + result = invokeOptionalMethod(globalObject, iterator, vm.propertyNames->returnKeyword, args); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(result ? result : jsUndefined()); +} + +// close(): the consumer is gone; the iterator's finally still runs via return(). +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundAsyncIterableSourceClose, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(0)); + op->m_cancelled = true; + asyncIterReturnIteratorAndSettle(globalObject, op); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSAsyncIteratorSourceOperation; +using WebCore::JSReadableStream; +using WebCore::JSStreamsRuntime; + +// An `async function*` value is not itself async-iterable; ReadableStreamTag__tagged and +// readableStreamFromAsyncIterator both accept one and start it eagerly. +bool isNonHostAsyncGeneratorFunction(JSObject* object) +{ + auto* function = dynamicDowncast(object); + return function && !function->isHostFunction() && function->jsExecutable() && function->jsExecutable()->isAsyncGenerator(); +} + +// Bun's async-iterable body extension: a DIRECT stream driven natively (the spec's +// ReadableStream.from() semantics are NOT used here). The iterator starts eagerly so that +// reused objects work. +JSReadableStream* readableStreamFromAsyncIterator(JSGlobalObject* globalObject, JSValue asyncIterableOrGeneratorFn) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + auto& names = WebCore::builtinNames(vm); + + JSValue target = jsUndefined(); + JSValue iteratorFn = asyncIterableOrGeneratorFn; + if (JSObject* object = asyncIterableOrGeneratorFn.getObject(); object && !isNonHostAsyncGeneratorFunction(object)) { + iteratorFn = object->get(globalObject, vm.propertyNames->asyncIteratorSymbol); + RETURN_IF_EXCEPTION(scope, nullptr); + target = object; + } + + auto callData = JSC::getCallData(iteratorFn); + if (callData.type == JSC::CallData::Type::None) { + throwTypeError(globalObject, scope, "Expected an async generator"_s); + return nullptr; + } + MarkedArgumentBuffer noArgs; + JSValue iteratorValue = JSC::call(globalObject, iteratorFn, callData, target, noArgs); + RETURN_IF_EXCEPTION(scope, nullptr); + JSObject* iterator = iteratorValue.getObject(); + JSValue nextMethod = iterator ? iterator->get(globalObject, vm.propertyNames->next) : jsUndefined(); + RETURN_IF_EXCEPTION(scope, nullptr); + if (!nextMethod.isCallable()) { + throwTypeError(globalObject, scope, "Expected an async generator"_s); + return nullptr; + } + + auto* op = JSAsyncIteratorSourceOperation::create(vm, runtime->asyncIteratorSourceOperationStructure(zigGlobalObject)); + op->m_iterator.set(vm, op, iterator); + + auto* source = constructEmptyObject(globalObject); + source->putDirect(vm, names.typePublicName(), jsString(vm, String("direct"_s)), 0); + auto* pullFunction = createStreamsBoundHandler(globalObject, runtime->boundAsyncIterableSourcePull(), op); + RETURN_IF_EXCEPTION(scope, nullptr); + source->putDirect(vm, names.pullPublicName(), pullFunction, 0); + auto* cancelFunction = createStreamsBoundHandler(globalObject, runtime->boundAsyncIterableSourceCancel(), op); + RETURN_IF_EXCEPTION(scope, nullptr); + source->putDirect(vm, builtinNames(vm).cancelPublicName(), cancelFunction, 0); + auto* closeFunction = createStreamsBoundHandler(globalObject, runtime->boundAsyncIterableSourceClose(), op); + RETURN_IF_EXCEPTION(scope, nullptr); + source->putDirect(vm, names.closePublicName(), closeFunction, 0); + + auto* stream = JSReadableStream::create(vm, WebCore::getDOMStructure(vm, *zigGlobalObject)); + initializeReadableStream(stream); + stream->m_bunMode = WebCore::BunStreamMode::DirectPending; + stream->m_directUnderlyingSource.set(vm, stream, source); + return stream; +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/BunStandaloneTextSink.h b/src/jsc/bindings/webcore/streams/BunStandaloneTextSink.h new file mode 100644 index 000000000000..5c9a0fb9d144 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/BunStandaloneTextSink.h @@ -0,0 +1,104 @@ +// BunStandaloneTextSink.h — the GENERIC `toText` accumulator owner cell. +// `convertChunksToText` (BunStreamConsumers.cpp) allocates ONE of these and drives the +// shared accumulator through it (the cell is the GC owner of the accumulated chunk +// barriers). It is deliberately DISTINCT from `JSDirectStreamController`'s Text arm (the +// two have different BOM behaviors); the accumulation LOGIC is shared through the ONE +// `BunTextAccumulator` value type below — "one implementation, two owners". +// Internal cell: no prototype, no constructor, never exposed to JS. +// DESTRUCTIBLE: the accumulator owns a WTF::StringBuilder + a WTF::Vector of barriers. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +// The shared Text accumulator ("one implementation, two owners") — the `createTextStream` +// rope + pieces state, owned BY VALUE by BOTH `WebCore::JSBunStandaloneTextSink` (below) and +// `JSDirectStreamController`'s Text arm. NOT a cell (namespace Bun::WebStreams like every +// non-cell struct). `pieces` is a barrier container: the OWNING cell mutates AND visits it +// inside its ONE `Locker { cellLock() }` scope and proves that with the AbstractLocker +// parameter (cellLock() is non-recursive — see StreamQueue.h's discipline comment). +struct BunTextAccumulator { + // the pure-string fast-path rope. RecordOverflow: an append past + // StringImpl::MaxLength must surface as a catchable out-of-memory error at the + // write site, never as the default policy's process abort. + WTF::StringBuilder rope { WTF::OverflowPolicy::RecordOverflow }; + // string + typed-array-view pieces (the mixed path). + WTF::Vector> pieces; + double estimatedLength { 0 }; + bool hasString { false }; + bool hasBuffer { false }; + + // Releases everything accumulated. Called as soon as the final result string has + // been materialized so a long-lived owner (the direct stream's controller) does + // not retain the whole payload until it is collected. Takes the owning cell's + // lock like visit(): `pieces` is a barrier container. + void reset(const WTF::AbstractLocker&) + { + pieces.clear(); + pieces.shrinkToFit(); + rope.clear(); + estimatedLength = 0; + hasString = false; + hasBuffer = false; + } + + // Appends every barrier in `pieces`. Called from the OWNING cell's visitChildrenImpl, + // inside that cell's single cellLock() scope. + template + void visit(const WTF::AbstractLocker&, Visitor& visitor) + { + for (auto& piece : pieces) + visitor.append(piece); + } +}; + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +class JSBunStandaloneTextSink final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + static JSBunStandaloneTextSink* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit the barrier container m_accumulator.pieces (via + // m_accumulator.visit(locker, visitor) inside ONE `Locker { cellLock() }` scope). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The shared accumulator (see BunTextAccumulator above). userJS: the write arm can + // run chunk getters; the owner of this cell holds no raw pointers across it. + Bun::WebStreams::BunTextAccumulator m_accumulator; + +private: + JSBunStandaloneTextSink(JSC::VM&, JSC::Structure*); + ~JSBunStandaloneTextSink(); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp b/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp new file mode 100644 index 000000000000..9b95e54306a0 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/BunStreamConsumers.cpp @@ -0,0 +1,1736 @@ +#include "config.h" +#include "BunStreamConsumers.h" + +#include "BufferEncodingType.h" +#include "BunClientData.h" +#include "BunObject.h" +#include "BunStandaloneTextSink.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "ErrorCode.h" +#include "helpers.h" +#include "JSDOMFormData.h" +#include "JSDOMGlobalObject.h" +#include "JSDirectStreamController.h" +#include "JSOneShotDirectSink.h" +#include "JSReadableStreamIntoArrayOperation.h" +#include "JSReadRequest.h" +#include "JSReadableStream.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSStreamsRuntime.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +// JSBunStandaloneTextSink — the GENERIC toText accumulator cell (BunStandaloneTextSink.h). + +const ClassInfo JSBunStandaloneTextSink::s_info = { "BunStandaloneTextSink"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSBunStandaloneTextSink) }; + +JSBunStandaloneTextSink::JSBunStandaloneTextSink(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSBunStandaloneTextSink::~JSBunStandaloneTextSink() = default; + +void JSBunStandaloneTextSink::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSBunStandaloneTextSink* JSBunStandaloneTextSink::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSBunStandaloneTextSink(vm, structure); + cell->finishCreation(vm); + return cell; +} + +void JSBunStandaloneTextSink::destroy(JSCell* cell) +{ + static_cast(cell)->JSBunStandaloneTextSink::~JSBunStandaloneTextSink(); +} + +Structure* JSBunStandaloneTextSink::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSBunStandaloneTextSink::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForBunStandaloneTextSink.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForBunStandaloneTextSink = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForBunStandaloneTextSink.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForBunStandaloneTextSink = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSBunStandaloneTextSink); + +template +void JSBunStandaloneTextSink::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + WTF::Locker locker { thisObject->cellLock() }; + thisObject->m_accumulator.visit(locker, visitor); +} + +// JSOneShotDirectSink — consumeDirectStreamToArrayBuffer's throwaway controller cell. + +const ClassInfo JSOneShotDirectSink::s_info = { "OneShotDirectSink"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSOneShotDirectSink) }; + +JSOneShotDirectSink::JSOneShotDirectSink(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSOneShotDirectSink::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSOneShotDirectSink* JSOneShotDirectSink::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSOneShotDirectSink(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSOneShotDirectSink::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSOneShotDirectSink::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForOneShotDirectSink.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForOneShotDirectSink = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForOneShotDirectSink.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForOneShotDirectSink = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSOneShotDirectSink); + +template +void JSOneShotDirectSink::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_arrayBufferSink); + visitor.append(thisObject->m_capabilityPromise); + visitor.append(thisObject->m_closeFunction); +} + +// JSReadableStreamIntoArrayOperation — the queue-backed array pump's persistent state. + +const ClassInfo JSReadableStreamIntoArrayOperation::s_info = { "ReadableStreamIntoArrayOperation"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamIntoArrayOperation) }; + +JSReadableStreamIntoArrayOperation::JSReadableStreamIntoArrayOperation(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSReadableStreamIntoArrayOperation::finishCreation(VM& vm, JSReadableStreamDefaultReader* reader, JSArray* chunks, JSPromise* result) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + m_reader.set(vm, this, reader); + m_chunks.set(vm, this, chunks); + m_result.set(vm, this, result); +} + +JSReadableStreamIntoArrayOperation* JSReadableStreamIntoArrayOperation::create(VM& vm, Structure* structure, JSReadableStreamDefaultReader* reader, JSArray* chunks, JSPromise* result) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSReadableStreamIntoArrayOperation(vm, structure); + cell->finishCreation(vm, reader, chunks, result); + return cell; +} + +Structure* JSReadableStreamIntoArrayOperation::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSReadableStreamIntoArrayOperation::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamIntoArrayOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamIntoArrayOperation = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamIntoArrayOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamIntoArrayOperation = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadableStreamIntoArrayOperation); + +template +void JSReadableStreamIntoArrayOperation::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_reader); + visitor.append(thisObject->m_chunks); + visitor.append(thisObject->m_result); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSBunStandaloneTextSink; +using WebCore::JSDirectStreamController; +using WebCore::JSOneShotDirectSink; +using WebCore::JSReadableStreamIntoArrayOperation; +using WebCore::JSReadRequest; +using WebCore::JSStreamsRuntime; + +WTF::String withoutUTF8BOM(const WTF::String& string) +{ + if (string.length() && string[0] == 0xFEFF) + return string.substring(1); + return string; +} + +// The generic toText result strip: the accumulator's rope-path strip followed by the +// end()-path strip of the sink pump this replaced (so "\uFEFF\uFEFF..." loses both). +static WTF::String stripTextResultBOM(const WTF::String& string) +{ + return withoutUTF8BOM(withoutUTF8BOM(string)); +} + +// UTF-8 size / write via the simdutf-backed Buffer encoders. Lone surrogates count (and +// write) as U+FFFD, so the pair always agrees; BunString::utf8ByteLength does not. +static size_t utf8ByteLengthWithReplacement(const WTF::String& string) +{ + if (string.isEmpty()) + return 0; + if (string.is8Bit()) + return Bun__encoding__byteLengthLatin1AsUTF8(string.span8().data(), string.span8().size()); + return Bun__encoding__byteLengthUTF16AsUTF8(string.span16().data(), string.span16().size()); +} + +static size_t writeUTF8(const WTF::String& string, std::span destination) +{ + if (string.isEmpty()) + return 0; + constexpr auto utf8 = static_cast(WebCore::BufferEncodingType::utf8); + if (string.is8Bit()) + return Bun__encoding__writeLatin1(string.span8().data(), string.span8().size(), destination.data(), destination.size(), utf8); + return Bun__encoding__writeUTF16(string.span16().data(), string.span16().size(), destination.data(), destination.size(), utf8); +} + +// `obj[name](...args)` with `this` = obj. +static JSValue invokeMethod(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* object, const Identifier& name, const MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue method = object->get(globalObject, name); + RETURN_IF_EXCEPTION(scope, {}); + auto callData = JSC::getCallData(method); + if (callData.type == CallData::Type::None) [[unlikely]] { + throwTypeError(globalObject, scope, makeString(name.string(), " is not a function"_s)); + return {}; + } + RELEASE_AND_RETURN(scope, JSC::call(globalObject, method, callData, object, args)); +} + +static JSC::JSUint8Array* encodeStringToUint8Array(JSC::VM& vm, JSGlobalObject* globalObject, JSValue stringValue) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + WTF::String string = stringValue.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + // The same simdutf sizer/writer pair the chunk appender uses: one sizing pass, one + // encode straight into the result (no intermediate CString copy). The result is + // buffer-backed from birth so a later `.buffer` access never has to change modes. + size_t byteLength = utf8ByteLengthWithReplacement(string); + RefPtr resultBuffer = JSC::ArrayBuffer::tryCreateUninitialized(byteLength, 1); + if (!resultBuffer) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + if (byteLength) { + size_t written = writeUTF8(string, { static_cast(resultBuffer->data()), byteLength }); + ASSERT_UNUSED(written, written == byteLength); + } + auto* structure = globalObject->typedArrayStructureWithTypedArrayType(); + RELEASE_AND_RETURN(scope, JSC::JSUint8Array::create(globalObject, structure, WTF::move(resultBuffer), 0, byteLength)); +} + +static bool appendChunkBytes(JSC::VM& vm, JSGlobalObject* globalObject, JSValue chunk, WTF::Vector& bytes) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (chunk.isString()) { + WTF::String string = asString(chunk)->value(globalObject); + RETURN_IF_EXCEPTION(scope, false); + if (size_t byteLength = utf8ByteLengthWithReplacement(string)) { + size_t oldSize = bytes.size(); + bytes.grow(oldSize + byteLength); + size_t written = writeUTF8(string, bytes.mutableSpan().subspan(oldSize)); + // The sizer and writer must agree; never expose ungrown (uninitialized) bytes. + ASSERT(written == byteLength); + if (written < byteLength) [[unlikely]] + bytes.shrink(oldSize + written); + } + return true; + } + if (auto* view = dynamicDowncast(chunk)) { + if (!view->isDetached()) + bytes.append(view->span()); + return true; + } + if (auto* jsBuffer = dynamicDowncast(chunk)) { + if (auto* impl = jsBuffer->impl(); impl && !impl->isDetached()) + bytes.append(impl->span()); + return true; + } + throwTypeError(globalObject, scope, "Expected an ArrayBuffer, ArrayBufferView, or string chunk"_s); + return false; +} + +// The N-chunk concatenation shared by toArrayBuffer / toBytes (the concatArrayBuffers / +// ArrayBufferSink arms of RS:157-289 produce the same bytes; only the wrapper type differs). +static JSValue concatenateChunks(JSC::VM& vm, JSGlobalObject* globalObject, JSArray* chunks, bool asUint8Array) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + unsigned length = chunks->length(); + + // ONE pass over the array: read each element exactly once, materialize each string + // exactly once, and size the output as we go. `values` roots every chunk across the + // string materializations; `stringChunks` carries each string and its UTF-8 size so + // the write pass below never re-reads the array or re-encodes. + MarkedArgumentBuffer values; + WTF::Vector, 16> stringChunks; + bool anyString = false; + WTF::CheckedSize total = 0; + for (unsigned i = 0; i < length; i++) { + JSValue chunk = chunks->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, {}); + values.append(chunk); + if (chunk.isString()) { + anyString = true; + WTF::String string = asString(chunk)->value(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + size_t byteLength = utf8ByteLengthWithReplacement(string); + total += byteLength; + stringChunks.append({ WTF::move(string), byteLength }); + continue; + } + stringChunks.append({ WTF::String(), 0 }); + if (auto* view = dynamicDowncast(chunk)) + total += view->isDetached() ? 0 : view->byteLength(); + else if (auto* jsBuffer = dynamicDowncast(chunk)) + total += (jsBuffer->impl() && !jsBuffer->impl()->isDetached()) ? jsBuffer->impl()->byteLength() : 0; + else { + throwTypeError(globalObject, scope, "Expected an ArrayBuffer, ArrayBufferView, or string chunk"_s); + return {}; + } + } + if (values.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + // All-binary chunk arrays (the hot path) use `Bun.concatArrayBuffers`' single-allocation + // concatenation, exactly as the previous implementation did. + if (!anyString) + RELEASE_AND_RETURN(scope, JSValue::decode(Bun::flattenArrayOfBuffersIntoArrayBufferOrUint8Array(globalObject, chunks, std::numeric_limits::max(), asUint8Array))); + + if (total.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + WTF::Vector bytes; + bytes.reserveInitialCapacity(total.value()); + for (unsigned i = 0; i < length; i++) { + auto& [string, stringByteLength] = stringChunks[i]; + if (!string.isNull()) { + if (stringByteLength) { + size_t oldSize = bytes.size(); + bytes.grow(oldSize + stringByteLength); + size_t written = writeUTF8(string, bytes.mutableSpan().subspan(oldSize)); + // The sizer and writer must agree; never expose ungrown (uninitialized) bytes. + ASSERT(written == stringByteLength); + if (written < stringByteLength) [[unlikely]] + bytes.shrink(oldSize + written); + } + continue; + } + JSValue chunk = values.at(i); + if (auto* view = dynamicDowncast(chunk)) { + if (!view->isDetached()) + bytes.append(view->span()); + } else if (auto* jsBuffer = dynamicDowncast(chunk)) { + if (auto* impl = jsBuffer->impl(); impl && !impl->isDetached()) + bytes.append(impl->span()); + } + } + if (asUint8Array) { + // Buffer-backed from birth: a later `.buffer` access never has to change modes. + RefPtr resultBuffer = JSC::ArrayBuffer::tryCreate(bytes.span()); + if (!resultBuffer) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + auto* structure = globalObject->typedArrayStructureWithTypedArrayType(); + RELEASE_AND_RETURN(scope, JSC::JSUint8Array::create(globalObject, structure, WTF::move(resultBuffer), 0, bytes.size())); + } + auto buffer = JSC::ArrayBuffer::tryCreate(bytes.span()); + if (!buffer) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + return JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(JSC::ArrayBufferSharingMode::Default), WTF::move(buffer)); +} + +// The toArrayBuffer chunk-array converter (RS:157-206). +static JSValue convertChunksToArrayBuffer(JSGlobalObject* globalObject, JSValue chunksValue) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* chunks = dynamicDowncast(chunksValue); + if (!chunks) [[unlikely]] { + throwTypeError(globalObject, scope, "Expected an array of chunks"_s); + return {}; + } + unsigned length = chunks->length(); + if (!length) { + auto buffer = JSC::ArrayBuffer::tryCreate(size_t { 0 }, 1); + if (!buffer) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + return JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(JSC::ArrayBufferSharingMode::Default), WTF::move(buffer)); + } + if (length == 1) { + JSValue chunk = chunks->getIndex(globalObject, 0); + RETURN_IF_EXCEPTION(scope, {}); + if (auto* jsBuffer = dynamicDowncast(chunk)) + return jsBuffer; + if (auto* view = dynamicDowncast(chunk)) { + RefPtr impl = view->possiblySharedBuffer(); + if (impl && !view->byteOffset() && view->byteLength() == impl->byteLength()) { + auto* jsBuffer = view->possiblySharedJSBuffer(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return jsBuffer; + } + auto copied = JSC::ArrayBuffer::tryCreate(view->span()); + if (!copied) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + return JSC::JSArrayBuffer::create(vm, globalObject->arrayBufferStructure(JSC::ArrayBufferSharingMode::Default), WTF::move(copied)); + } + if (chunk.isString()) + RELEASE_AND_RETURN(scope, encodeStringToUint8Array(vm, globalObject, chunk)); + } + RELEASE_AND_RETURN(scope, concatenateChunks(vm, globalObject, chunks, /* asUint8Array */ false)); +} + +// The toBytes chunk-array converter (RS:238-283). +static JSValue convertChunksToBytes(JSGlobalObject* globalObject, JSValue chunksValue) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* chunks = dynamicDowncast(chunksValue); + if (!chunks) [[unlikely]] { + throwTypeError(globalObject, scope, "Expected an array of chunks"_s); + return {}; + } + auto* structure = globalObject->typedArrayStructureWithTypedArrayType(); + unsigned length = chunks->length(); + if (!length) + RELEASE_AND_RETURN(scope, JSC::JSUint8Array::create(globalObject, structure, size_t { 0 })); + if (length == 1) { + JSValue chunk = chunks->getIndex(globalObject, 0); + RETURN_IF_EXCEPTION(scope, {}); + if (auto* uint8 = dynamicDowncast(chunk)) + return uint8; + if (auto* view = dynamicDowncast(chunk)) { + size_t byteOffset = view->byteOffset(); + size_t byteLength = view->byteLength(); + RefPtr impl = view->possiblySharedBuffer(); + RELEASE_AND_RETURN(scope, JSC::JSUint8Array::create(globalObject, structure, WTF::move(impl), byteOffset, byteLength)); + } + if (auto* jsBuffer = dynamicDowncast(chunk)) { + RefPtr impl = jsBuffer->impl(); + size_t byteLength = impl ? impl->byteLength() : 0; + RELEASE_AND_RETURN(scope, JSC::JSUint8Array::create(globalObject, structure, WTF::move(impl), 0, byteLength)); + } + if (chunk.isString()) + RELEASE_AND_RETURN(scope, encodeStringToUint8Array(vm, globalObject, chunk)); + } + RELEASE_AND_RETURN(scope, concatenateChunks(vm, globalObject, chunks, /* asUint8Array */ true)); +} + +static JSValue textAccumulatorWrite(JSC::VM& vm, JSGlobalObject*, JSC::JSObject* owner, BunTextAccumulator&, JSValue chunk); +static WTF::String finishTextAccumulator(JSC::VM& vm, JSGlobalObject*, JSC::JSObject* owner, BunTextAccumulator&); + +// The chunk-array -> text conversion: pure-string arrays join once (no UTF-8 round trip); +// mixed/binary chunk arrays run through the shared text accumulator. +static JSValue convertChunksToText(JSGlobalObject* globalObject, JSValue chunksValue) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* chunks = dynamicDowncast(chunksValue); + if (!chunks) [[unlikely]] { + throwTypeError(globalObject, scope, "Expected an array of chunks"_s); + return {}; + } + unsigned length = chunks->length(); + if (!length) + return jsEmptyString(vm); + + if (length == 1) { + JSValue chunk = chunks->getIndex(globalObject, 0); + RETURN_IF_EXCEPTION(scope, {}); + if (chunk.isString()) { + WTF::String string = asString(chunk)->value(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + WTF::String stripped = stripTextResultBOM(string); + if (stripped.impl() == string.impl()) + return chunk; + RELEASE_AND_RETURN(scope, jsString(vm, stripped)); + } + bool isBinary = false; + std::span span; + if (auto* view = dynamicDowncast(chunk)) { + isBinary = true; + span = view->isDetached() ? std::span {} : view->span(); + } else if (auto* jsBuffer = dynamicDowncast(chunk)) { + isBinary = true; + if (auto* impl = jsBuffer->impl(); impl && !impl->isDetached()) + span = impl->span(); + } + if (isBinary) { + if (exceedsStringLimit(span.size())) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + WTF::String text = WTF::String::fromUTF8ReplacingInvalidSequences(span); + RELEASE_AND_RETURN(scope, jsString(vm, withoutUTF8BOM(text))); + } + } + + // ONE pass over the array: every element is read exactly once and held in a + // MarkedArgumentBuffer for the conversion below. + MarkedArgumentBuffer values; + bool allStrings = true; + WTF::CheckedUint32 codeUnits = 0; + for (unsigned i = 0; i < length; i++) { + JSValue chunk = chunks->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, {}); + values.append(chunk); + if (!chunk.isString()) + allStrings = false; + else if (allStrings) + codeUnits += asString(chunk)->length(); + } + if (values.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + if (allStrings) { + if (codeUnits.hasOverflowed() || exceedsStringLimit(codeUnits.value())) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + WTF::StringBuilder rope; + rope.reserveCapacity(codeUnits.value()); + for (unsigned i = 0; i < length; i++) { + WTF::String string = asString(values.at(i))->value(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + rope.append(string); + } + if (rope.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + RELEASE_AND_RETURN(scope, jsString(vm, stripTextResultBOM(rope.toString()))); + } + + // Mixed string/binary chunks: drive the shared accumulator so adjacent-string rope + // joining, the flush-on-buffer ordering, and both BOM strips stay identical. + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* sink = WebCore::JSBunStandaloneTextSink::create(vm, runtime->standaloneTextSinkStructure(domGlobalObject)); + for (unsigned i = 0; i < length; i++) { + textAccumulatorWrite(vm, globalObject, sink, sink->m_accumulator, values.at(i)); + RETURN_IF_EXCEPTION(scope, {}); + } + WTF::String text = finishTextAccumulator(vm, globalObject, sink, sink->m_accumulator); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, jsString(vm, withoutUTF8BOM(text))); +} + +static JSObject* createLockedError(JSGlobalObject* globalObject) +{ + return Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream is locked"_s); +} + +// Consuming an already-consumed (disturbed, no longer locked) stream must reject +// instead of resolving with an empty result: https://github.com/oven-sh/bun/issues/6860 +static JSObject* createAlreadyUsedError(JSGlobalObject* globalObject) +{ + return Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream has already been used"_s); +} + +// The one shared `BunTextAccumulator` write arm (createTextStream.write, RSI:1411-1441). +static JSValue textAccumulatorWrite(JSC::VM& vm, JSGlobalObject* globalObject, JSC::JSObject* owner, BunTextAccumulator& accumulator, JSValue chunk) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (chunk.isString()) { + WTF::String string = asString(chunk)->value(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + unsigned length = string.length(); + if (length) { + accumulator.rope.append(string); + if (accumulator.rope.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + accumulator.hasString = true; + accumulator.estimatedLength += length; + } + return jsNumber(length); + } + size_t byteLength = 0; + if (auto* view = dynamicDowncast(chunk)) + byteLength = view->isDetached() ? 0 : view->byteLength(); + else if (auto* jsBuffer = dynamicDowncast(chunk)) + byteLength = jsBuffer->impl() ? jsBuffer->impl()->byteLength() : 0; + else { + throwTypeError(globalObject, scope, "Expected text, ArrayBuffer or ArrayBufferView"_s); + return {}; + } + if (byteLength) { + accumulator.hasBuffer = true; + JSC::JSString* flushedRope = nullptr; + if (accumulator.rope.length()) { + flushedRope = jsString(vm, accumulator.rope.toString()); + RETURN_IF_EXCEPTION(scope, {}); + accumulator.rope.clear(); + } + WTF::Locker locker { owner->cellLock() }; + if (flushedRope) + accumulator.pieces.append(JSC::WriteBarrier(vm, owner, flushedRope)); + accumulator.pieces.append(JSC::WriteBarrier(vm, owner, chunk)); + } + accumulator.estimatedLength += byteLength; + return jsNumber(static_cast(byteLength)); +} + +// createTextStream.finishInternal (RSI:1463-1501). Does NOT strip the leading UTF-8 BOM on +// the buffer / mixed paths (only the pure-string rope path strips it) — see withoutUTF8BOM. +static WTF::String finishTextAccumulator(JSC::VM& vm, JSGlobalObject* globalObject, JSC::JSObject* owner, BunTextAccumulator& accumulator) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + // Once the result is materialized nothing may keep the accumulated payload alive: + // release at every return below (the owner can outlive this call by a lot). + auto releaseAccumulated = [&] { + WTF::Locker locker { owner->cellLock() }; + accumulator.reset(locker); + }; + if (!accumulator.hasString && !accumulator.hasBuffer) + return WTF::emptyString(); + if (accumulator.hasString && !accumulator.hasBuffer) { + if (exceedsStringLimit(accumulator.rope.length())) [[unlikely]] { + releaseAccumulated(); + throwOutOfMemoryError(globalObject, scope); + return WTF::String(); + } + WTF::String rope = accumulator.rope.toString(); + releaseAccumulated(); + if (rope.length() && rope[0] == 0xFEFF) + return rope.substring(1); + return rope; + } + WTF::Vector bytes; + if (accumulator.estimatedLength > 0 && accumulator.estimatedLength < static_cast(std::numeric_limits::max())) + bytes.reserveInitialCapacity(static_cast(accumulator.estimatedLength)); + for (auto& piece : accumulator.pieces) { + JSValue value = piece.get(); + if (!value) + continue; + bool appended = appendChunkBytes(vm, globalObject, value, bytes); + RETURN_IF_EXCEPTION(scope, WTF::String()); + if (!appended) + return WTF::String(); + } + if (accumulator.rope.length()) { + WTF::String rope = accumulator.rope.toString(); + if (rope[0] == 0xFEFF) + rope = rope.substring(1); + WTF::CString utf8 = rope.utf8(); + bytes.append(std::span { reinterpret_cast(utf8.data()), utf8.length() }); + } + releaseAccumulated(); + if (exceedsStringLimit(bytes.size())) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return WTF::String(); + } + return WTF::String::fromUTF8ReplacingInvalidSequences(bytes.span()); +} + +// reader.read() as a Promise-kind read request. +static JSPromise* readerReadAsPromise(JSC::VM& vm, JSGlobalObject* globalObject, WebCore::JSReadableStreamDefaultReader* reader) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* request = JSReadRequest::create(vm, runtime->readRequestStructure(domGlobalObject), ReadRequestKind::Promise, promise); + readableStreamDefaultReaderRead(globalObject, reader, request); + RETURN_IF_EXCEPTION(scope, nullptr); + return promise; +} + +// The readableStreamIntoArray readMany continuation. Runs synchronously until readMany +// returns a promise, then chains the next hop onto a fresh derived promise it returns. +static JSValue intoArrayLoop(JSC::VM& vm, JSGlobalObject* globalObject, WebCore::JSReadableStreamDefaultReader* reader, JSArray* chunks, JSValue manyResult) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + JSValue many = manyResult; + while (true) { + if (auto* manyPromise = dynamicDowncast(many)) { + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* context = InternalFieldTuple::create(vm, domGlobalObject->internalFieldTupleStructure(), reader, chunks); + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + manyPromise->performPromiseThenWithContext(vm, globalObject, runtime->onIntoArrayReadManyFulfilled(), runtime->onIntoArrayReadManyRejected(), derived, context); + return derived; + } + JSObject* result = many.getObject(); + if (!result) [[unlikely]] { + throwTypeError(globalObject, scope, "readMany() did not return an object"_s); + return {}; + } + JSValue doneValue = result->get(globalObject, vm.propertyNames->done); + RETURN_IF_EXCEPTION(scope, {}); + JSValue value = result->get(globalObject, vm.propertyNames->value); + RETURN_IF_EXCEPTION(scope, {}); + if (auto* valueArray = dynamicDowncast(value)) { + unsigned valueLength = valueArray->length(); + for (unsigned i = 0; i < valueLength; i++) { + JSValue element = valueArray->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, {}); + chunks->push(globalObject, element); + RETURN_IF_EXCEPTION(scope, {}); + } + } + if (doneValue.toBoolean(globalObject)) { + readableStreamDefaultReaderRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + return chunks; + } + many = readableStreamDefaultReaderReadMany(globalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + } +} + +JSValue readableStreamIntoArray(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + stream->materializeIfNeeded(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto* reader = acquireReadableStreamDefaultReader(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + auto* chunks = constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + bool isQueueBacked = stream->m_controllerKind == ControllerKind::Default || stream->m_controllerKind == ControllerKind::Byte; + if (!isQueueBacked) { + // Direct (and controller-less) streams keep the generic readMany loop. + JSValue result; + { + // readMany() throws synchronously on an already-errored stream; convert every + // synchronous abrupt completion to a rejection. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + JSValue many = readableStreamDefaultReaderReadMany(globalObject, reader); + if (!catchScope.exception()) + result = intoArrayLoop(vm, globalObject, reader, chunks, many); + if (catchScope.exception()) { + JSValue error = takeAbruptCompletion(globalObject, catchScope); + if (error.isEmpty()) + return {}; + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, error)); + } + } + if (auto* promise = dynamicDowncast(result)) + return promise; + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, result)); + } + // Queue-backed streams: one persistent op {reader, chunks, result promise} carries the + // pump across every read, so a pending hop costs one reaction registration and nothing else. + JSPromise* pendingRead = nullptr; + JSValue thrown; + ConsumerFillStep step = ConsumerFillStep::Done; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + step = readableStreamDefaultReaderFillFromQueue(globalObject, reader, chunks, &pendingRead); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return {}; + } + } + if (!thrown.isEmpty()) [[unlikely]] + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (step == ConsumerFillStep::Done) { + readableStreamDefaultReaderRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, chunks)); + } + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* op = JSReadableStreamIntoArrayOperation::create(vm, runtime->intoArrayOperationStructure(domGlobalObject), reader, chunks, resultPromise); + pendingRead->performPromiseThenWithContext(vm, globalObject, runtime->onIntoArrayReadFulfilled(), runtime->onIntoArrayReadRejected(), jsUndefined(), op); + RETURN_IF_EXCEPTION(scope, {}); + return resultPromise; +} + +enum class ChunkArrayConversion : uint8_t { ArrayBuffer, + Bytes, + Text }; +static JSValue convertChunkArrayPromise(JSC::VM& vm, JSGlobalObject*, JSValue arrayResult, ChunkArrayConversion); + +JSValue readableStreamIntoText(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue arrayResult = readableStreamIntoArray(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, convertChunkArrayPromise(vm, globalObject, arrayResult, ChunkArrayConversion::Text)); +} + +// The buffered-native fast path (RSI:1240-1268). +JSValue tryUseReadableStreamBufferedFastPath(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, const Identifier& method) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue nativePtr = stream->nativePtrForJS(); + if (!nativePtr || !nativePtr.isCell()) + return {}; + JSObject* handle = nativePtr.getObject(); + if (!handle) + return {}; + if (stream->m_disturbed) + return {}; + JSValue methodValue = handle->get(globalObject, method); + RETURN_IF_EXCEPTION(scope, {}); + if (!methodValue.isCallable()) + return {}; + auto callData = JSC::getCallData(methodValue); + MarkedArgumentBuffer noArguments; + JSValue promiseValue = JSC::call(globalObject, methodValue, callData, handle, noArguments); + // If the native call throws, propagate WITHOUT setting m_disturbed. + RETURN_IF_EXCEPTION(scope, {}); + stream->m_disturbed = true; + stream->m_bunMode = BunStreamMode::Default; + stream->m_lockedWithoutReader = true; + auto* promise = dynamicDowncast(promiseValue); + if (!promise) [[unlikely]] + return promiseValue; + if (promise->status() == JSPromise::Status::Fulfilled) { + stream->m_lockedWithoutReader = false; + readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return promise; + } + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + promise->performPromiseThenWithContext(vm, globalObject, runtime->onBufferedFastPathSettled(), runtime->onBufferedFastPathRejected(), derived, stream); + return derived; +} + +// The direct read loop shared by readableStreamTo{Text,Array}Direct. +// context tuple = { stream, reader }. + +static JSValue finishDirectConsumeLoop(JSC::VM& vm, JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, WebCore::JSReadableStreamDefaultReader* reader) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (reader->m_stream) { + readableStreamDefaultReaderRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + } + if (stream->m_controllerKind == ControllerKind::Direct) { + auto* controller = uncheckedDowncast(stream->m_controller.get()); + if (controller->m_closingPromise) + return controller->m_closingPromise.get(); + } + return jsUndefined(); +} + +static JSValue directConsumeLoopStep(JSC::VM& vm, JSGlobalObject* globalObject, InternalFieldTuple* context) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = uncheckedDowncast(context->getInternalField(0)); + auto* reader = uncheckedDowncast(context->getInternalField(1)); + if (stream->m_state != ReadableStreamState::Readable) + RELEASE_AND_RETURN(scope, finishDirectConsumeLoop(vm, globalObject, stream, reader)); + auto* readPromise = readerReadAsPromise(vm, globalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + readPromise->performPromiseThenWithContext(vm, globalObject, runtime->onDirectConsumeLoopReadFulfilled(), runtime->onDirectConsumeLoopReadRejected(), derived, context); + return derived; +} + +static JSValue consumeDirectStreamBody(JSC::VM& vm, JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, DirectSinkKind kind) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + setUpDirectStreamController(globalObject, stream, kind, stream->m_bunHighWaterMark); + RETURN_IF_EXCEPTION(scope, {}); + stream->materializeIfNeeded(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto* reader = acquireReadableStreamDefaultReader(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + auto* context = InternalFieldTuple::create(vm, defaultGlobalObject(globalObject)->internalFieldTupleStructure(), stream, reader); + RELEASE_AND_RETURN(scope, directConsumeLoopStep(vm, globalObject, context)); +} + +static JSValue consumeDirectStream(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, DirectSinkKind kind) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue result; + { + // Today's function is async: every synchronous abrupt completion becomes a rejection. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + result = consumeDirectStreamBody(vm, globalObject, stream, kind); + if (catchScope.exception()) { + JSValue error = takeAbruptCompletion(globalObject, catchScope); + if (error.isEmpty()) + return {}; + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, error)); + } + } + if (auto* promise = dynamicDowncast(result)) + return promise; + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, result)); +} + +JSValue readableStreamToTextDirect(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + return consumeDirectStream(globalObject, stream, DirectSinkKind::Text); +} + +JSValue readableStreamToArrayDirect(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + return consumeDirectStream(globalObject, stream, DirectSinkKind::Array); +} + +// The one-shot direct → ArrayBuffer/Uint8Array conversion (RSI:2474-2554). + +static JSObject* createOneShotBoundMethod(JSC::VM& vm, JSGlobalObject* globalObject, JSFunction* target, JSValue contextArgument, unsigned length, ASCIILiteral name) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + MarkedArgumentBuffer boundArguments; + boundArguments.append(contextArgument); + SourceCode source = makeSource(WTF::String(name), SourceOrigin(), SourceTaintedOrigin::Untainted); + JSString* boundName = jsString(vm, WTF::String(name)); + RELEASE_AND_RETURN(scope, JSBoundFunction::create(vm, globalObject, target, jsUndefined(), ArgList(boundArguments), length, boundName, source)); +} + +static void installOneShotMethods(JSC::VM& vm, JSGlobalObject* globalObject, JSOneShotDirectSink* sink) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* startMethod = createOneShotBoundMethod(vm, globalObject, runtime->boundOneShotStart(), sink, 0, "start"_s); + RETURN_IF_EXCEPTION(scope, ); + sink->putDirect(vm, builtinNames(vm).startPublicName(), startMethod, 0); + auto* writeMethod = createOneShotBoundMethod(vm, globalObject, runtime->boundOneShotDirectWrite(), sink, 1, "write"_s); + RETURN_IF_EXCEPTION(scope, ); + sink->putDirect(vm, builtinNames(vm).writePublicName(), writeMethod, 0); + auto* endMethod = createOneShotBoundMethod(vm, globalObject, runtime->boundOneShotDirectClose(), sink, 0, "end"_s); + RETURN_IF_EXCEPTION(scope, ); + sink->putDirect(vm, builtinNames(vm).endPublicName(), endMethod, 0); + auto* closeMethod = createOneShotBoundMethod(vm, globalObject, runtime->boundOneShotDirectClose(), sink, 1, "close"_s); + RETURN_IF_EXCEPTION(scope, ); + sink->putDirect(vm, builtinNames(vm).closePublicName(), closeMethod, 0); + auto* flushMethod = createOneShotBoundMethod(vm, globalObject, runtime->boundOneShotDirectFlush(), sink, 0, "flush"_s); + RETURN_IF_EXCEPTION(scope, ); + sink->putDirect(vm, builtinNames(vm).flushPublicName(), flushMethod, 0); +} + +// Calls the user's pull(oneShotController) exactly once (its own scope so the caller may +// catch the abrupt completion). +static JSValue oneShotCallPull(JSC::VM& vm, JSGlobalObject* globalObject, JSValue pullFunction, JSOneShotDirectSink* sink) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto callData = JSC::getCallData(pullFunction); + if (callData.type == CallData::Type::None) [[unlikely]] { + throwTypeError(globalObject, scope, "The 'pull' method of a direct ReadableStream's underlying source is not a function"_s); + return {}; + } + MarkedArgumentBuffer arguments; + arguments.append(sink); + RELEASE_AND_RETURN(scope, JSC::call(globalObject, pullFunction, callData, jsUndefined(), arguments)); +} + +JSValue consumeDirectStreamToArrayBuffer(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, bool asUint8Array) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + + JSObject* underlyingSource = stream->m_directUnderlyingSource.get(); + if (!underlyingSource) [[unlikely]] + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + + MarkedArgumentBuffer noArguments; + JSObject* arrayBufferSink = JSC::construct(globalObject, domGlobalObject->ArrayBufferSink(), noArguments, "ArrayBufferSink is not constructible"_s); + RETURN_IF_EXCEPTION(scope, {}); + + stream->m_directUnderlyingSource.clear(); + stream->m_bunMode = BunStreamMode::Default; + stream->m_lockedWithoutReader = true; + stream->m_disturbed = true; + + JSObject* startOptions = constructEmptyObject(globalObject); + bool hasNumericHighWaterMark = stream->m_bunHighWaterMarkIsNumber || !std::isnan(stream->m_bunHighWaterMark); + startOptions->putDirect(vm, builtinNames(vm).highWaterMarkPublicName(), hasNumericHighWaterMark ? jsNumber(stream->m_bunHighWaterMark) : jsUndefined()); + startOptions->putDirect(vm, builtinNames(vm).asUint8ArrayPublicName(), jsBoolean(asUint8Array)); + MarkedArgumentBuffer startArguments; + startArguments.append(startOptions); + invokeMethod(vm, globalObject, arrayBufferSink, builtinNames(vm).startPublicName(), startArguments); + RETURN_IF_EXCEPTION(scope, {}); + + JSValue pullFunction = underlyingSource->get(globalObject, builtinNames(vm).pullPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + JSValue closeFunction = underlyingSource->get(globalObject, builtinNames(vm).closePublicName()); + RETURN_IF_EXCEPTION(scope, {}); + + auto* capability = JSPromise::create(vm, globalObject->promiseStructure()); + auto* sink = JSOneShotDirectSink::create(vm, runtime->oneShotDirectSinkStructure(domGlobalObject)); + sink->m_stream.set(vm, sink, stream); + sink->m_arrayBufferSink.set(vm, sink, arrayBufferSink); + sink->m_capabilityPromise.set(vm, sink, capability); + sink->m_asUint8Array = asUint8Array; + sink->m_closeFunction.set(vm, sink, closeFunction); + installOneShotMethods(vm, globalObject, sink); + RETURN_IF_EXCEPTION(scope, {}); + + JSValue firstPull; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + firstPull = oneShotCallPull(vm, globalObject, pullFunction, sink); + if (catchScope.exception()) { + JSValue error = takeAbruptCompletion(globalObject, catchScope); + if (error.isEmpty()) + return {}; + stream->m_lockedWithoutReader = false; + readableStreamError(globalObject, stream, error); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, error)); + } + } + if (auto* pullPromise = dynamicDowncast(firstPull)) { + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + pullPromise->performPromiseThenWithContext(vm, globalObject, runtime->onConsumeDirectToArrayBufferPullFulfilled(), runtime->onConsumeDirectToArrayBufferPullRejected(), derived, sink); + return derived; + } + // A synchronous (non-promise) producer: close the stream and return the capability. + stream->m_lockedWithoutReader = false; + readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return capability; +} + +// Bun.readableStreamTo* — each function's check order is observable; do not reorder. + +JSValue readableStreamToText(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (stream->m_bunMode == BunStreamMode::DirectPending) + RELEASE_AND_RETURN(scope, readableStreamToTextDirect(globalObject, stream)); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + if (stream->m_disturbed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject))); + JSValue fastPath = tryUseReadableStreamBufferedFastPath(globalObject, stream, builtinNames(vm).textPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + if (fastPath) + return fastPath; + RELEASE_AND_RETURN(scope, readableStreamIntoText(globalObject, stream)); +} + +JSValue readableStreamToArray(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (stream->m_bunMode == BunStreamMode::DirectPending) + RELEASE_AND_RETURN(scope, readableStreamToArrayDirect(globalObject, stream)); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + if (stream->m_disturbed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject))); + RELEASE_AND_RETURN(scope, readableStreamIntoArray(globalObject, stream)); +} + +// The chunk arrays these conversions consume are built by the array pump and never escape +// to user code; empty the array once converted so the per-chunk buffers die at the next +// collection instead of living as long as the settled reaction cells. +static void releaseInternalChunkArray(JSGlobalObject* globalObject, JSValue chunksValue) +{ + if (auto* array = dynamicDowncast(chunksValue)) + array->setLength(globalObject, 0); +} + +static JSValue convertChunks(JSGlobalObject* globalObject, JSValue chunks, ChunkArrayConversion kind) +{ + switch (kind) { + case ChunkArrayConversion::ArrayBuffer: + return convertChunksToArrayBuffer(globalObject, chunks); + case ChunkArrayConversion::Bytes: + return convertChunksToBytes(globalObject, chunks); + case ChunkArrayConversion::Text: + return convertChunksToText(globalObject, chunks); + } + RELEASE_ASSERT_NOT_REACHED(); +} + +// Shared toArrayBuffer/toBytes/toText tail: preserve the fulfilled-promise peek (RS:207-213). +static JSValue convertChunkArrayPromise(JSC::VM& vm, JSGlobalObject* globalObject, JSValue arrayResult, ChunkArrayConversion kind) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* arrayPromise = dynamicDowncast(arrayResult); + if (!arrayPromise) [[unlikely]] + return arrayResult; + auto* runtime = JSStreamsRuntime::from(globalObject); + if (arrayPromise->status() == JSPromise::Status::Fulfilled) { + JSValue converted; + JSValue thrown; + { + // Text consumers are promise-returning: a synchronous conversion failure rejects. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + converted = convertChunks(globalObject, arrayPromise->result(), kind); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) [[unlikely]] + return {}; + } + } + if (!thrown.isEmpty()) [[unlikely]] { + if (kind == ChunkArrayConversion::Text) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + throwException(globalObject, scope, thrown); + return {}; + } + releaseInternalChunkArray(globalObject, arrayPromise->result()); + RETURN_IF_EXCEPTION(scope, {}); + auto* fulfilled = JSPromise::create(vm, globalObject->promiseStructure()); + fulfilled->fulfill(vm, converted); + return fulfilled; + } + JSFunction* onFulfilled = nullptr; + switch (kind) { + case ChunkArrayConversion::ArrayBuffer: + onFulfilled = runtime->onReadableStreamToArrayBufferFulfilled(); + break; + case ChunkArrayConversion::Bytes: + onFulfilled = runtime->onReadableStreamToBytesFulfilled(); + break; + case ChunkArrayConversion::Text: + onFulfilled = runtime->onReadableStreamToTextChunksFulfilled(); + break; + } + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + arrayPromise->performPromiseThenWithContext(vm, globalObject, onFulfilled, jsUndefined(), derived, jsUndefined()); + return derived; +} + +JSValue readableStreamToArrayBuffer(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (stream->m_bunMode == BunStreamMode::DirectPending) + RELEASE_AND_RETURN(scope, consumeDirectStreamToArrayBuffer(globalObject, stream, /* asUint8Array */ false)); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + if (stream->m_disturbed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject))); + JSValue fastPath = tryUseReadableStreamBufferedFastPath(globalObject, stream, builtinNames(vm).arrayBufferPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + if (fastPath) + return fastPath; + JSValue arrayResult = readableStreamToArray(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, convertChunkArrayPromise(vm, globalObject, arrayResult, ChunkArrayConversion::ArrayBuffer)); +} + +JSValue readableStreamToBytes(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (stream->m_bunMode == BunStreamMode::DirectPending) + RELEASE_AND_RETURN(scope, consumeDirectStreamToArrayBuffer(globalObject, stream, /* asUint8Array */ true)); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + if (stream->m_disturbed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject))); + JSValue fastPath = tryUseReadableStreamBufferedFastPath(globalObject, stream, builtinNames(vm).bytesPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + if (fastPath) + return fastPath; + JSValue arrayResult = readableStreamToArray(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, convertChunkArrayPromise(vm, globalObject, arrayResult, ChunkArrayConversion::Bytes)); +} + +JSValue readableStreamToJSON(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + if (stream->m_disturbed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject))); + JSValue fastPath = tryUseReadableStreamBufferedFastPath(globalObject, stream, builtinNames(vm).jsonPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + if (fastPath) + return fastPath; + JSValue textResult = readableStreamToText(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + auto* textPromise = dynamicDowncast(textResult); + if (!textPromise) [[unlikely]] + return textResult; + auto* runtime = JSStreamsRuntime::from(globalObject); + if (textPromise->status() == JSPromise::Status::Fulfilled) { + JSValue parsed; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + WTF::String text = textPromise->result().toWTFString(globalObject); + if (!catchScope.exception()) + parsed = JSONParseWithException(globalObject, text); + if (catchScope.exception()) { + JSValue error = takeAbruptCompletion(globalObject, catchScope); + if (error.isEmpty()) + return {}; + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, error)); + } + } + auto* fulfilled = JSPromise::create(vm, globalObject->promiseStructure()); + fulfilled->fulfill(vm, parsed); + return fulfilled; + } + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + textPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadableStreamToJSONFulfilled(), jsUndefined(), derived, jsUndefined()); + return derived; +} + +JSValue readableStreamToBlob(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + if (stream->m_disturbed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject))); + JSValue fastPath = tryUseReadableStreamBufferedFastPath(globalObject, stream, builtinNames(vm).blobPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + if (fastPath) + return fastPath; + JSValue arrayResult = readableStreamToArray(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + auto* arrayPromise = dynamicDowncast(arrayResult); + if (!arrayPromise) [[unlikely]] { + arrayPromise = promiseFulfilledWith(globalObject, arrayResult); + RETURN_IF_EXCEPTION(scope, {}); + } + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + arrayPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadableStreamToBlobFulfilled(), jsUndefined(), derived, jsUndefined()); + return derived; +} + +JSValue readableStreamToFormData(JSGlobalObject* globalObject, WebCore::JSReadableStream* stream, JSValue contentType) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createLockedError(globalObject))); + if (stream->m_disturbed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createAlreadyUsedError(globalObject))); + JSValue blobResult = readableStreamToBlob(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + auto* blobPromise = dynamicDowncast(blobResult); + if (!blobPromise) [[unlikely]] { + blobPromise = promiseFulfilledWith(globalObject, blobResult); + RETURN_IF_EXCEPTION(scope, {}); + } + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* derived = JSPromise::create(vm, globalObject->promiseStructure()); + blobPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadableStreamToFormDataFulfilled(), jsUndefined(), derived, contentType); + return derived; +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +// The js2native host-function surface (BunStreamConsumers.h). + +JSC_DEFINE_HOST_FUNCTION(jsFunctionReadableStreamToText, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue streamValue = callFrame->argument(0); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readableStreamToText(globalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionReadableStreamToArray, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue streamValue = callFrame->argument(0); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readableStreamToArray(globalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionReadableStreamToArrayBuffer, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue streamValue = callFrame->argument(0); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readableStreamToArrayBuffer(globalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionReadableStreamToBytes, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue streamValue = callFrame->argument(0); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readableStreamToBytes(globalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionReadableStreamToJSON, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue streamValue = callFrame->argument(0); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readableStreamToJSON(globalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionReadableStreamToBlob, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue streamValue = callFrame->argument(0); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readableStreamToBlob(globalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionReadableStreamToFormData, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue streamValue = callFrame->argument(0); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readableStreamToFormData(globalObject, stream, callFrame->argument(1)))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionTransferToNativeReadableStream, (JSGlobalObject*, CallFrame* callFrame)) +{ + if (auto* stream = dynamicDowncast(callFrame->argument(0))) { + stream->m_transferred = true; + stream->m_disturbed = true; + } + return JSValue::encode(jsUndefined()); +} + +// [reaction-convention] handlers (FOR_EACH_WEB_STREAMS_REACTION_HANDLER_BUN_CONSUMERS). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onBufferedFastPathRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = uncheckedDowncast(callFrame->uncheckedArgument(1)); + JSValue error = callFrame->argument(0); + stream->m_lockedWithoutReader = false; + Bun::WebStreams::readableStreamCancel(globalObject, stream, error); + RETURN_IF_EXCEPTION(scope, {}); + Bun::WebStreams::readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + throwException(globalObject, scope, error); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onBufferedFastPathSettled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = uncheckedDowncast(callFrame->uncheckedArgument(1)); + stream->m_lockedWithoutReader = false; + Bun::WebStreams::readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(callFrame->argument(0)); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToArrayBufferFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue chunksValue = callFrame->argument(0); + JSValue result = Bun::WebStreams::convertChunksToArrayBuffer(globalObject, chunksValue); + RETURN_IF_EXCEPTION(scope, {}); + Bun::WebStreams::releaseInternalChunkArray(globalObject, chunksValue); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToBytesFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue chunksValue = callFrame->argument(0); + JSValue result = Bun::WebStreams::convertChunksToBytes(globalObject, chunksValue); + RETURN_IF_EXCEPTION(scope, {}); + Bun::WebStreams::releaseInternalChunkArray(globalObject, chunksValue); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToTextChunksFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue chunksValue = callFrame->argument(0); + JSValue result = Bun::WebStreams::convertChunksToText(globalObject, chunksValue); + RETURN_IF_EXCEPTION(scope, {}); + Bun::WebStreams::releaseInternalChunkArray(globalObject, chunksValue); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToJSONFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + WTF::String text = callFrame->argument(0).toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(JSONParseWithException(globalObject, text))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToBlobFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + MarkedArgumentBuffer arguments; + arguments.append(callFrame->argument(0)); + JSObject* blob = JSC::construct(globalObject, defaultGlobalObject(globalObject)->JSBlobConstructor(), arguments, "Blob is not constructible"_s); + RETURN_IF_EXCEPTION(scope, {}); + Bun::WebStreams::releaseInternalChunkArray(globalObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(blob); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadableStreamToFormDataFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue blob = callFrame->argument(0); + JSValue contentType = callFrame->argument(1); + JSValue constructor = JSDOMFormData::getConstructor(vm, globalObject); + JSValue fromFunction = constructor.get(globalObject, vm.propertyNames->from); + RETURN_IF_EXCEPTION(scope, {}); + auto callData = JSC::getCallData(fromFunction); + if (callData.type == CallData::Type::None) [[unlikely]] { + throwTypeError(globalObject, scope, "FormData.from is not a function"_s); + return {}; + } + MarkedArgumentBuffer arguments; + arguments.append(blob); + arguments.append(contentType); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::call(globalObject, fromFunction, callData, constructor, arguments))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onIntoArrayReadManyFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* reader = uncheckedDowncast(context->getInternalField(0)); + auto* chunks = uncheckedDowncast(context->getInternalField(1)); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::intoArrayLoop(vm, globalObject, reader, chunks, callFrame->argument(0)))); +} + +// The persistent-op pump: settle the op's result promise with an error, releasing the reader. +static void intoArrayFinishWithError(JSC::VM& vm, JSGlobalObject* globalObject, WebCore::JSReadableStreamDefaultReader* reader, JSPromise* resultPromise, JSValue error) +{ + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + if (reader->m_stream) + Bun::WebStreams::readableStreamDefaultReaderRelease(globalObject, reader); + if (catchScope.exception()) [[unlikely]] { + JSValue releaseError = takeAbruptCompletion(globalObject, catchScope); + if (releaseError.isEmpty()) + return; + } + } + resultPromise->reject(vm, error); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onIntoArrayReadFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* reader = op->m_reader.get(); + auto* chunks = op->m_chunks.get(); + auto* resultPromise = op->m_result.get(); + + JSValue thrown; + bool finished = false; + JSPromise* pendingRead = nullptr; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + do { + JSValue readResult = callFrame->argument(0); + if (!readResult.isObject()) [[unlikely]] { + finished = true; + break; + } + JSValue done = asObject(readResult)->get(globalObject, vm.propertyNames->done); + if (catchScope.exception()) [[unlikely]] + break; + JSValue value = asObject(readResult)->get(globalObject, vm.propertyNames->value); + if (catchScope.exception()) [[unlikely]] + break; + if (done.toBoolean(globalObject)) { + finished = true; + break; + } + chunks->push(globalObject, value); + if (catchScope.exception()) [[unlikely]] + break; + auto step = Bun::WebStreams::readableStreamDefaultReaderFillFromQueue(globalObject, reader, chunks, &pendingRead); + if (catchScope.exception()) [[unlikely]] + break; + if (step == Bun::WebStreams::ConsumerFillStep::Done) + finished = true; + } while (false); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return JSValue::encode(jsUndefined()); + } + } + if (!thrown.isEmpty()) [[unlikely]] { + intoArrayFinishWithError(vm, globalObject, reader, resultPromise, thrown); + RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); + } + if (finished) { + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + if (reader->m_stream) + Bun::WebStreams::readableStreamDefaultReaderRelease(globalObject, reader); + if (catchScope.exception()) [[unlikely]] { + JSValue releaseError = takeAbruptCompletion(globalObject, catchScope); + if (releaseError.isEmpty()) + return JSValue::encode(jsUndefined()); + resultPromise->reject(vm, releaseError); + RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); + } + } + resultPromise->fulfill(vm, chunks); + RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); + } + auto* runtime = JSStreamsRuntime::from(globalObject); + pendingRead->performPromiseThenWithContext(vm, globalObject, runtime->onIntoArrayReadFulfilled(), runtime->onIntoArrayReadRejected(), jsUndefined(), op); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onIntoArrayReadRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->uncheckedArgument(1)); + intoArrayFinishWithError(vm, globalObject, op->m_reader.get(), op->m_result.get(), callFrame->argument(0)); + RELEASE_AND_RETURN(scope, JSValue::encode(jsUndefined())); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onIntoArrayReadManyRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* reader = uncheckedDowncast(context->getInternalField(0)); + JSValue error = callFrame->argument(0); + if (reader->m_stream) { + Bun::WebStreams::readableStreamDefaultReaderRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + } + throwException(globalObject, scope, error); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onDirectConsumeLoopReadFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->uncheckedArgument(1)); + bool done = false; + if (JSObject* result = callFrame->argument(0).getObject()) { + JSValue doneValue = result->get(globalObject, vm.propertyNames->done); + RETURN_IF_EXCEPTION(scope, {}); + done = doneValue.toBoolean(globalObject); + } + if (!done) + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::directConsumeLoopStep(vm, globalObject, context))); + auto* stream = uncheckedDowncast(context->getInternalField(0)); + auto* reader = uncheckedDowncast(context->getInternalField(1)); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::finishDirectConsumeLoop(vm, globalObject, stream, reader))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onDirectConsumeLoopReadRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + throwException(globalObject, scope, callFrame->argument(0)); + return {}; +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onConsumeDirectToArrayBufferPullFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* sink = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* stream = sink->m_stream.get(); + if (stream) { + stream->m_lockedWithoutReader = false; + Bun::WebStreams::readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(sink->m_capabilityPromise.get()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onConsumeDirectToArrayBufferPullRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* sink = uncheckedDowncast(callFrame->uncheckedArgument(1)); + JSValue error = callFrame->argument(0); + auto* stream = sink->m_stream.get(); + if (stream) { + stream->m_lockedWithoutReader = false; + if (stream->m_state == ReadableStreamState::Readable) { + Bun::WebStreams::readableStreamError(globalObject, stream, error); + RETURN_IF_EXCEPTION(scope, {}); + } + } + throwException(globalObject, scope, error); + return {}; +} + +// [bound-convention] targets (FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_ONE_SHOT). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundOneShotStart, (JSGlobalObject*, CallFrame*)) +{ + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundOneShotDirectWrite, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* sink = uncheckedDowncast(callFrame->uncheckedArgument(0)); + if (sink->m_closed) + return JSValue::encode(jsUndefined()); + MarkedArgumentBuffer arguments; + arguments.append(callFrame->argument(1)); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::invokeMethod(vm, globalObject, sink->m_arrayBufferSink.get(), builtinNames(vm).writePublicName(), arguments))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundOneShotDirectClose, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* sink = uncheckedDowncast(callFrame->uncheckedArgument(0)); + if (sink->m_closed) + return JSValue::encode(jsUndefined()); + sink->m_closed = true; + JSValue closeFunction = sink->m_closeFunction.get(); + if (closeFunction.toBoolean(globalObject)) { + auto callData = JSC::getCallData(closeFunction); + if (callData.type == CallData::Type::None) [[unlikely]] { + throwTypeError(globalObject, scope, "The 'close' member of a direct ReadableStream's underlying source is not a function"_s); + return {}; + } + MarkedArgumentBuffer noArguments; + JSC::call(globalObject, closeFunction, callData, jsUndefined(), noArguments); + RETURN_IF_EXCEPTION(scope, {}); + } + MarkedArgumentBuffer noArguments; + JSValue endResult = Bun::WebStreams::invokeMethod(vm, globalObject, sink->m_arrayBufferSink.get(), builtinNames(vm).endPublicName(), noArguments); + RETURN_IF_EXCEPTION(scope, {}); + if (auto* capability = sink->m_capabilityPromise.get(); capability && capability->status() == JSPromise::Status::Pending) + capability->fulfill(vm, endResult); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundOneShotDirectFlush, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* sink = uncheckedDowncast(callFrame->uncheckedArgument(0)); + if (sink->m_closed) + return JSValue::encode(jsUndefined()); + return JSValue::encode(jsNumber(0)); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/BunStreamConsumers.h b/src/jsc/bindings/webcore/streams/BunStreamConsumers.h new file mode 100644 index 000000000000..d1d21017032a --- /dev/null +++ b/src/jsc/bindings/webcore/streams/BunStreamConsumers.h @@ -0,0 +1,24 @@ +// BunStreamConsumers.h — the host functions JavaScript reaches via +// `$newCppFunction("BunStreamConsumers.cpp", "", n)` and via BunObject / the +// ReadableStream prototype. The js2native generator resolves the symbol inside a +// `using namespace WebCore;` block keyed on that file name, so these MUST be declared in +// `namespace WebCore` and DEFINED (JSC_DEFINE_HOST_FUNCTION) in BunStreamConsumers.cpp. +#pragma once + +#include "root.h" + +namespace WebCore { + +// All userJS: yes — BunStreamConsumers.cpp +JSC_DECLARE_HOST_FUNCTION(jsFunctionReadableStreamToText); +JSC_DECLARE_HOST_FUNCTION(jsFunctionReadableStreamToArray); +JSC_DECLARE_HOST_FUNCTION(jsFunctionReadableStreamToArrayBuffer); +JSC_DECLARE_HOST_FUNCTION(jsFunctionReadableStreamToBytes); +JSC_DECLARE_HOST_FUNCTION(jsFunctionReadableStreamToJSON); +JSC_DECLARE_HOST_FUNCTION(jsFunctionReadableStreamToBlob); +JSC_DECLARE_HOST_FUNCTION(jsFunctionReadableStreamToFormData); +// body: dynamicDowncast(arg0)->{m_transferred = true, m_disturbed = true}. +// Referenced by src/js/internal/streams/native-readable.ts via $newCppFunction. +JSC_DECLARE_HOST_FUNCTION(jsFunctionTransferToNativeReadableStream); + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/BunStreamSource.cpp b/src/jsc/bindings/webcore/streams/BunStreamSource.cpp new file mode 100644 index 000000000000..0f2fb46108c5 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/BunStreamSource.cpp @@ -0,0 +1,1716 @@ +#include "BunClientData.h" +#include "config.h" +#include "BunStreamSource.h" + +#include "AsyncContextFrame.h" +#include "BunStandaloneTextSink.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "ErrorCode.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMWrapperCache.h" +#include "JSDirectSinkCloseState.h" +#include "JSReadRequest.h" +#include "JSReadStreamIntoSinkOperation.h" +#include "JSReadableStream.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSResumableSinkPumpOperation.h" +#include "JSSink.h" +#include "JSStreamsRuntime.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +const ClassInfo JSNativeStreamSourceAdapter::s_info = { "NativeStreamSourceAdapter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSNativeStreamSourceAdapter) }; + +JSNativeStreamSourceAdapter::JSNativeStreamSourceAdapter(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSNativeStreamSourceAdapter::~JSNativeStreamSourceAdapter() = default; + +void JSNativeStreamSourceAdapter::destroy(JSCell* cell) +{ + static_cast(cell)->JSNativeStreamSourceAdapter::~JSNativeStreamSourceAdapter(); +} + +void JSNativeStreamSourceAdapter::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSNativeStreamSourceAdapter* JSNativeStreamSourceAdapter::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSNativeStreamSourceAdapter(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSNativeStreamSourceAdapter::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSNativeStreamSourceAdapter::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForNativeStreamSourceAdapter.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForNativeStreamSourceAdapter = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForNativeStreamSourceAdapter.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForNativeStreamSourceAdapter = std::forward(space); }); +} + +template +void JSNativeStreamSourceAdapter::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_handle); + visitor.append(thisObject->m_pendingView); + visitor.append(thisObject->m_closer); + visitor.append(thisObject->m_drainValue); +} + +DEFINE_VISIT_CHILDREN(JSNativeStreamSourceAdapter); + +const ClassInfo JSDirectSinkCloseState::s_info = { "DirectSinkCloseState"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDirectSinkCloseState) }; + +JSDirectSinkCloseState::JSDirectSinkCloseState(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSDirectSinkCloseState::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSDirectSinkCloseState* JSDirectSinkCloseState::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSDirectSinkCloseState(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSDirectSinkCloseState::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSDirectSinkCloseState::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForDirectSinkCloseState.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForDirectSinkCloseState = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForDirectSinkCloseState.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForDirectSinkCloseState = std::forward(space); }); +} + +template +void JSDirectSinkCloseState::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_underlyingSource); + visitor.append(thisObject->m_sinkController); + visitor.append(thisObject->m_closePromise); +} + +DEFINE_VISIT_CHILDREN(JSDirectSinkCloseState); + +const ClassInfo JSReadStreamIntoSinkOperation::s_info = { "ReadStreamIntoSinkOperation"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadStreamIntoSinkOperation) }; + +JSReadStreamIntoSinkOperation::JSReadStreamIntoSinkOperation(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSReadStreamIntoSinkOperation::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSReadStreamIntoSinkOperation* JSReadStreamIntoSinkOperation::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSReadStreamIntoSinkOperation(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSReadStreamIntoSinkOperation::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSReadStreamIntoSinkOperation::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadStreamIntoSinkOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadStreamIntoSinkOperation = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadStreamIntoSinkOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadStreamIntoSinkOperation = std::forward(space); }); +} + +template +void JSReadStreamIntoSinkOperation::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_reader); + visitor.append(thisObject->m_sink); + visitor.append(thisObject->m_result); +} + +DEFINE_VISIT_CHILDREN(JSReadStreamIntoSinkOperation); + +const ClassInfo JSResumableSinkPumpOperation::s_info = { "ResumableSinkPumpOperation"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSResumableSinkPumpOperation) }; + +JSResumableSinkPumpOperation::JSResumableSinkPumpOperation(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSResumableSinkPumpOperation::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSResumableSinkPumpOperation* JSResumableSinkPumpOperation::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSResumableSinkPumpOperation(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSResumableSinkPumpOperation::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSResumableSinkPumpOperation::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForResumableSinkPumpOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForResumableSinkPumpOperation = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForResumableSinkPumpOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForResumableSinkPumpOperation = std::forward(space); }); +} + +template +void JSResumableSinkPumpOperation::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_sink); + visitor.append(thisObject->m_reader); + visitor.append(thisObject->m_error); +} + +DEFINE_VISIT_CHILDREN(JSResumableSinkPumpOperation); + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSBunStandaloneTextSink; + +static constexpr size_t nativeSourceDefaultChunkSize = 256 * 1024; +static constexpr size_t nativeSourceMaxChunkSize = 2 * 1024 * 1024; + +// Shared bound-convention wrapper: see createStreamsBoundHandler (WebStreamsMisc.cpp). +static inline JSBoundFunction* createBoundHandler(JSGlobalObject* globalObject, JSFunction* target, JSCell* context) +{ + return createStreamsBoundHandler(globalObject, target, context); +} + +// object.(...args) with a real [[Get]], as the replaced builtins did. +static JSValue invokeMethod(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* object, const Identifier& name, const MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue method = object->get(globalObject, name); + RETURN_IF_EXCEPTION(scope, {}); + if (!method.isCallable()) [[unlikely]] { + throwTypeError(globalObject, scope, makeString(name.string(), " is not a function"_s)); + return {}; + } + RELEASE_AND_RETURN(scope, call(globalObject, method, getCallData(method), object, args)); +} + +static JSValue wrapWithAsyncContext(JSGlobalObject* globalObject, JSReadableStream* stream, JSValue callable) +{ + JSValue asyncContext = stream->m_asyncContext.get(); + if (callable.isUndefined() || asyncContext.isEmpty() || asyncContext.isUndefined()) + return callable; + return AsyncContextFrame::create(globalObject, callable, asyncContext); +} + +// The generated JSSink controller's C++ start(readableStream, onPull, onClose) registration. +static void startJSSinkController(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sink, JSValue streamValue, JSValue onPull, JSValue onClose) +{ + auto scope = DECLARE_THROW_SCOPE(vm); +#define BUN_START_JSSINK_CONTROLLER(ControllerType) \ + if (auto* controller = dynamicDowncast(sink)) { \ + if (!controller->wrapped()) [[unlikely]] { \ + throwTypeError(globalObject, scope, "Cannot start stream with closed controller"_s); \ + return; \ + } \ + controller->start(globalObject, streamValue, onPull, onClose); \ + return; \ + } + BUN_START_JSSINK_CONTROLLER(JSReadableArrayBufferSinkController) + BUN_START_JSSINK_CONTROLLER(JSReadableFileSinkController) + BUN_START_JSSINK_CONTROLLER(JSReadableHTTPResponseSinkController) + BUN_START_JSSINK_CONTROLLER(JSReadableHTTPSResponseSinkController) + BUN_START_JSSINK_CONTROLLER(JSReadableH3ResponseSinkController) + BUN_START_JSSINK_CONTROLLER(JSReadableNetworkSinkController) +#undef BUN_START_JSSINK_CONTROLLER + throwTypeError(globalObject, scope, "Unknown direct controller. This is a bug in Bun."_s); +} + +// ReadableStream.prototype.cancel semantics; the result promise is only ever markAsHandled'd. +static void publicStreamCancelIgnoringResult(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStream* stream, JSValue reason) +{ + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + JSPromise* promise = nullptr; + if (isReadableStreamLocked(stream)) + promise = promiseRejectedWith(globalObject, createTypeError(globalObject, "ReadableStream is locked"_s)); + else + promise = readableStreamCancel(globalObject, stream, reason); + if (catchScope.exception()) [[unlikely]] { + takeAbruptCompletion(globalObject, catchScope); + return; + } + if (promise) + markPromiseAsHandled(vm, promise); +} + +static void clearStreamControllerSlots(JSReadableStream* stream) +{ + stream->m_controller.clear(); + stream->m_controllerKind = ControllerKind::None; + stream->m_directUnderlyingSource.clear(); +} + +// SourceKind::Native — the lazily materialized native source + +static void nativeStorePendingView(JSC::VM& vm, JSNativeStreamSourceAdapter* adapter, JSValue newView) +{ + if (JSObject* object = newView.getObject()) + adapter->m_pendingView.set(vm, adapter, object); + else + adapter->m_pendingView.clear(); +} + +static bool nativeCloserFlag(JSC::VM& vm, JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* closer = uncheckedDowncast(adapter->m_closer.get()); + JSValue flag = closer->getIndex(globalObject, 0); + RETURN_IF_EXCEPTION(scope, false); + return flag.toBoolean(globalObject); +} + +// Terminal severing: the handle's callback slots, the handle edge, and the pending view. +static void nativeSourceSever(JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter) +{ + auto& vm = getVM(globalObject); + if (JSObject* handle = adapter->m_handle.get()) { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + PutPropertySlot onCloseSlot(handle, false); + handle->methodTable()->put(handle, globalObject, builtinNames(vm).onClosePublicName(), jsUndefined(), onCloseSlot); + if (!catchScope.exception()) { + PutPropertySlot onDrainSlot(handle, false); + handle->methodTable()->put(handle, globalObject, builtinNames(vm).onDrainPublicName(), jsUndefined(), onDrainSlot); + } + if (catchScope.exception()) [[unlikely]] { + if (takeAbruptCompletion(globalObject, catchScope).isEmpty()) + return; + } + } + adapter->m_handle.clear(); + adapter->m_pendingView.clear(); +} + +// The queued callClose job body: close the controller if the consumer is still alive, then sever. +static void nativeSourceCallClose(JSC::VM& vm, JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter) +{ + auto* controller = adapter->m_controller.get(); + if (controller && readableStreamDefaultControllerCanCloseOrEnqueue(controller)) { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + readableStreamDefaultControllerClose(globalObject, controller); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return; + Bun__reportError(globalObject, JSValue::encode(thrown)); + } + } + nativeSourceSever(globalObject, adapter); +} + +static void scheduleNativeSourceCallClose(JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter) +{ + queueStreamsMicrotask(globalObject, WebCore::JSStreamsRuntime::from(globalObject)->onNativeSourceCallCloseMicrotask(), jsUndefined(), adapter); +} + +static void nativeAdjustChunkSize(JSNativeStreamSourceAdapter* adapter, size_t resultBytes) +{ + if (resultBytes >= adapter->m_chunkSize && !adapter->m_hasResized) { + adapter->m_hasResized = true; + adapter->m_chunkSize = std::min(adapter->m_chunkSize * 2, nativeSourceMaxChunkSize); + } +} + +static JSC::JSUint8Array* uint8Subarray(JSGlobalObject* globalObject, JSC::JSUint8Array* view, size_t offset, size_t length) +{ + RefPtr buffer = view->possiblySharedBuffer(); + return JSC::JSUint8Array::create(globalObject, globalObject->typedArrayStructure(JSC::TypeUint8, false), WTF::move(buffer), view->byteOffset() + offset, length); +} + +// Reuse the pending view only when its BACKING BUFFER is large enough. +static JSC::JSUint8Array* nativeGetInternalBuffer(JSC::VM& vm, JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (JSObject* pending = adapter->m_pendingView.get()) { + auto* view = uncheckedDowncast(pending); + if (!view->isDetached() && view->possiblySharedBuffer() && view->possiblySharedBuffer()->byteLength() >= adapter->m_chunkSize) + return view; + } + auto* fresh = JSC::JSUint8Array::create(globalObject, globalObject->typedArrayStructure(JSC::TypeUint8, false), adapter->m_chunkSize); + RETURN_IF_EXCEPTION(scope, nullptr); + adapter->m_pendingView.set(vm, adapter, fresh); + return fresh; +} + +// Decodes one pull result. Returns the value to store as the pending view (a view or undefined). +static JSValue nativeDecodePullResult(JSC::VM& vm, JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter, JSReadableStreamDefaultController* controller, JSValue result, JSC::JSUint8Array* view, bool isClosed) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (result.isNumber()) { + double written = result.asNumber(); + if (!isClosed) + nativeAdjustChunkSize(adapter, written > 0 ? static_cast(written) : 0); + JSValue newView = view ? JSValue(view) : jsUndefined(); + if (written > 0 && view) { + size_t count = std::min(static_cast(written), static_cast(view->length())); + JSC::JSArrayBufferView* toEnqueue = view; + if (view->length() - count > 0) { + toEnqueue = uint8Subarray(globalObject, view, 0, count); + RETURN_IF_EXCEPTION(scope, {}); + auto* tail = uint8Subarray(globalObject, view, count, view->length() - count); + RETURN_IF_EXCEPTION(scope, {}); + newView = tail; + } else + newView = jsUndefined(); + if (controller) { + readableStreamDefaultControllerEnqueue(globalObject, controller, toEnqueue); + RETURN_IF_EXCEPTION(scope, {}); + } + } + if (isClosed) { + scheduleNativeSourceCallClose(globalObject, adapter); + return jsUndefined(); + } + return newView; + } + if (result.isBoolean()) { + scheduleNativeSourceCallClose(globalObject, adapter); + return jsUndefined(); + } + if (auto* chunk = dynamicDowncast(result)) { + if (!isClosed) + nativeAdjustChunkSize(adapter, chunk->byteLength()); + if (chunk->byteLength() > 0 && controller) { + readableStreamDefaultControllerEnqueue(globalObject, controller, chunk); + RETURN_IF_EXCEPTION(scope, {}); + } + if (isClosed) { + scheduleNativeSourceCallClose(globalObject, adapter); + return jsUndefined(); + } + return view ? JSValue(view) : jsUndefined(); + } + Bun::ERR::INVALID_STATE(scope, globalObject, "Internal error: invalid result from pull. This is a bug in Bun. Please report it."_s); + return {}; +} + +void materializeNativeSource(JSGlobalObject* globalObject, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (stream->nativeHandleDetached()) + return; + JSObject* handle = stream->m_nativePtr.get().getObject(); + if (!handle) + return; + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + + stream->m_disturbed = true; + size_t autoAllocateChunkSize = stream->m_autoAllocateChunkSize ? static_cast(stream->m_autoAllocateChunkSize) : nativeSourceDefaultChunkSize; + + MarkedArgumentBuffer startArgs; + startArgs.append(jsNumber(static_cast(autoAllocateChunkSize))); + ASSERT(!startArgs.hasOverflowed()); + JSValue startResult = invokeMethod(vm, globalObject, handle, builtinNames(vm).startPublicName(), startArgs); + RETURN_IF_EXCEPTION(scope, ); + + double chunkSize = 0; + JSValue drainValue; + if (dynamicDowncast(startResult)) + drainValue = startResult; + else { + chunkSize = startResult.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, ); + MarkedArgumentBuffer noArgs; + drainValue = invokeMethod(vm, globalObject, handle, builtinNames(vm).drainPublicName(), noArgs); + RETURN_IF_EXCEPTION(scope, ); + } + + // Fully-buffered fast path: no adapter, no further native round-trips. + if (chunkSize == 0) { + auto* controller = WebCore::JSReadableStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = SourceKind::Nothing; + setUpReadableStreamDefaultController(globalObject, stream, controller, jsUndefined(), 1); + RETURN_IF_EXCEPTION(scope, ); + auto* drainView = dynamicDowncast(drainValue); + if (drainView && drainView->byteLength() > 0) { + readableStreamDefaultControllerEnqueue(globalObject, controller, drainView); + RETURN_IF_EXCEPTION(scope, ); + } + readableStreamDefaultControllerClose(globalObject, controller); + RETURN_IF_EXCEPTION(scope, ); + return; + } + + auto* adapter = WebCore::JSNativeStreamSourceAdapter::create(vm, runtime->nativeStreamSourceAdapterStructure(domGlobalObject)); + adapter->m_handle.set(vm, adapter, handle); + adapter->m_chunkSize = std::max(static_cast(chunkSize), autoAllocateChunkSize); + auto* closer = JSC::constructEmptyArray(globalObject, nullptr, 1); + RETURN_IF_EXCEPTION(scope, ); + closer->putDirectIndex(globalObject, 0, jsBoolean(false)); + RETURN_IF_EXCEPTION(scope, ); + adapter->m_closer.set(vm, adapter, closer); + if (!drainValue.isUndefined()) + adapter->m_drainValue.set(vm, adapter, drainValue); + + auto* onCloseBound = createBoundHandler(globalObject, runtime->boundOnNativeSourceClose(), adapter); + RETURN_IF_EXCEPTION(scope, ); + auto* onDrainBound = createBoundHandler(globalObject, runtime->boundOnNativeSourceDrain(), adapter); + RETURN_IF_EXCEPTION(scope, ); + PutPropertySlot onCloseSlot(handle, false); + handle->methodTable()->put(handle, globalObject, builtinNames(vm).onClosePublicName(), onCloseBound, onCloseSlot); + RETURN_IF_EXCEPTION(scope, ); + PutPropertySlot onDrainSlot(handle, false); + handle->methodTable()->put(handle, globalObject, builtinNames(vm).onDrainPublicName(), onDrainBound, onDrainSlot); + RETURN_IF_EXCEPTION(scope, ); + + auto* controller = WebCore::JSReadableStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = SourceKind::Native; + controller->m_algorithms.algorithmContext.set(vm, controller, adapter); + setUpReadableStreamDefaultController(globalObject, stream, controller, jsUndefined(), 1); + RETURN_IF_EXCEPTION(scope, ); + nativeSourceStart(globalObject, controller); + RETURN_IF_EXCEPTION(scope, ); +} + +JSValue nativeSourceStart(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(controller->m_algorithms.algorithmContext.get()); + JSValue drainValue = adapter->m_drainValue.get(); + if (!drainValue.isEmpty()) { + adapter->m_drainValue.clear(); + if (!adapter->m_controller) + adapter->m_controller = JSC::Weak(controller); + readableStreamDefaultControllerEnqueue(globalObject, controller, drainValue); + RETURN_IF_EXCEPTION(scope, {}); + } + return jsUndefined(); +} + +static JSPromise* nativeSourcePullImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter, JSReadableStreamDefaultController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (!adapter->m_controller) + adapter->m_controller = JSC::Weak(controller); + + JSObject* handle = adapter->m_handle.get(); + if (!handle || adapter->m_closed) { + adapter->m_closed = true; + scheduleNativeSourceCallClose(globalObject, adapter); + nativeSourceSever(globalObject, adapter); + RETURN_IF_EXCEPTION(scope, nullptr); + return nullptr; + } + + auto* closer = uncheckedDowncast(adapter->m_closer.get()); + closer->putDirectIndex(globalObject, 0, jsBoolean(false)); + RETURN_IF_EXCEPTION(scope, nullptr); + + if (JSObject* pendingObject = adapter->m_pendingView.get()) { + MarkedArgumentBuffer noArgs; + JSValue drained = invokeMethod(vm, globalObject, handle, builtinNames(vm).drainPublicName(), noArgs); + RETURN_IF_EXCEPTION(scope, nullptr); + bool isTruthy = drained.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + if (isTruthy) { + bool isClosed = nativeCloserFlag(vm, globalObject, adapter); + RETURN_IF_EXCEPTION(scope, nullptr); + JSValue newView = nativeDecodePullResult(vm, globalObject, adapter, controller, drained, uncheckedDowncast(pendingObject), isClosed); + RETURN_IF_EXCEPTION(scope, nullptr); + nativeStorePendingView(vm, adapter, newView); + return nullptr; + } + } + + auto* view = nativeGetInternalBuffer(vm, globalObject, adapter); + RETURN_IF_EXCEPTION(scope, nullptr); + + MarkedArgumentBuffer pullArgs; + pullArgs.append(view); + pullArgs.append(closer); + ASSERT(!pullArgs.hasOverflowed()); + JSValue result = invokeMethod(vm, globalObject, handle, builtinNames(vm).pullPublicName(), pullArgs); + RETURN_IF_EXCEPTION(scope, nullptr); + + if (auto* pullPromise = dynamicDowncast(result)) { + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + pullPromise->performPromiseThenWithContext(vm, globalObject, runtime->onNativePullFulfilled(), runtime->onNativePullRejected(), jsUndefined(), adapter); + return pullPromise; + } + + bool isClosed = nativeCloserFlag(vm, globalObject, adapter); + RETURN_IF_EXCEPTION(scope, nullptr); + JSValue newView = nativeDecodePullResult(vm, globalObject, adapter, controller, result, view, isClosed); + RETURN_IF_EXCEPTION(scope, nullptr); + nativeStorePendingView(vm, adapter, newView); + if (adapter->m_closed) + adapter->m_pendingView.clear(); + return nullptr; +} + +JSPromise* nativeSourcePull(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(controller->m_algorithms.algorithmContext.get()); + JSValue thrown; + JSPromise* asyncResult = nullptr; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + asyncResult = nativeSourcePullImpl(vm, globalObject, adapter, controller); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return nullptr; + } + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (asyncResult) + return asyncResult; + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +JSPromise* nativeSourceCancel(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(controller->m_algorithms.algorithmContext.get()); + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + adapter->m_pendingView.clear(); + if (JSObject* handle = adapter->m_handle.get()) { + MarkedArgumentBuffer updateRefArgs; + updateRefArgs.append(jsBoolean(false)); + ASSERT(!updateRefArgs.hasOverflowed()); + invokeMethod(vm, globalObject, handle, builtinNames(vm).updateRefPublicName(), updateRefArgs); + if (!catchScope.exception()) { + MarkedArgumentBuffer cancelArgs; + cancelArgs.append(reason); + ASSERT(!cancelArgs.hasOverflowed()); + invokeMethod(vm, globalObject, handle, builtinNames(vm).cancelPublicName(), cancelArgs); + } + } + if (!catchScope.exception()) + nativeSourceSever(globalObject, adapter); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return nullptr; + } + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +// The [bound-convention] onDrain body: a dead consumer drops the chunk. +static void nativeSourceOnDrain(JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter, JSValue chunk) +{ + auto* controller = adapter->m_controller.get(); + if (!controller) + return; + readableStreamDefaultControllerEnqueue(globalObject, controller, chunk); +} + +// The [bound-convention] native-initiated onClose body. +static void nativeSourceOnClose(JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter) +{ + adapter->m_closed = true; + if (adapter->m_controller.get()) + scheduleNativeSourceCallClose(globalObject, adapter); + nativeSourceSever(globalObject, adapter); +} + +static void nativeSourcePullFulfilled(JSC::VM& vm, JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter, JSValue result) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = adapter->m_controller.get(); + JSC::JSUint8Array* view = nullptr; + if (JSObject* pendingObject = adapter->m_pendingView.get()) + view = uncheckedDowncast(pendingObject); + bool isClosed = nativeCloserFlag(vm, globalObject, adapter); + RETURN_IF_EXCEPTION(scope, ); + JSValue newView = nativeDecodePullResult(vm, globalObject, adapter, controller, result, view, isClosed); + RETURN_IF_EXCEPTION(scope, ); + nativeStorePendingView(vm, adapter, newView); + if (adapter->m_closed) + adapter->m_pendingView.clear(); +} + +static void nativeSourcePullRejected(JSC::VM& vm, JSGlobalObject* globalObject, JSNativeStreamSourceAdapter* adapter, JSValue error) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + adapter->m_pendingView.clear(); + adapter->m_closed = true; + auto* controller = adapter->m_controller.get(); + adapter->m_controller.clear(); + if (controller) { + readableStreamDefaultControllerError(globalObject, controller, error); + RETURN_IF_EXCEPTION(scope, ); + } + nativeSourceSever(globalObject, adapter); +} + +// The native-sink path + +// readDirectStreamOnClose: the state-mutation half runs only when a stream is provided. +static void readDirectStreamCloseImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectSinkCloseState* state, JSValue streamValue, JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + // The sink closed (or is closing): end() detaches the controller cell from the native + // sink so a later GC of the cell cannot release a reference it does not own. + if (JSObject* sinkController = state->m_sinkController.get()) { + state->m_sinkController.clear(); + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + MarkedArgumentBuffer noArgs; + invokeMethod(vm, globalObject, sinkController, builtinNames(vm).endPublicName(), noArgs); + if (catchScope.exception()) [[unlikely]] + catchScope.clearExceptionExceptTermination(); + } + JSObject* underlyingSource = state->m_underlyingSource.get(); + state->m_underlyingSource.clear(); + if (underlyingSource) { + JSValue cancelFunction = underlyingSource->get(globalObject, builtinNames(vm).cancelPublicName()); + RETURN_IF_EXCEPTION(scope, ); + bool hasCancel = cancelFunction.toBoolean(globalObject); + if (hasCancel) { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + if (cancelFunction.isCallable()) { + MarkedArgumentBuffer cancelArgs; + cancelArgs.append(reason); + ASSERT(!cancelArgs.hasOverflowed()); + JSValue cancelResult = call(globalObject, cancelFunction, getCallData(cancelFunction), underlyingSource, cancelArgs); + if (!catchScope.exception()) { + if (auto* cancelPromise = dynamicDowncast(cancelResult)) + markPromiseAsHandled(vm, cancelPromise); + } + } + if (catchScope.exception()) [[unlikely]] { + if (takeAbruptCompletion(globalObject, catchScope).isEmpty()) + return; + } + } + } + if (auto* stream = dynamicDowncast(streamValue)) { + clearStreamControllerSlots(stream); + stream->m_reader.clear(); + stream->m_lockedWithoutReader = false; + if (reason.toBoolean(globalObject)) { + stream->m_state = ReadableStreamState::Errored; + stream->m_storedError.set(vm, stream, reason); + } else + stream->m_state = ReadableStreamState::Closed; + } + if (auto* closePromise = state->m_closePromise.get()) { + state->m_closePromise.clear(); + resolvePromise(globalObject, closePromise, jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + } +} + +JSValue readDirectStream(JSGlobalObject* globalObject, JSReadableStream* stream, JSObject* sinkController, JSObject* underlyingSource) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + + stream->m_directUnderlyingSource.clear(); + stream->m_bunMode = BunStreamMode::Default; + + auto* state = WebCore::JSDirectSinkCloseState::create(vm, runtime->directSinkCloseStateStructure(domGlobalObject)); + state->m_underlyingSource.set(vm, state, underlyingSource); + state->m_sinkController.set(vm, state, sinkController); + + JSValue pull = underlyingSource->get(globalObject, builtinNames(vm).pullPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + bool pullIsTruthy = pull.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (!pullIsTruthy) { + readDirectStreamCloseImpl(vm, globalObject, state, jsUndefined(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + return jsUndefined(); + } + if (!pull.isCallable()) { + readDirectStreamCloseImpl(vm, globalObject, state, jsUndefined(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + throwTypeError(globalObject, scope, "pull is not a function"_s); + return {}; + } + + stream->m_controller.set(vm, stream, sinkController); + stream->m_controllerKind = ControllerKind::NativeSink; + + double rawHighWaterMark = stream->m_bunHighWaterMark; + double highWaterMark = (std::isnan(rawHighWaterMark) || rawHighWaterMark < 64) ? 64 : rawHighWaterMark; + auto* startOptions = constructEmptyObject(globalObject); + startOptions->putDirect(vm, builtinNames(vm).highWaterMarkPublicName(), jsNumber(highWaterMark)); + MarkedArgumentBuffer startArgs; + startArgs.append(startOptions); + ASSERT(!startArgs.hasOverflowed()); + invokeMethod(vm, globalObject, sinkController, builtinNames(vm).startPublicName(), startArgs); + RETURN_IF_EXCEPTION(scope, {}); + + auto* closeBound = createBoundHandler(globalObject, runtime->boundReadDirectStreamOnClose(), state); + RETURN_IF_EXCEPTION(scope, {}); + JSValue onPull = wrapWithAsyncContext(globalObject, stream, pull); + RETURN_IF_EXCEPTION(scope, {}); + JSValue onClose = wrapWithAsyncContext(globalObject, stream, closeBound); + RETURN_IF_EXCEPTION(scope, {}); + startJSSinkController(vm, globalObject, sinkController, stream, onPull, onClose); + RETURN_IF_EXCEPTION(scope, {}); + + stream->m_lockedWithoutReader = true; + + MarkedArgumentBuffer pullArgs; + pullArgs.append(sinkController); + ASSERT(!pullArgs.hasOverflowed()); + JSValue maybePromise = call(globalObject, pull, getCallData(pull), underlyingSource, pullArgs); + RETURN_IF_EXCEPTION(scope, {}); + + if (auto* pullPromise = dynamicDowncast(maybePromise)) { + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + pullPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReturnUndefined(), jsUndefined(), result, jsUndefined()); + return result; + } + if (stream->m_state == ReadableStreamState::Readable) { + auto* closePromise = JSPromise::create(vm, globalObject->promiseStructure()); + state->m_closePromise.set(vm, state, closePromise); + return closePromise; + } + return jsUndefined(); +} + +JSValue assignToStream(JSGlobalObject* globalObject, JSReadableStream* stream, JSValue jsSinkController) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* sink = jsSinkController.getObject(); + if (!sink) [[unlikely]] { + throwTypeError(globalObject, scope, "Expected a sink controller"_s); + return {}; + } + JSObject* underlyingSource = stream->m_directUnderlyingSource.get(); + if (stream->m_bunMode == BunStreamMode::DirectPending && underlyingSource) + RELEASE_AND_RETURN(scope, readDirectStream(globalObject, stream, sink, underlyingSource)); + RELEASE_AND_RETURN(scope, readStreamIntoSink(globalObject, stream, sink)); +} + +// readStreamIntoSink — the generic pump + +using WebCore::JSReadStreamIntoSinkOperation; + +static void rsisIssueRead(JSGlobalObject*, JSReadStreamIntoSinkOperation*); +static void rsisFinish(JSGlobalObject*, JSReadStreamIntoSinkOperation*); +static void rsisAbrupt(JSC::VM&, JSGlobalObject*, JSReadStreamIntoSinkOperation*, JSValue error); + +static JSValue rsisSinkWrite(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSValue chunk) +{ + MarkedArgumentBuffer args; + args.append(chunk); + ASSERT(!args.hasOverflowed()); + return invokeMethod(vm, globalObject, op->m_sink.get(), builtinNames(vm).writePublicName(), args); +} + +static JSValue rsisSinkFlushPending(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + MarkedArgumentBuffer args; + args.append(jsBoolean(true)); + ASSERT(!args.hasOverflowed()); + return invokeMethod(vm, globalObject, op->m_sink.get(), builtinNames(vm).flushPublicName(), args); +} + +static JSValue rsisSinkEnd(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + MarkedArgumentBuffer noArgs; + return invokeMethod(vm, globalObject, op->m_sink.get(), builtinNames(vm).endPublicName(), noArgs); +} + +static void rsisSinkClose(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSValue error) +{ + MarkedArgumentBuffer args; + args.append(error); + ASSERT(!args.hasOverflowed()); + invokeMethod(vm, globalObject, op->m_sink.get(), builtinNames(vm).closePublicName(), args); +} + +static JSReadStreamIntoSinkOperation* rsisOpFromContext(JSValue context) +{ + if (auto* tuple = dynamicDowncast(context)) + return uncheckedDowncast(tuple->getInternalField(0)); + return uncheckedDowncast(context); +} + +// Runs one synchronous segment of the pump; an abrupt completion becomes the pump's catch path. +template +static void rsisRunCatching(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, const Body& body) +{ + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + body(); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return; + } + } + if (!thrown.isEmpty()) + rsisAbrupt(vm, globalObject, op, thrown); +} + +// The pump's `finally`: release the reader (unless the throw path orphaned it) and detach. +static void rsisFinally(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (auto* reader = op->m_reader.get()) { + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + readableStreamDefaultReaderRelease(globalObject, reader); + if (catchScope.exception()) [[unlikely]] { + if (takeAbruptCompletion(globalObject, catchScope).isEmpty()) + return; + } + } + reader->m_pipeOperation.clear(); + op->m_reader.clear(); + } + op->m_sink.clear(); + auto* stream = op->m_stream.get(); + if (!stream) + return; + ReadableStreamState state = stream->m_state; + clearStreamControllerSlots(stream); + if (!op->m_didThrow && state != ReadableStreamState::Closed && state != ReadableStreamState::Errored) { + readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, ); + } + op->m_stream.clear(); +} + +static void rsisFinish(JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + op->m_didClose = true; + auto* result = op->m_result.get(); + JSValue endResult = rsisSinkEnd(vm, globalObject, op); + RETURN_IF_EXCEPTION(scope, ); + rsisFinally(vm, globalObject, op); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, result, endResult)); +} + +// The pump's `catch (e)`: the reader is deliberately orphaned, never released. +static void rsisAbrupt(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSValue error) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + op->m_didThrow = true; + op->m_reader.clear(); + auto* result = op->m_result.get(); + if (auto* stream = op->m_stream.get()) + publicStreamCancelIgnoringResult(vm, globalObject, stream, error); + JSValue rejectionValue = error; + if (op->m_sink && !op->m_didClose) { + op->m_didClose = true; + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + rsisSinkClose(vm, globalObject, op, error); + if (catchScope.exception()) [[unlikely]] { + JSValue secondError = takeAbruptCompletion(globalObject, catchScope); + if (secondError.isEmpty()) + return; + auto* errors = constructEmptyArray(globalObject, nullptr, 0); + RETURN_IF_EXCEPTION(scope, ); + errors->putDirectIndex(globalObject, 0, error); + RETURN_IF_EXCEPTION(scope, ); + errors->putDirectIndex(globalObject, 1, secondError); + RETURN_IF_EXCEPTION(scope, ); + rejectionValue = createAggregateError(vm, globalObject->errorStructure(ErrorType::AggregateError), errors, String(), jsUndefined()); + } + } + rsisFinally(vm, globalObject, op); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, rejectPromise(globalObject, result, rejectionValue)); +} + +// One sink.write(chunk). `wrote < 0` = HTTP-sink backpressure: register the flush continuation +// (its context carries the unwritten batch tail) and suspend. A Promise `wrote` is +// deliberately NOT awaited, only marked as handled. +static std::optional rsisWriteChunk(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSValue chunk, JSObject* batchValues, unsigned nextIndex, unsigned length) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue wrote = rsisSinkWrite(vm, globalObject, op, chunk); + RETURN_IF_EXCEPTION(scope, std::nullopt); + if (wrote.isNumber() && wrote.asNumber() < 0) { + JSValue flushed = rsisSinkFlushPending(vm, globalObject, op); + RETURN_IF_EXCEPTION(scope, std::nullopt); + JSPromise* flushPromise = dynamicDowncast(flushed); + if (!flushPromise) { + flushPromise = promiseResolvedWith(globalObject, flushed); + RETURN_IF_EXCEPTION(scope, std::nullopt); + } + JSValue context = op; + if (batchValues) { + auto* tail = constructEmptyArray(globalObject, nullptr, 0); + RETURN_IF_EXCEPTION(scope, std::nullopt); + unsigned tailIndex = 0; + for (unsigned i = nextIndex; i < length; i++) { + JSValue rest = batchValues->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, std::nullopt); + tail->putDirectIndex(globalObject, tailIndex++, rest); + RETURN_IF_EXCEPTION(scope, std::nullopt); + } + context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), op, tail); + } + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + flushPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadStreamIntoSinkFlushFulfilled(), runtime->onReadStreamIntoSinkRejected(), jsUndefined(), context); + return false; + } + if (auto* wrotePromise = dynamicDowncast(wrote)) + markPromiseAsHandled(vm, wrotePromise); + return true; +} + +// Writes values[start..length); false = suspended on backpressure (or an exception is pending). +static bool rsisWriteChunkArrayFrom(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSObject* values, unsigned start, unsigned length) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + for (unsigned i = start; i < length; i++) { + JSValue chunk = values->getIndex(globalObject, i); + RETURN_IF_EXCEPTION(scope, false); + auto step = rsisWriteChunk(vm, globalObject, op, chunk, values, i + 1, length); + RETURN_IF_EXCEPTION(scope, false); + if (!step.value_or(false)) + return false; + } + return true; +} + +static void rsisAfterBatch(JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + auto* stream = op->m_stream.get(); + if (op->m_didClose || (stream && stream->m_state == ReadableStreamState::Closed)) { + rsisFinish(globalObject, op); + return; + } + rsisIssueRead(globalObject, op); +} + +// Resumes after `await sink.flush(true)`: the batch tail (if any), then the read loop. +static void rsisContinueAfterFlush(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSArray* tail) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (op->m_didClose) { + RELEASE_AND_RETURN(scope, rsisFinish(globalObject, op)); + } + if (!tail) { + RELEASE_AND_RETURN(scope, rsisIssueRead(globalObject, op)); + } + bool completed = rsisWriteChunkArrayFrom(vm, globalObject, op, tail, 0, tail->length()); + RETURN_IF_EXCEPTION(scope, ); + if (!completed) + return; + RELEASE_AND_RETURN(scope, rsisAfterBatch(globalObject, op)); +} + +static void rsisRegisterAndStart(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + { + auto* stream = op->m_stream.get(); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + auto* onCloseBound = createBoundHandler(globalObject, runtime->boundReadStreamIntoSinkOnClose(), op); + RETURN_IF_EXCEPTION(scope, ); + JSValue onClose = wrapWithAsyncContext(globalObject, stream, onCloseBound); + RETURN_IF_EXCEPTION(scope, ); + startJSSinkController(vm, globalObject, op->m_sink.get(), stream, jsUndefined(), onClose); + RETURN_IF_EXCEPTION(scope, ); + double rawHighWaterMark = stream->m_bunHighWaterMark; + auto* startOptions = constructEmptyObject(globalObject); + startOptions->putDirect(vm, builtinNames(vm).highWaterMarkPublicName(), jsNumber(std::isnan(rawHighWaterMark) ? 0 : rawHighWaterMark)); + MarkedArgumentBuffer startArgs; + startArgs.append(startOptions); + ASSERT(!startArgs.hasOverflowed()); + invokeMethod(vm, globalObject, op->m_sink.get(), builtinNames(vm).startPublicName(), startArgs); + RETURN_IF_EXCEPTION(scope, ); + } + op->m_started = true; +} + +static void rsisContinueWithMany(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSValue many) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* manyObject = many.getObject(); + if (!manyObject) [[unlikely]] { + throwTypeError(globalObject, scope, "readMany() returned an invalid result"_s); + return; + } + JSValue done = manyObject->get(globalObject, vm.propertyNames->done); + RETURN_IF_EXCEPTION(scope, ); + bool isDone = done.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, ); + if (isDone) { + RELEASE_AND_RETURN(scope, rsisFinish(globalObject, op)); + } + if (!op->m_started) { + rsisRegisterAndStart(vm, globalObject, op); + RETURN_IF_EXCEPTION(scope, ); + } + JSValue valuesValue = manyObject->get(globalObject, vm.propertyNames->value); + RETURN_IF_EXCEPTION(scope, ); + JSObject* values = valuesValue.getObject(); + unsigned length = 0; + if (values) { + JSValue lengthValue = values->get(globalObject, vm.propertyNames->length); + RETURN_IF_EXCEPTION(scope, ); + length = lengthValue.toUInt32(globalObject); + RETURN_IF_EXCEPTION(scope, ); + } + if (length) { + bool completed = rsisWriteChunkArrayFrom(vm, globalObject, op, values, 0, length); + RETURN_IF_EXCEPTION(scope, ); + if (!completed) + return; + } + RELEASE_AND_RETURN(scope, rsisAfterBatch(globalObject, op)); +} + +static void rsisIssueRead(JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + auto* readPromise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* readRequest = WebCore::JSReadRequest::create(vm, runtime->readRequestStructure(domGlobalObject), ReadRequestKind::Promise, readPromise); + readableStreamDefaultReaderRead(globalObject, op->m_reader.get(), readRequest); + RETURN_IF_EXCEPTION(scope, ); + readPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadStreamIntoSinkReadFulfilled(), runtime->onReadStreamIntoSinkRejected(), jsUndefined(), op); +} + +static void rsisHandleReadResult(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSValue iterationResult) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* resultObject = iterationResult.getObject(); + if (!resultObject) [[unlikely]] { + throwTypeError(globalObject, scope, "read() resolved with an invalid result"_s); + return; + } + JSValue done = resultObject->get(globalObject, vm.propertyNames->done); + RETURN_IF_EXCEPTION(scope, ); + bool isDone = done.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, ); + if (isDone) { + RELEASE_AND_RETURN(scope, rsisFinish(globalObject, op)); + } + JSValue chunk = resultObject->get(globalObject, vm.propertyNames->value); + RETURN_IF_EXCEPTION(scope, ); + auto step = rsisWriteChunk(vm, globalObject, op, chunk, nullptr, 0, 0); + RETURN_IF_EXCEPTION(scope, ); + if (!step.value_or(false)) + return; + // write() runs user code that may close the sink; re-check before the next read. + RELEASE_AND_RETURN(scope, rsisAfterBatch(globalObject, op)); +} + +static void rsisBegin(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = op->m_stream.get(); + stream->materializeIfNeeded(globalObject); + RETURN_IF_EXCEPTION(scope, ); + auto* reader = acquireReadableStreamDefaultReader(globalObject, stream); + RETURN_IF_EXCEPTION(scope, ); + op->m_reader.set(vm, op, reader); + reader->m_pipeOperation.set(vm, reader, op); + JSValue many = readableStreamDefaultReaderReadMany(globalObject, reader); + RETURN_IF_EXCEPTION(scope, ); + if (auto* manyPromise = dynamicDowncast(many)) { + // The sink may abort before readMany settles (#6758): start it now. + rsisRegisterAndStart(vm, globalObject, op); + RETURN_IF_EXCEPTION(scope, ); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + manyPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadStreamIntoSinkReadManyFulfilled(), runtime->onReadStreamIntoSinkRejected(), jsUndefined(), op); + return; + } + RELEASE_AND_RETURN(scope, rsisContinueWithMany(vm, globalObject, op, many)); +} + +JSPromise* readStreamIntoSink(JSGlobalObject* globalObject, JSReadableStream* stream, JSObject* sink) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + auto* op = JSReadStreamIntoSinkOperation::create(vm, runtime->readStreamIntoSinkOperationStructure(domGlobalObject)); + op->m_stream.set(vm, op, stream); + op->m_sink.set(vm, op, sink); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + op->m_result.set(vm, op, result); + rsisRunCatching(vm, globalObject, op, [&] { + rsisBegin(vm, globalObject, op); + }); + RETURN_IF_EXCEPTION(scope, nullptr); + return result; +} + +// readStreamIntoSinkOnClose(op, stream, reason) — the JSSink onClose [bound-convention] body. +static void readStreamIntoSinkOnCloseImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSReadStreamIntoSinkOperation* op, JSValue streamValue, JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + // The sink closed underneath the pump (which may stay suspended forever): end() FIRST, + // before the fallible cancel below, so the controller cell always detaches from the + // native sink instead of being collected attached (its destructor would over-release). + if (JSObject* sink = op->m_sink.get()) { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + MarkedArgumentBuffer noArgs; + invokeMethod(vm, globalObject, sink, builtinNames(vm).endPublicName(), noArgs); + if (catchScope.exception()) [[unlikely]] + catchScope.clearExceptionExceptTermination(); + } + if (!op->m_didThrow && !op->m_didClose) { + auto* stream = dynamicDowncast(streamValue); + if (stream && stream->m_state != ReadableStreamState::Closed) { + readableStreamCancel(globalObject, stream, reason); + if (scope.exception()) [[unlikely]] { + op->m_didClose = true; + return; + } + } + } + op->m_didClose = true; +} + +// assignStreamIntoResumableSink — the ResumableSink pump + +using WebCore::JSResumableSinkPumpOperation; + +static void resumableIssueRead(JSC::VM&, JSGlobalObject*, JSResumableSinkPumpOperation*); +static void resumableEnd(JSC::VM&, JSGlobalObject*, JSResumableSinkPumpOperation*, JSValue error, bool hasError); + +static void resumableReleaseReader(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (auto* reader = op->m_reader.get()) { + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + readableStreamDefaultReaderRelease(globalObject, reader); + if (catchScope.exception()) [[unlikely]] { + if (takeAbruptCompletion(globalObject, catchScope).isEmpty()) + return; + } + } + reader->m_pipeOperation.clear(); + op->m_reader.clear(); + } + op->m_sink.clear(); + auto* stream = op->m_stream.get(); + if (!stream) + return; + ReadableStreamState state = stream->m_state; + clearStreamControllerSlots(stream); + JSValue error = op->m_error.get(); + bool hasTruthyError = !error.isEmpty() && error.toBoolean(globalObject); + if (!hasTruthyError && state != ReadableStreamState::Closed && state != ReadableStreamState::Errored) { + readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, ); + } + op->m_stream.clear(); +} + +static void resumableEnd(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op, JSValue error, bool hasError) +{ + if (JSObject* sink = op->m_sink.get()) { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + MarkedArgumentBuffer args; + if (hasError) + args.append(error); + ASSERT(!args.hasOverflowed()); + invokeMethod(vm, globalObject, sink, builtinNames(vm).endPublicName(), args); + if (catchScope.exception()) [[unlikely]] { + if (takeAbruptCompletion(globalObject, catchScope).isEmpty()) + return; + } + } + resumableReleaseReader(vm, globalObject, op); +} + +// The drain loop's catch: sticky error, public cancel, end(error) on a fresh microtask. +static void resumableHandleAbrupt(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op, JSValue error) +{ + op->m_error.set(vm, op, error); + op->m_closed = true; + if (auto* stream = op->m_stream.get()) + publicStreamCancelIgnoringResult(vm, globalObject, stream, error); + queueStreamsMicrotask(globalObject, WebCore::JSStreamsRuntime::from(globalObject)->onResumableSinkEndMicrotask(), error, op); + op->m_reading = false; +} + +static void resumableHandleReadResult(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op, JSValue iterationResult) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* resultObject = iterationResult.getObject(); + if (!resultObject) [[unlikely]] { + throwTypeError(globalObject, scope, "read() resolved with an invalid result"_s); + return; + } + JSValue chunk = resultObject->get(globalObject, vm.propertyNames->value); + RETURN_IF_EXCEPTION(scope, ); + JSValue done = resultObject->get(globalObject, vm.propertyNames->done); + RETURN_IF_EXCEPTION(scope, ); + if (op->m_closed) { + op->m_reading = false; + return; + } + bool isDone = done.toBoolean(globalObject); + RETURN_IF_EXCEPTION(scope, ); + bool hasChunk = chunk.toBoolean(globalObject); + if (isDone) { + op->m_closed = true; + if (hasChunk) { + MarkedArgumentBuffer args; + args.append(chunk); + ASSERT(!args.hasOverflowed()); + invokeMethod(vm, globalObject, op->m_sink.get(), builtinNames(vm).writePublicName(), args); + RETURN_IF_EXCEPTION(scope, ); + } + op->m_reading = false; + RELEASE_AND_RETURN(scope, resumableEnd(vm, globalObject, op, jsUndefined(), false)); + } + if (hasChunk) { + MarkedArgumentBuffer args; + args.append(chunk); + ASSERT(!args.hasOverflowed()); + JSValue wrote = invokeMethod(vm, globalObject, op->m_sink.get(), builtinNames(vm).writePublicName(), args); + RETURN_IF_EXCEPTION(scope, ); + // write() runs user code that may synchronously cancel the pump and release the + // reader; re-validate before issuing the next read through it. + if (op->m_closed || !op->m_reader) { + op->m_reading = false; + return; + } + // `false` = backpressure: the native side re-enters drain when it releases. + bool keepGoing = wrote.toBoolean(globalObject); + if (!keepGoing) { + op->m_reading = false; + return; + } + } + RELEASE_AND_RETURN(scope, resumableIssueRead(vm, globalObject, op)); +} + +static void resumableIssueRead(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + auto* readPromise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* readRequest = WebCore::JSReadRequest::create(vm, runtime->readRequestStructure(domGlobalObject), ReadRequestKind::Promise, readPromise); + readableStreamDefaultReaderRead(globalObject, op->m_reader.get(), readRequest); + RETURN_IF_EXCEPTION(scope, ); + readPromise->performPromiseThenWithContext(vm, globalObject, runtime->onResumableSinkReadFulfilled(), runtime->onResumableSinkReadRejected(), jsUndefined(), op); +} + +static void resumableDrain(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op) +{ + if (!op->m_error.get().isEmpty() || op->m_closed || op->m_reading) + return; + op->m_reading = true; + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + resumableIssueRead(vm, globalObject, op); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return; + } + } + if (!thrown.isEmpty()) + resumableHandleAbrupt(vm, globalObject, op, thrown); +} + +// resumableSinkCancel(unused, reason): the native side invokes it as (undefined, reason). +static void resumableCancelImpl(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op, JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (op->m_closed) + return; + op->m_closed = true; + auto* stream = op->m_stream.get(); + JSValue error = op->m_error.get(); + bool hasTruthyError = !error.isEmpty() && error.toBoolean(globalObject); + if (stream && !hasTruthyError && stream->m_state != ReadableStreamState::Closed) { + readableStreamCancel(globalObject, stream, reason); + RETURN_IF_EXCEPTION(scope, ); + } + RELEASE_AND_RETURN(scope, resumableReleaseReader(vm, globalObject, op)); +} + +static void resumableSetup(JSC::VM& vm, JSGlobalObject* globalObject, JSResumableSinkPumpOperation* op) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = op->m_stream.get(); + JSObject* sink = op->m_sink.get(); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + + // The sink's start runs FIRST, even if acquiring the reader throws. + double rawHighWaterMark = stream->m_bunHighWaterMark; + auto* startOptions = constructEmptyObject(globalObject); + startOptions->putDirect(vm, builtinNames(vm).highWaterMarkPublicName(), jsNumber(std::isnan(rawHighWaterMark) ? 0 : rawHighWaterMark)); + MarkedArgumentBuffer startArgs; + startArgs.append(startOptions); + ASSERT(!startArgs.hasOverflowed()); + invokeMethod(vm, globalObject, sink, builtinNames(vm).startPublicName(), startArgs); + RETURN_IF_EXCEPTION(scope, ); + + stream->materializeIfNeeded(globalObject); + RETURN_IF_EXCEPTION(scope, ); + auto* reader = acquireReadableStreamDefaultReader(globalObject, stream); + RETURN_IF_EXCEPTION(scope, ); + op->m_reader.set(vm, op, reader); + reader->m_pipeOperation.set(vm, reader, op); + + auto* drainBound = createBoundHandler(globalObject, runtime->boundResumableSinkDrain(), op); + RETURN_IF_EXCEPTION(scope, ); + auto* cancelBound = createBoundHandler(globalObject, runtime->boundResumableSinkCancel(), op); + RETURN_IF_EXCEPTION(scope, ); + MarkedArgumentBuffer handlerArgs; + handlerArgs.append(drainBound); + handlerArgs.append(cancelBound); + ASSERT(!handlerArgs.hasOverflowed()); + invokeMethod(vm, globalObject, sink, builtinNames(vm).setHandlersPublicName(), handlerArgs); + RETURN_IF_EXCEPTION(scope, ); + + RELEASE_AND_RETURN(scope, resumableDrain(vm, globalObject, op)); +} + +JSValue assignStreamIntoResumableSink(JSGlobalObject* globalObject, JSReadableStream* stream, JSObject* resumableSink) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = WebCore::JSStreamsRuntime::from(globalObject); + auto* op = JSResumableSinkPumpOperation::create(vm, runtime->resumableSinkPumpOperationStructure(domGlobalObject)); + op->m_stream.set(vm, op, stream); + op->m_sink.set(vm, op, resumableSink); + + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + resumableSetup(vm, globalObject, op); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return {}; + } + } + if (!thrown.isEmpty()) { + op->m_error.set(vm, op, thrown); + op->m_closed = true; + queueStreamsMicrotask(globalObject, runtime->onResumableSinkEndMicrotask(), thrown, op); + } + RETURN_IF_EXCEPTION(scope, {}); + return jsUndefined(); +} + +} // namespace WebStreams +} // namespace Bun + +// The shared handler bodies (JSStreamsRuntime targets) + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +// [reaction-convention]: handler(resolutionValue, contextCell). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onNativePullFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(callFrame->argument(1)); + JSValue result = callFrame->argument(0); + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + Bun::WebStreams::nativeSourcePullFulfilled(vm, globalObject, adapter, result); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return {}; + } + } + // Boundary: an internal decode failure errors the stream instead of escaping. + if (!thrown.isEmpty()) { + if (auto* controller = adapter->m_controller.get()) { + readableStreamDefaultControllerError(globalObject, controller, thrown); + RETURN_IF_EXCEPTION(scope, {}); + } + } + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onNativePullRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(callFrame->argument(1)); + Bun::WebStreams::nativeSourcePullRejected(vm, globalObject, adapter, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onNativeSourceCallCloseMicrotask, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(callFrame->argument(1)); + Bun::WebStreams::nativeSourceCallClose(vm, globalObject, adapter); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadStreamIntoSinkReadManyFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(1)); + JSValue many = callFrame->argument(0); + Bun::WebStreams::rsisRunCatching(vm, globalObject, op, [&] { + Bun::WebStreams::rsisContinueWithMany(vm, globalObject, op, many); + }); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadStreamIntoSinkReadFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(1)); + JSValue iterationResult = callFrame->argument(0); + Bun::WebStreams::rsisRunCatching(vm, globalObject, op, [&] { + Bun::WebStreams::rsisHandleReadResult(vm, globalObject, op, iterationResult); + }); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadStreamIntoSinkFlushFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue context = callFrame->argument(1); + auto* op = Bun::WebStreams::rsisOpFromContext(context); + JSArray* tail = nullptr; + if (auto* tuple = dynamicDowncast(context)) + tail = uncheckedDowncast(tuple->getInternalField(1)); + Bun::WebStreams::rsisRunCatching(vm, globalObject, op, [&] { + Bun::WebStreams::rsisContinueAfterFlush(vm, globalObject, op, tail); + }); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadStreamIntoSinkRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = Bun::WebStreams::rsisOpFromContext(callFrame->argument(1)); + Bun::WebStreams::rsisAbrupt(vm, globalObject, op, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onResumableSinkReadFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(1)); + JSValue iterationResult = callFrame->argument(0); + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + Bun::WebStreams::resumableHandleReadResult(vm, globalObject, op, iterationResult); + if (catchScope.exception()) [[unlikely]] { + thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return {}; + } + } + if (!thrown.isEmpty()) + Bun::WebStreams::resumableHandleAbrupt(vm, globalObject, op, thrown); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onResumableSinkReadRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(1)); + Bun::WebStreams::resumableHandleAbrupt(vm, globalObject, op, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onResumableSinkEndMicrotask, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(1)); + Bun::WebStreams::resumableEnd(vm, globalObject, op, callFrame->argument(0), true); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// [bound-convention]: handler(contextCell, ...callArgs). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundOnNativeSourceClose, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(callFrame->argument(0)); + Bun::WebStreams::nativeSourceOnClose(globalObject, adapter); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundOnNativeSourceDrain, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* adapter = uncheckedDowncast(callFrame->argument(0)); + Bun::WebStreams::nativeSourceOnDrain(globalObject, adapter, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundReadDirectStreamOnClose, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* state = uncheckedDowncast(callFrame->argument(0)); + Bun::WebStreams::readDirectStreamCloseImpl(vm, globalObject, state, callFrame->argument(1), callFrame->argument(2)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundReadStreamIntoSinkOnClose, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(0)); + Bun::WebStreams::readStreamIntoSinkOnCloseImpl(vm, globalObject, op, callFrame->argument(1), callFrame->argument(2)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundResumableSinkDrain, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(0)); + Bun::WebStreams::resumableDrain(vm, globalObject, op); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundResumableSinkCancel, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = uncheckedDowncast(callFrame->argument(0)); + Bun::WebStreams::resumableCancelImpl(vm, globalObject, op, callFrame->argument(2)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/BunStreamSource.h b/src/jsc/bindings/webcore/streams/BunStreamSource.h new file mode 100644 index 000000000000..e6a51ede213b --- /dev/null +++ b/src/jsc/bindings/webcore/streams/BunStreamSource.h @@ -0,0 +1,71 @@ +// BunStreamSource.h — JSNativeStreamSourceAdapter, the C++ port of the old +// NativeReadableStreamSource JS class. Its .cpp also owns materializeNativeSource and the +// SourceKind::Native pull/cancel/start algorithm arms. +// +// DESTRUCTIBLE: it owns a JSC::Weak (a non-trivially-destructible member). +// The Weak member is THE one sanctioned JSC::Weak in the whole subsystem: a STRONG back-edge +// would let Rust's external Strong root on the native handle pin the entire abandoned JS +// consumer graph forever. +// Internal cell: no prototype, no constructor, never exposed to JS. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSReadableStreamDefaultController.h" +#include +#include + +namespace WebCore { + +class JSNativeStreamSourceAdapter final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + static JSNativeStreamSourceAdapter* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_handle, m_pendingView, m_closer, m_drainValue. + // m_controller is a JSC::Weak and MUST NOT be visited (that is the whole point). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // the JS{Blob,File,Bytes}InternalReadableStreamSource handle cell. CLEARED (with + // handle.onClose/onDrain and m_pendingView) on all three terminal paths. + JSC::WriteBarrier m_handle; + // `$data`: the unfilled tail Uint8Array reused across pulls. + JSC::WriteBarrier m_pendingView; + // `#closer`: a per-instance length-1 JSArray the native pull writes EOF into (#29787). + JSC::WriteBarrier m_closer; + // the drain value returned by handle.start()/drain(), enqueued by the Native + // startAlgorithm and then cleared. + JSC::WriteBarrier m_drainValue; + // THE ONE SANCTIONED JSC::Weak in the subsystem. Null-check EVERY read: null ⇒ the JS + // consumer side was collected ⇒ drop the data / no-op. Assigned lazily — never eagerly. + JSC::Weak m_controller; + // adaptive chunk size (256 KiB default, doubled once up to 2 MiB). + size_t m_chunkSize { 0 }; + // #hasResized — the one-shot chunk-size adaptation already happened. + bool m_hasResized { false }; + // #closed + bool m_closed { false }; + +private: + JSNativeStreamSourceAdapter(JSC::VM&, JSC::Structure*); + ~JSNativeStreamSourceAdapter(); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/CrossRealmTransform.cpp b/src/jsc/bindings/webcore/streams/CrossRealmTransform.cpp new file mode 100644 index 000000000000..dbd65b28e0a3 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/CrossRealmTransform.cpp @@ -0,0 +1,64 @@ +#include "config.h" +#include "WebStreamsInternals.h" + +#include "JSStreamsRuntime.h" +#include + +// Transferable streams are out of scope: Bun's structured clone never transfers a stream, so +// no caller can reach these today. Each entry point fails loudly (a thrown TypeError) so an +// accidental future caller cannot half-set-up a cross-realm transform. + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +void crossRealmTransformSendError(JSGlobalObject* globalObject, WebCore::MessagePort&, JSValue) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + throwTypeError(globalObject, scope, "ReadableStream transfer is not implemented"_s); +} + +void packAndPostMessage(JSGlobalObject* globalObject, WebCore::MessagePort&, CrossRealmMessageType, JSValue) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + throwTypeError(globalObject, scope, "ReadableStream transfer is not implemented"_s); +} + +bool packAndPostMessageHandlingError(JSGlobalObject* globalObject, WebCore::MessagePort&, CrossRealmMessageType, JSValue) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + throwTypeError(globalObject, scope, "ReadableStream transfer is not implemented"_s); + return false; +} + +void setUpCrossRealmTransformReadable(JSGlobalObject* globalObject, JSReadableStream*, WebCore::MessagePort&) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + throwTypeError(globalObject, scope, "ReadableStream transfer is not implemented"_s); +} + +void setUpCrossRealmTransformWritable(JSGlobalObject* globalObject, JSWritableStream*, WebCore::MessagePort&) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + throwTypeError(globalObject, scope, "WritableStream transfer is not implemented"_s); +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +// Registered only by setUpCrossRealmTransformWritable, which never sets a transform up. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onCrossRealmWritableBackpressureFulfilled, (JSC::JSGlobalObject*, JSC::CallFrame*)) +{ + RELEASE_ASSERT_NOT_REACHED(); + return {}; +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSAsyncIteratorSourceOperation.h b/src/jsc/bindings/webcore/streams/JSAsyncIteratorSourceOperation.h new file mode 100644 index 000000000000..ff08ce257cae --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSAsyncIteratorSourceOperation.h @@ -0,0 +1,54 @@ +// JSAsyncIteratorSourceOperation — state cell for Bun's async-iterable → direct-stream body +// extension (BunAsyncIterableSource.cpp): the iterator, the controller handed to pull(), and +// the one promise every pull() returns. Internal cell: no prototype, no constructor. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSAsyncIteratorSourceOperation final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSAsyncIteratorSourceOperation* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_iterator, m_controller, m_pullPromise. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The async iterator; cleared when cancellation or the error path hands it off. + JSC::WriteBarrier m_iterator; + // Whatever object pull() received (the direct controller, or the HTTP sink facade). + JSC::WriteBarrier m_controller; + // The single promise returned to every pull() while the iterator runs. + JSC::WriteBarrier m_pullPromise; + bool m_cancelled { false }; + bool m_done { false }; + bool m_running { false }; + // {done:true, value} still writes the value first; this remembers the done across a + // backpressure suspension on that final write. + bool m_iteratorDone { false }; + +private: + JSAsyncIteratorSourceOperation(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.cpp b/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.cpp new file mode 100644 index 000000000000..fa0658bd4e93 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.cpp @@ -0,0 +1,269 @@ +#include "config.h" +#include "JSByteLengthQueuingStrategy.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSStreamsRuntime.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsByteLengthQueuingStrategyPrototypeGetter_constructor); +static JSC_DECLARE_CUSTOM_GETTER(jsByteLengthQueuingStrategyPrototypeGetter_highWaterMark); +static JSC_DECLARE_CUSTOM_GETTER(jsByteLengthQueuingStrategyPrototypeGetter_size); + +class JSByteLengthQueuingStrategyPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSByteLengthQueuingStrategyPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSByteLengthQueuingStrategyPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSByteLengthQueuingStrategyPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSByteLengthQueuingStrategyPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSByteLengthQueuingStrategyPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSByteLengthQueuingStrategyPrototype, JSByteLengthQueuingStrategyPrototype::Base); + +// JSByteLengthQueuingStrategyConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSByteLengthQueuingStrategyConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSByteLengthQueuingStrategyConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSByteLengthQueuingStrategyConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSByteLengthQueuingStrategyConstructor::subspaceForImpl(JSC::VM&); +template<> void JSByteLengthQueuingStrategyConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSByteLengthQueuingStrategyConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSByteLengthQueuingStrategyConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSByteLengthQueuingStrategyConstructor::s_info = { "ByteLengthQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSByteLengthQueuingStrategyConstructor) }; + +template<> JSValue JSByteLengthQueuingStrategyConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSByteLengthQueuingStrategyConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSByteLengthQueuingStrategyConstructor); + +template<> GCClient::IsoSubspace* JSByteLengthQueuingStrategyConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForByteLengthQueuingStrategyConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForByteLengthQueuingStrategyConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForByteLengthQueuingStrategyConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForByteLengthQueuingStrategyConstructor = std::forward(space); }); +} + +template<> void JSByteLengthQueuingStrategyConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "ByteLengthQueuingStrategy"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSByteLengthQueuingStrategy::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSByteLengthQueuingStrategyConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +// `QueuingStrategyInit init` — `highWaterMark` is a required `unrestricted double` member. +static double convertQueuingStrategyInit(JSC::VM& vm, JSGlobalObject* globalObject, JSValue init) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (!init.isObject()) { + if (!init.isUndefinedOrNull()) { + throwTypeError(globalObject, scope, "The QueuingStrategyInit argument must be an object"_s); + return 0; + } + throwTypeError(globalObject, scope, "QueuingStrategyInit requires a 'highWaterMark' member"_s); + return 0; + } + JSValue highWaterMark = asObject(init)->get(globalObject, builtinNames(vm).highWaterMarkPublicName()); + RETURN_IF_EXCEPTION(scope, 0); + if (highWaterMark.isUndefined()) { + throwTypeError(globalObject, scope, "QueuingStrategyInit requires a 'highWaterMark' member"_s); + return 0; + } + RELEASE_AND_RETURN(scope, highWaterMark.toNumber(globalObject)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSByteLengthQueuingStrategyConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + if (callFrame->argumentCount() < 1) + return throwVMError(lexicalGlobalObject, scope, createNotEnoughArgumentsError(lexicalGlobalObject)); + + double highWaterMark = convertQueuingStrategyInit(vm, lexicalGlobalObject, callFrame->uncheckedArgument(0)); + RETURN_IF_EXCEPTION(scope, {}); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(JSByteLengthQueuingStrategy::create(vm, structure, highWaterMark)); +} +JSC_ANNOTATE_HOST_FUNCTION(JSByteLengthQueuingStrategyConstructorConstruct, JSByteLengthQueuingStrategyConstructor::construct); + +// JSByteLengthQueuingStrategyPrototype + +static const HashTableValue JSByteLengthQueuingStrategyPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsByteLengthQueuingStrategyPrototypeGetter_constructor, 0 } }, + { "highWaterMark"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsByteLengthQueuingStrategyPrototypeGetter_highWaterMark, 0 } }, + { "size"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsByteLengthQueuingStrategyPrototypeGetter_size, 0 } }, +}; + +const ClassInfo JSByteLengthQueuingStrategyPrototype::s_info = { "ByteLengthQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSByteLengthQueuingStrategyPrototype) }; + +void JSByteLengthQueuingStrategyPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSByteLengthQueuingStrategy::info(), JSByteLengthQueuingStrategyPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSByteLengthQueuingStrategy + +const ClassInfo JSByteLengthQueuingStrategy::s_info = { "ByteLengthQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSByteLengthQueuingStrategy) }; + +JSByteLengthQueuingStrategy::JSByteLengthQueuingStrategy(VM& vm, Structure* structure, double highWaterMark) + : Base(vm, structure) + , m_highWaterMark(highWaterMark) +{ +} + +void JSByteLengthQueuingStrategy::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSByteLengthQueuingStrategy* JSByteLengthQueuingStrategy::create(VM& vm, Structure* structure, double highWaterMark) +{ + auto* strategy = new (NotNull, allocateCell(vm)) JSByteLengthQueuingStrategy(vm, structure, highWaterMark); + strategy->finishCreation(vm); + return strategy; +} + +Structure* JSByteLengthQueuingStrategy::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSByteLengthQueuingStrategy::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSByteLengthQueuingStrategyPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSByteLengthQueuingStrategyPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSByteLengthQueuingStrategy::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSByteLengthQueuingStrategy::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSByteLengthQueuingStrategy::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForByteLengthQueuingStrategy.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForByteLengthQueuingStrategy = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForByteLengthQueuingStrategy.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForByteLengthQueuingStrategy = std::forward(space); }); +} + +// Prototype accessors + +JSC_DEFINE_CUSTOM_GETTER(jsByteLengthQueuingStrategyPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSByteLengthQueuingStrategy::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsByteLengthQueuingStrategyPrototypeGetter_highWaterMark, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* strategy = dynamicDowncast(JSValue::decode(thisValue)); + if (!strategy) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ByteLengthQueuingStrategy"_s); + return JSValue::encode(jsDoubleNumber(strategy->m_highWaterMark)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsByteLengthQueuingStrategyPrototypeGetter_size, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* strategy = dynamicDowncast(JSValue::decode(thisValue)); + if (!strategy) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ByteLengthQueuingStrategy"_s); + // The same per-realm function object for every instance of this's realm. + auto* globalObject = strategy->globalObject(); + return JSValue::encode(JSStreamsRuntime::from(globalObject)->byteLengthQueuingStrategySizeFunction(defaultGlobalObject(globalObject))); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.h b/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.h new file mode 100644 index 000000000000..edd68b9819a8 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSByteLengthQueuingStrategy.h @@ -0,0 +1,50 @@ +// JSByteLengthQueuingStrategy — the ByteLengthQueuingStrategy instance cell. +// Non-destructible. The per-realm `size` function +// (%byteLengthQueuingStrategySizeFunction%) is owned by JSStreamsRuntime, not the instance. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include + +namespace WebCore { + +class JSByteLengthQueuingStrategy final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSByteLengthQueuingStrategy* create(JSC::VM&, JSC::Structure*, double highWaterMark); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // No WriteBarrier / barrier-container / Weak members ⇒ no DECLARE_VISIT_CHILDREN. + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // [[highWaterMark]] — the `unrestricted double` given in the constructor, verbatim. + double m_highWaterMark { 0 }; + +private: + JSByteLengthQueuingStrategy(JSC::VM&, JSC::Structure*, double highWaterMark); + void finishCreation(JSC::VM&); +}; + +using JSByteLengthQueuingStrategyConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.cpp b/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.cpp new file mode 100644 index 000000000000..377c106689d9 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.cpp @@ -0,0 +1,269 @@ +#include "config.h" +#include "JSCountQueuingStrategy.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSStreamsRuntime.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsCountQueuingStrategyPrototypeGetter_constructor); +static JSC_DECLARE_CUSTOM_GETTER(jsCountQueuingStrategyPrototypeGetter_highWaterMark); +static JSC_DECLARE_CUSTOM_GETTER(jsCountQueuingStrategyPrototypeGetter_size); + +class JSCountQueuingStrategyPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSCountQueuingStrategyPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSCountQueuingStrategyPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSCountQueuingStrategyPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCountQueuingStrategyPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSCountQueuingStrategyPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSCountQueuingStrategyPrototype, JSCountQueuingStrategyPrototype::Base); + +// JSCountQueuingStrategyConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSCountQueuingStrategyConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSCountQueuingStrategyConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSCountQueuingStrategyConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSCountQueuingStrategyConstructor::subspaceForImpl(JSC::VM&); +template<> void JSCountQueuingStrategyConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSCountQueuingStrategyConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSCountQueuingStrategyConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSCountQueuingStrategyConstructor::s_info = { "CountQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCountQueuingStrategyConstructor) }; + +template<> JSValue JSCountQueuingStrategyConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSCountQueuingStrategyConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSCountQueuingStrategyConstructor); + +template<> GCClient::IsoSubspace* JSCountQueuingStrategyConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCountQueuingStrategyConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCountQueuingStrategyConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForCountQueuingStrategyConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCountQueuingStrategyConstructor = std::forward(space); }); +} + +template<> void JSCountQueuingStrategyConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "CountQueuingStrategy"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSCountQueuingStrategy::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSCountQueuingStrategyConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +// `QueuingStrategyInit init` — `highWaterMark` is a required `unrestricted double` member. +static double convertQueuingStrategyInit(JSC::VM& vm, JSGlobalObject* globalObject, JSValue init) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (!init.isObject()) { + if (!init.isUndefinedOrNull()) { + throwTypeError(globalObject, scope, "The QueuingStrategyInit argument must be an object"_s); + return 0; + } + throwTypeError(globalObject, scope, "QueuingStrategyInit requires a 'highWaterMark' member"_s); + return 0; + } + JSValue highWaterMark = asObject(init)->get(globalObject, builtinNames(vm).highWaterMarkPublicName()); + RETURN_IF_EXCEPTION(scope, 0); + if (highWaterMark.isUndefined()) { + throwTypeError(globalObject, scope, "QueuingStrategyInit requires a 'highWaterMark' member"_s); + return 0; + } + RELEASE_AND_RETURN(scope, highWaterMark.toNumber(globalObject)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSCountQueuingStrategyConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + if (callFrame->argumentCount() < 1) + return throwVMError(lexicalGlobalObject, scope, createNotEnoughArgumentsError(lexicalGlobalObject)); + + double highWaterMark = convertQueuingStrategyInit(vm, lexicalGlobalObject, callFrame->uncheckedArgument(0)); + RETURN_IF_EXCEPTION(scope, {}); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(JSCountQueuingStrategy::create(vm, structure, highWaterMark)); +} +JSC_ANNOTATE_HOST_FUNCTION(JSCountQueuingStrategyConstructorConstruct, JSCountQueuingStrategyConstructor::construct); + +// JSCountQueuingStrategyPrototype + +static const HashTableValue JSCountQueuingStrategyPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsCountQueuingStrategyPrototypeGetter_constructor, 0 } }, + { "highWaterMark"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCountQueuingStrategyPrototypeGetter_highWaterMark, 0 } }, + { "size"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsCountQueuingStrategyPrototypeGetter_size, 0 } }, +}; + +const ClassInfo JSCountQueuingStrategyPrototype::s_info = { "CountQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCountQueuingStrategyPrototype) }; + +void JSCountQueuingStrategyPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSCountQueuingStrategy::info(), JSCountQueuingStrategyPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSCountQueuingStrategy + +const ClassInfo JSCountQueuingStrategy::s_info = { "CountQueuingStrategy"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCountQueuingStrategy) }; + +JSCountQueuingStrategy::JSCountQueuingStrategy(VM& vm, Structure* structure, double highWaterMark) + : Base(vm, structure) + , m_highWaterMark(highWaterMark) +{ +} + +void JSCountQueuingStrategy::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSCountQueuingStrategy* JSCountQueuingStrategy::create(VM& vm, Structure* structure, double highWaterMark) +{ + auto* strategy = new (NotNull, allocateCell(vm)) JSCountQueuingStrategy(vm, structure, highWaterMark); + strategy->finishCreation(vm); + return strategy; +} + +Structure* JSCountQueuingStrategy::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSCountQueuingStrategy::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSCountQueuingStrategyPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSCountQueuingStrategyPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSCountQueuingStrategy::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSCountQueuingStrategy::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSCountQueuingStrategy::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCountQueuingStrategy.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCountQueuingStrategy = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForCountQueuingStrategy.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCountQueuingStrategy = std::forward(space); }); +} + +// Prototype accessors + +JSC_DEFINE_CUSTOM_GETTER(jsCountQueuingStrategyPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSCountQueuingStrategy::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsCountQueuingStrategyPrototypeGetter_highWaterMark, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* strategy = dynamicDowncast(JSValue::decode(thisValue)); + if (!strategy) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "CountQueuingStrategy"_s); + return JSValue::encode(jsDoubleNumber(strategy->m_highWaterMark)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsCountQueuingStrategyPrototypeGetter_size, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* strategy = dynamicDowncast(JSValue::decode(thisValue)); + if (!strategy) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "CountQueuingStrategy"_s); + // The same per-realm function object for every instance of this's realm. + auto* globalObject = strategy->globalObject(); + return JSValue::encode(JSStreamsRuntime::from(globalObject)->countQueuingStrategySizeFunction(defaultGlobalObject(globalObject))); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.h b/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.h new file mode 100644 index 000000000000..64a6a8f652f4 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSCountQueuingStrategy.h @@ -0,0 +1,50 @@ +// JSCountQueuingStrategy — the CountQueuingStrategy instance cell. Non-destructible. +// The per-realm `size` function (%countQueuingStrategySizeFunction%) is owned by +// JSStreamsRuntime, not the instance. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include + +namespace WebCore { + +class JSCountQueuingStrategy final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSCountQueuingStrategy* create(JSC::VM&, JSC::Structure*, double highWaterMark); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // No WriteBarrier / barrier-container / Weak members ⇒ no DECLARE_VISIT_CHILDREN. + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // [[highWaterMark]] — the `unrestricted double` given in the constructor, verbatim. + double m_highWaterMark { 0 }; + +private: + JSCountQueuingStrategy(JSC::VM&, JSC::Structure*, double highWaterMark); + void finishCreation(JSC::VM&); +}; + +using JSCountQueuingStrategyConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSCrossRealmTransformState.cpp b/src/jsc/bindings/webcore/streams/JSCrossRealmTransformState.cpp new file mode 100644 index 000000000000..c79fb893da64 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSCrossRealmTransformState.cpp @@ -0,0 +1,68 @@ +#include "config.h" +#include "JSCrossRealmTransformState.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSReadableStreamDefaultController.h" +#include "JSWritableStreamDefaultController.h" +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +const ClassInfo JSCrossRealmTransformState::s_info = { "CrossRealmTransformState"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCrossRealmTransformState) }; + +JSCrossRealmTransformState::JSCrossRealmTransformState(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSCrossRealmTransformState::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSCrossRealmTransformState* JSCrossRealmTransformState::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSCrossRealmTransformState(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSCrossRealmTransformState::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSCrossRealmTransformState::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForCrossRealmTransformState.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCrossRealmTransformState = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForCrossRealmTransformState.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForCrossRealmTransformState = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSCrossRealmTransformState); + +template +void JSCrossRealmTransformState::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_port); + visitor.append(thisObject->m_backpressurePromise); + visitor.append(thisObject->m_readableController); + visitor.append(thisObject->m_writableController); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSCrossRealmTransformState.h b/src/jsc/bindings/webcore/streams/JSCrossRealmTransformState.h new file mode 100644 index 000000000000..e026541256d6 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSCrossRealmTransformState.h @@ -0,0 +1,58 @@ +// JSCrossRealmTransformState — one cell per cross-realm (transferred) stream endpoint. +// Transferable streams are NOT implemented: CrossRealmTransform.cpp may stub its entry +// points, but this cell and the CrossRealm enum arms stay in the frozen headers so nothing +// has to be re-frozen later. +// The port's message/messageerror handlers MUST be registered through the port's GC-visited +// listener machinery with THIS cell as the context (a raw-pointer native listener is a UAF). +// Internal cell: no prototype, no constructor. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSCrossRealmTransformState final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSCrossRealmTransformState* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_port, m_backpressurePromise, m_readableController, + // m_writableController. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The JSMessagePort wrapper cell this endpoint sends/receives on. + JSC::WriteBarrier m_port; + // MUTABLE — the writable side's message handler reassigns it on every "pull"/"error". + JSC::WriteBarrier m_backpressurePromise; + // Back-pointers to the controller in THIS realm — EXACT-TYPED (the subsystem allows + // exactly ONE erased back-pointer, JSReadableStream::m_controller, so this is not a + // second one). EXACTLY ONE of the two is non-null: m_readableController on the readable + // (transfer-receiving) endpoint, m_writableController on the writable endpoint. Dispatch + // on which is non-null; never jsCast an erased slot here. + JSC::WriteBarrier m_readableController; + JSC::WriteBarrier m_writableController; + +private: + JSCrossRealmTransformState(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSDirectSinkCloseState.h b/src/jsc/bindings/webcore/streams/JSDirectSinkCloseState.h new file mode 100644 index 000000000000..9a9c1fa87940 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSDirectSinkCloseState.h @@ -0,0 +1,52 @@ +// JSDirectSinkCloseState — the context cell of readDirectStream's bound onClose callable: +// the port of the `{underlyingSource, closePromiseCapability}` bound `this`. +// Internal cell: no prototype, no constructor. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSDirectSinkCloseState final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSDirectSinkCloseState* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit ALL THREE: m_underlyingSource, m_sinkController, + // m_closePromise. (An unvisited m_closePromise is a premature collection of the + // promise handed to Rust.) + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // the direct stream's user underlyingSource (its `cancel` runs from onClose). + JSC::WriteBarrier m_underlyingSource; + // the JS sink controller driving the source; onClose must end() it so the cell + // detaches from the native sink before it can be collected. + JSC::WriteBarrier m_sinkController; + // the close-capability promise returned to the caller when `pull` returned synchronously + // without closing; initially null, armed by readDirectStream, resolved by onClose. + JSC::WriteBarrier m_closePromise; + +private: + JSDirectSinkCloseState(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSDirectStreamController.cpp b/src/jsc/bindings/webcore/streams/JSDirectStreamController.cpp new file mode 100644 index 000000000000..4498298ee503 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSDirectStreamController.cpp @@ -0,0 +1,862 @@ +#include "config.h" +#include "JSDirectStreamController.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "helpers.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSReadRequest.h" +#include "JSReadableStream.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSStreamsRuntime.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static constexpr auto directControllerClosedMessage = "ReadableStreamDirectController is now closed"_s; + +const ClassInfo JSDirectStreamController::s_info = { "DirectStreamController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSDirectStreamController) }; + +JSDirectStreamController::JSDirectStreamController(VM& vm, Structure* structure, DirectSinkKind sinkKind) + : Base(vm, structure) +{ + m_sinkKind = sinkKind; +} + +JSDirectStreamController::~JSDirectStreamController() = default; + +void JSDirectStreamController::destroy(JSCell* cell) +{ + static_cast(cell)->JSDirectStreamController::~JSDirectStreamController(); +} + +void JSDirectStreamController::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSDirectStreamController* JSDirectStreamController::create(VM& vm, Structure* structure, DirectSinkKind sinkKind) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSDirectStreamController(vm, structure, sinkKind); + cell->finishCreation(vm); + return cell; +} + +// Deliver buffered data to a waiting reader at the end of this tick via the runtime's +// deferred-task service (JSStreamsRuntime.cpp); a no-op there if the data was already taken. +// A write made inside pull() runs before the read that triggered it is recorded, so arming +// does not require a waiting consumer. +void JSDirectStreamController::armEndOfTickFlush(JSGlobalObject* globalObject) +{ + if (m_endOfTickFlushArmed || m_closed || !m_stream) + return; + JSStreamsRuntime::from(globalObject)->armEndOfTickFlush(globalObject, this); + m_endOfTickFlushArmed = true; +} + +Structure* JSDirectStreamController::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSDirectStreamController::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForDirectStreamController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForDirectStreamController = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForDirectStreamController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForDirectStreamController = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSDirectStreamController); + +template +void JSDirectStreamController::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_underlyingSource); + visitor.append(thisObject->m_pendingRead); + visitor.append(thisObject->m_deferCloseReason); + visitor.append(thisObject->m_arrayBufferSink); + visitor.append(thisObject->m_array); + visitor.append(thisObject->m_closingPromise); + visitor.append(thisObject->m_finalChunk); + Locker locker { thisObject->cellLock() }; + thisObject->m_textAccumulator.visit(locker, visitor); +} + +static size_t byteLengthOf(JSValue value) +{ + if (auto* view = dynamicDowncast(value)) + return view->isDetached() ? 0 : view->byteLength(); + if (auto* buffer = dynamicDowncast(value)) { + auto* impl = buffer->impl(); + return (!impl || impl->isDetached()) ? 0 : impl->byteLength(); + } + return 0; +} + +static JSValue callArrayBufferSinkMethod(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* sink, const Identifier& name, MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue function = sink->get(globalObject, name); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSC::call(globalObject, function, sink, args, "ArrayBufferSink method is not a function"_s)); +} + +static JSValue writeToArrayBufferSink(JSGlobalObject* globalObject, JSDirectStreamController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + JSObject* sink = controller->m_arrayBufferSink.get(); + if (!sink) [[unlikely]] + return jsUndefined(); + MarkedArgumentBuffer args; + args.append(chunk); + return callArrayBufferSinkMethod(vm, globalObject, sink, builtinNames(vm).writePublicName(), args); +} + +static JSValue writeToTextSink(JSGlobalObject* globalObject, JSDirectStreamController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto& accumulator = controller->m_textAccumulator; + + if (chunk.isString()) { + auto* string = asString(chunk); + unsigned length = string->length(); + if (length > 0) { + String value = string->value(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + accumulator.rope.append(value); + if (accumulator.rope.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return {}; + } + accumulator.hasString = true; + accumulator.estimatedLength += length; + } + return jsNumber(length); + } + + size_t byteLength = 0; + if (auto* view = dynamicDowncast(chunk)) + byteLength = view->isDetached() ? 0 : view->byteLength(); + else if (auto* buffer = dynamicDowncast(chunk)) + byteLength = (!buffer->impl() || buffer->impl()->isDetached()) ? 0 : buffer->impl()->byteLength(); + else { + throwTypeError(globalObject, scope, "Expected text, ArrayBuffer or ArrayBufferView"_s); + return {}; + } + + if (byteLength > 0) { + accumulator.hasBuffer = true; + JSString* ropeString = nullptr; + if (!accumulator.rope.isEmpty()) { + ropeString = jsString(vm, accumulator.rope.toString()); + RETURN_IF_EXCEPTION(scope, {}); + } + // GC-allocation is done; the barrier container is only mutated under the cell lock. + Locker locker { controller->cellLock() }; + if (ropeString) { + accumulator.pieces.append(WriteBarrier(vm, controller, ropeString)); + accumulator.rope.clear(); + } + accumulator.pieces.append(WriteBarrier(vm, controller, chunk)); + } + accumulator.estimatedLength += byteLength; + return jsNumber(byteLength); +} + +static JSValue writeToArraySink(JSGlobalObject* globalObject, JSDirectStreamController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSArray* array = controller->m_array.get(); + if (!array) [[unlikely]] + return jsUndefined(); + array->push(globalObject, chunk); + RETURN_IF_EXCEPTION(scope, {}); + JSValue byteLength = chunk.get(globalObject, vm.propertyNames->byteLength); + RETURN_IF_EXCEPTION(scope, {}); + if (byteLength.toBoolean(globalObject)) + return byteLength; + RELEASE_AND_RETURN(scope, chunk.get(globalObject, vm.propertyNames->length)); +} + +static JSValue writeToDirectSink(JSGlobalObject* globalObject, JSDirectStreamController* controller, JSValue chunk) +{ + switch (controller->m_sinkKind) { + case DirectSinkKind::ArrayBuffer: + return writeToArrayBufferSink(globalObject, controller, chunk); + case DirectSinkKind::Text: + return writeToTextSink(globalObject, controller, chunk); + case DirectSinkKind::Array: + return writeToArraySink(globalObject, controller, chunk); + } + RELEASE_ASSERT_NOT_REACHED(); + return {}; +} + +static String finishTextSink(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller) +{ + auto& accumulator = controller->m_textAccumulator; + if (!accumulator.hasString && !accumulator.hasBuffer) + return emptyString(); + + auto scope = DECLARE_THROW_SCOPE(vm); + // Pure-string rope: the ONLY arm of the direct Text sink that strips a leading BOM. + if (accumulator.hasString && !accumulator.hasBuffer) { + if (Bun::WebStreams::exceedsStringLimit(accumulator.rope.length())) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return String(); + } + String rope = accumulator.rope.toString(); + if (rope.length() && rope[0] == 0xFEFF) + return rope.substring(1); + return rope; + } + + Vector bytes; + for (auto& piece : accumulator.pieces) { + JSValue value = piece.get(); + if (value.isString()) { + String string = asString(value)->value(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto utf8 = string.utf8(); + bytes.append(std::span { reinterpret_cast(utf8.data()), utf8.length() }); + } else if (auto* view = dynamicDowncast(value)) { + if (!view->isDetached()) + bytes.append(view->span()); + } else if (auto* buffer = dynamicDowncast(value)) { + if (buffer->impl() && !buffer->impl()->isDetached()) + bytes.append(buffer->impl()->span()); + } + } + if (!accumulator.rope.isEmpty()) { + String rope = accumulator.rope.toString(); + if (rope[0] == 0xFEFF) + rope = rope.substring(1); + auto utf8 = rope.utf8(); + bytes.append(std::span { reinterpret_cast(utf8.data()), utf8.length() }); + } + if (Bun::WebStreams::exceedsStringLimit(bytes.size())) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return String(); + } + return String::fromUTF8ReplacingInvalidSequences(bytes.span()); +} + +static JSValue endTextSink(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (controller->m_calledDone) + return jsEmptyString(vm); + controller->m_calledDone = true; + String result = finishTextSink(vm, globalObject, controller); + // The accumulated payload must not stay alive on the controller (it lives as long + // as the stream); the result string owns everything it needs. + { + Locker locker { controller->cellLock() }; + controller->m_textAccumulator.reset(locker); + } + RETURN_IF_EXCEPTION(scope, {}); + JSString* resultString = jsString(vm, result); + RETURN_IF_EXCEPTION(scope, {}); + if (auto* closingPromise = controller->m_closingPromise.get()) + closingPromise->fulfill(vm, resultString); + return resultString; +} + +static JSValue endArraySink(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (controller->m_calledDone) [[unlikely]] { + JSArray* empty = constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + return empty; + } + controller->m_calledDone = true; + JSArray* array = controller->m_array.get(); + // The array is the caller's result now; the controller must not keep it alive. + controller->m_array.clear(); + if (auto* closingPromise = controller->m_closingPromise.get()) { + resolvePromise(globalObject, closingPromise, array); + RETURN_IF_EXCEPTION(scope, {}); + } + return array; +} + +// `sink.end()`. May throw; the ArrayBufferSink slot is only cleared on success. +static JSValue endDirectSink(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_sinkKind) { + case DirectSinkKind::ArrayBuffer: { + JSObject* sink = controller->m_arrayBufferSink.get(); + if (!sink) [[unlikely]] + return jsUndefined(); + MarkedArgumentBuffer args; + JSValue flushed = callArrayBufferSinkMethod(vm, globalObject, sink, builtinNames(vm).endPublicName(), args); + RETURN_IF_EXCEPTION(scope, {}); + controller->m_arrayBufferSink.clear(); + return flushed; + } + case DirectSinkKind::Text: + RELEASE_AND_RETURN(scope, endTextSink(vm, globalObject, controller)); + case DirectSinkKind::Array: + RELEASE_AND_RETURN(scope, endArraySink(vm, globalObject, controller)); + } + RELEASE_ASSERT_NOT_REACHED(); + return {}; +} + +// `sink.flush()`: only the ArrayBuffer sink produces bytes; the Text/Array sinks return 0. +static JSValue flushDirectSink(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller) +{ + switch (controller->m_sinkKind) { + case DirectSinkKind::ArrayBuffer: { + JSObject* sink = controller->m_arrayBufferSink.get(); + if (!sink) [[unlikely]] + return jsNumber(0); + MarkedArgumentBuffer args; + return callArrayBufferSinkMethod(vm, globalObject, sink, builtinNames(vm).flushPublicName(), args); + } + case DirectSinkKind::Text: + case DirectSinkKind::Array: + return jsNumber(0); + } + RELEASE_ASSERT_NOT_REACHED(); + return {}; +} + +// `sink.close(error)`: the Text/Array sinks fulfill their closing promise with the partial result. +static void closeDirectSinkForError(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller, JSValue error) +{ + switch (controller->m_sinkKind) { + case DirectSinkKind::ArrayBuffer: { + JSObject* sink = controller->m_arrayBufferSink.get(); + if (!sink) + return; + controller->m_arrayBufferSink.clear(); + MarkedArgumentBuffer args; + args.append(error); + callArrayBufferSinkMethod(vm, globalObject, sink, builtinNames(vm).closePublicName(), args); + return; + } + case DirectSinkKind::Text: + if (!controller->m_calledDone) + endTextSink(vm, globalObject, controller); + return; + case DirectSinkKind::Array: + if (!controller->m_calledDone) + endArraySink(vm, globalObject, controller); + return; + } + RELEASE_ASSERT_NOT_REACHED(); +} + +// The Bun-only `underlyingSource.close(reason)` lifecycle callback; the call is swallowed. +static void callUnderlyingSourceClose(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller, JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSObject* underlyingSource = controller->m_underlyingSource.get(); + if (!underlyingSource) + return; + JSValue closeFunction = underlyingSource->get(globalObject, builtinNames(vm).closePublicName()); + RETURN_IF_EXCEPTION(scope, ); + auto callData = JSC::getCallData(closeFunction); + if (callData.type == CallData::Type::None) + return; + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + MarkedArgumentBuffer args; + args.append(reason); + JSC::call(globalObject, closeFunction, callData, underlyingSource, args); + if (catchScope.exception()) [[unlikely]] { + if (takeAbruptCompletion(globalObject, catchScope).isEmpty()) + return; + } +} + +void JSDirectStreamController::handleError(JSGlobalObject* globalObject, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (!m_closed) { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + closeDirectSinkForError(vm, globalObject, this, error); + if (catchScope.exception()) [[unlikely]] { + if (takeAbruptCompletion(globalObject, catchScope).isEmpty()) + return; + } + } + m_closed = true; + + callUnderlyingSourceClose(vm, globalObject, this, error); + RETURN_IF_EXCEPTION(scope, ); + + if (auto* pendingRead = m_pendingRead.get()) { + m_pendingRead.clear(); + rejectPromise(globalObject, pendingRead, error); + RETURN_IF_EXCEPTION(scope, ); + } + + auto* stream = m_stream.get(); + if (stream && stream->m_state == ReadableStreamState::Readable) + RELEASE_AND_RETURN(scope, readableStreamError(globalObject, stream, error)); +} + +JSValue JSDirectStreamController::onPull(JSGlobalObject* globalObject) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + // The one-shot final chunk armed by onClose: deliver it, then close. + if (m_finalChunkArmed) { + m_finalChunkArmed = false; + JSValue chunk = m_finalChunk.get(); + m_finalChunk.clear(); + JSObject* result = createIteratorResultObject(globalObject, chunk, false); + RETURN_IF_EXCEPTION(scope, {}); + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + promise->fulfill(vm, result); + RETURN_IF_EXCEPTION(scope, {}); + if (auto* stream = m_stream.get()) { + readableStreamCloseIfPossible(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + } + return promise; + } + + auto* stream = m_stream.get(); + if (!stream || stream->m_state != ReadableStreamState::Readable || m_closed) + return jsUndefined(); + // Re-entrant pull while a pull is already running. + if (m_deferClose == -1) + return jsUndefined(); + + m_deferClose = -1; + m_deferFlush = -1; + + JSValue abrupt; + bool threw = false; + { + StreamAsyncContextScope asyncContextScope(globalObject, stream); + JSObject* underlyingSource = m_underlyingSource.get(); + JSValue result; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + // Unlike the spec, pull may be called many times; backpressure is the destination's job. + JSValue pullFunction = underlyingSource->get(globalObject, builtinNames(vm).pullPublicName()); + if (!catchScope.exception()) [[likely]] { + MarkedArgumentBuffer args; + args.append(this); + result = JSC::call(globalObject, pullFunction, underlyingSource, args, "underlyingSource.pull is not a function"_s); + } + if (catchScope.exception()) [[unlikely]] { + threw = true; + abrupt = takeAbruptCompletion(globalObject, catchScope); + } + } + if (threw) { + // A synchronous throw from pull errors the stream and rejects the returned read. + if (abrupt) + handleError(globalObject, abrupt); + } else if (auto* pullPromise = dynamicDowncast(result)) { + // The un-handled result promise is load-bearing: a rejected pull must still unhandledReject. + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* rejectionResult = JSPromise::create(vm, globalObject->promiseStructure()); + pullPromise->performPromiseThenWithContext(vm, globalObject, jsUndefined(), runtime->onDirectPullRejected(), rejectionResult, this); + } + } + + int8_t deferredClose = m_deferClose; + int8_t deferredFlush = m_deferFlush; + m_deferClose = 0; + m_deferFlush = 0; + + if (threw && abrupt) { + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, abrupt)); + } + // A VM termination from the pull, or a failure while registering the rejection reaction. + RETURN_IF_EXCEPTION(scope, {}); + + // controller.error() inside pull is not deferred: re-validate before adding a read request. + stream = m_stream.get(); + if (!stream || stream->m_state != ReadableStreamState::Readable) { + if (auto* pendingRead = m_pendingRead.get()) + return pendingRead; + if (stream && stream->m_state == ReadableStreamState::Errored) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, stream->m_storedError.get())); + JSObject* doneResult = createIteratorResultObject(globalObject, jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, {}); + auto* doneP = JSPromise::create(vm, globalObject->promiseStructure()); + doneP->fulfill(vm, doneResult); + return doneP; + } + + JSPromise* promiseToReturn = nullptr; + if (!m_pendingRead) { + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + m_pendingRead.set(vm, this, promise); + promiseToReturn = promise; + } else { + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* readRequest = JSReadRequest::create(vm, runtime->readRequestStructure(defaultGlobalObject(globalObject)), ReadRequestKind::Promise, promise); + readableStreamAddReadRequest(vm, stream, readRequest); + promiseToReturn = promise; + } + + if (deferredClose == 1) { + JSValue reason = m_deferCloseReason.get(); + m_deferCloseReason.clear(); + onClose(globalObject, reason); + RETURN_IF_EXCEPTION(scope, {}); + return promiseToReturn; + } + if (deferredFlush == 1) { + onFlush(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + return promiseToReturn; +} + +// The pump's head-of-line promise (m_pendingRead) is the active consumer only while no +// non-promise read request (pipeTo / tee / for-await) is queued ahead of it: those are +// registered in [[readRequests]] BEFORE the pull runs and must get chunks via chunkSteps. +static bool headOfLinePromiseIsActiveConsumer(JSReadableStreamDefaultReader* reader) +{ + Locker locker { reader->cellLock() }; + if (reader->m_readRequests.isEmpty()) + return true; + return reader->m_readRequests.first().get()->kind() == ReadRequestKind::Promise; +} + +void JSDirectStreamController::onClose(JSGlobalObject* globalObject, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* stream = m_stream.get(); + if (!stream || stream->m_state != ReadableStreamState::Readable) + return; + if (m_deferClose != 0) { + m_deferClose = 1; + m_deferCloseReason.set(vm, this, reason); + return; + } + if (m_closed || (m_sinkKind == DirectSinkKind::ArrayBuffer && !m_arrayBufferSink)) + return; + // No "Closing" stream state exists: m_closed set here is what blocks re-entry. + m_closed = true; + + callUnderlyingSourceClose(vm, globalObject, this, reason); + RETURN_IF_EXCEPTION(scope, ); + + JSValue flushed; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + flushed = endDirectSink(vm, globalObject, this); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (!thrown) + return; + if (auto* pendingRead = m_pendingRead.get()) { + m_pendingRead.clear(); + rejectPromise(globalObject, pendingRead, thrown); + return; + } + throwException(globalObject, scope, thrown); + return; + } + } + + size_t flushedByteLength = byteLengthOf(flushed); + if (readableStreamHasDefaultReader(stream)) { + auto* reader = static_cast(stream->m_reader.get()); + auto* pendingRead = m_pendingRead.get(); + // Skipped when a non-promise read request is at the head: it is delivered below. + if (pendingRead && flushedByteLength && headOfLinePromiseIsActiveConsumer(reader)) { + m_pendingRead.clear(); + JSObject* result = createIteratorResultObject(globalObject, flushed, false); + RETURN_IF_EXCEPTION(scope, ); + pendingRead->fulfill(vm, result); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, readableStreamCloseIfPossible(globalObject, stream)); + } + } + + if (flushedByteLength) { + // The reader can have been released while the (async) pull was still running. + if (readableStreamHasDefaultReader(stream) && readableStreamGetNumReadRequests(stream) > 0) { + readableStreamFulfillReadRequest(globalObject, stream, flushed, false); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, readableStreamCloseIfPossible(globalObject, stream)); + } + // Nobody is reading: the NEXT read() delivers this chunk, then closes. + m_finalChunk.set(vm, this, flushed); + m_finalChunkArmed = true; + return; + } + + if (auto* pendingRead = m_pendingRead.get()) { + m_pendingRead.clear(); + JSObject* doneResult = createIteratorResultObject(globalObject, jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, ); + pendingRead->fulfill(vm, doneResult); + RETURN_IF_EXCEPTION(scope, ); + } + RELEASE_AND_RETURN(scope, readableStreamCloseIfPossible(globalObject, stream)); +} + +void JSDirectStreamController::onFlush(JSGlobalObject* globalObject) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* stream = m_stream.get(); + if (!stream) + return; + if (m_closed || (m_sinkKind == DirectSinkKind::ArrayBuffer && !m_arrayBufferSink)) + return; + // No default reader: return WITHOUT deferring. + auto* reader = dynamicDowncast(stream->m_reader.get()); + if (!reader) + return; + + if (auto* pendingRead = m_pendingRead.get()) { + m_pendingRead.clear(); + JSValue flushed = flushDirectSink(vm, globalObject, this); + RETURN_IF_EXCEPTION(scope, ); + if (byteLengthOf(flushed)) { + // A non-promise read request at the head is the active consumer: deliver the + // chunk through its own chunkSteps and leave the head-of-line promise pending + // (its registrar drops it). + if (!headOfLinePromiseIsActiveConsumer(reader)) { + m_pendingRead.set(vm, this, pendingRead); + RELEASE_AND_RETURN(scope, readableStreamFulfillReadRequest(globalObject, stream, flushed, false)); + } + { + Locker locker { reader->cellLock() }; + if (!reader->m_readRequests.isEmpty()) { + auto nextRequest = reader->m_readRequests.takeFirst(); + auto* readRequest = nextRequest.get(); + if (readRequest && readRequest->kind() == ReadRequestKind::Promise) + m_pendingRead.set(vm, this, uncheckedDowncast(readRequest->m_context.get())); + } + } + JSObject* result = createIteratorResultObject(globalObject, flushed, false); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, pendingRead->fulfill(vm, result)); + } + m_pendingRead.set(vm, this, pendingRead); + return; + } + + if (readableStreamGetNumReadRequests(stream) > 0) { + JSValue flushed = flushDirectSink(vm, globalObject, this); + RETURN_IF_EXCEPTION(scope, ); + if (byteLengthOf(flushed)) + RELEASE_AND_RETURN(scope, readableStreamFulfillReadRequest(globalObject, stream, flushed, false)); + return; + } + + if (m_deferFlush == -1) + m_deferFlush = 1; +} + +// The rejection reaction of the user pull()'s returned promise ([reaction-convention]). +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onDirectPullRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + JSValue error = callFrame->argument(0); + controller->handleError(globalObject, error); + RETURN_IF_EXCEPTION(scope, {}); + // Re-throw so the (deliberately un-handled) result promise rejects with the pull error. + throwException(globalObject, scope, error); + return {}; +} + +// The FIVE public own methods are JSBoundFunctions over these [bound-convention] targets. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundDirectWrite, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(0)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + if (controller->m_closed) + return throwVMTypeError(globalObject, scope, directControllerClosedMessage); + JSValue wrote = writeToDirectSink(globalObject, controller, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + controller->armEndOfTickFlush(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(wrote); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundDirectClose, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(0)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + if (controller->m_closed) + return throwVMTypeError(globalObject, scope, directControllerClosedMessage); + controller->onClose(globalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundDirectFlush, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(0)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + if (controller->m_closed) + return throwVMTypeError(globalObject, scope, directControllerClosedMessage); + controller->onFlush(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundDirectError, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(0)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + if (controller->m_closed) + return throwVMTypeError(globalObject, scope, directControllerClosedMessage); + controller->handleError(globalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// Installs write/end/close/flush/error as detachable OWN JSBoundFunction properties. +static void installDirectControllerMethods(JSC::VM& vm, JSGlobalObject* globalObject, JSDirectStreamController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto& names = builtinNames(vm); + struct Method { + const Identifier& key; + JSFunction* target; + double length; + }; + const Method methods[] = { + { names.writePublicName(), runtime->boundDirectWrite(), 1 }, + { names.endPublicName(), runtime->boundDirectClose(), 0 }, + { names.closePublicName(), runtime->boundDirectClose(), 1 }, + { names.flushPublicName(), runtime->boundDirectFlush(), 0 }, + { vm.propertyNames->error, runtime->boundDirectError(), 1 }, + }; + for (const auto& method : methods) { + MarkedArgumentBuffer boundArgs; + boundArgs.append(controller); + String name = method.key.string(); + auto* boundFunction = JSBoundFunction::create(vm, globalObject, method.target, jsUndefined(), ArgList(boundArgs), method.length, jsString(vm, name), makeSource(name, SourceOrigin(), SourceTaintedOrigin::Untainted)); + RETURN_IF_EXCEPTION(scope, ); + controller->putDirect(vm, method.key, boundFunction, 0); + } +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSDirectStreamController; +using WebCore::JSStreamsRuntime; + +void setUpDirectStreamController(JSC::JSGlobalObject* globalObject, JSReadableStream* stream, DirectSinkKind sinkKind, double highWaterMark) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* zigGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* controller = JSDirectStreamController::create(vm, runtime->directStreamControllerStructure(zigGlobalObject), sinkKind); + controller->m_stream.set(vm, controller, stream); + if (JSObject* underlyingSource = stream->m_directUnderlyingSource.get()) + controller->m_underlyingSource.set(vm, controller, underlyingSource); + + switch (sinkKind) { + case DirectSinkKind::ArrayBuffer: { + JSObject* sinkConstructor = zigGlobalObject->ArrayBufferSink(); + auto constructData = JSC::getConstructData(sinkConstructor); + MarkedArgumentBuffer constructArgs; + JSObject* sink = JSC::construct(globalObject, sinkConstructor, constructData, constructArgs); + RETURN_IF_EXCEPTION(scope, ); + controller->m_arrayBufferSink.set(vm, controller, sink); + JSObject* options = constructEmptyObject(globalObject); + // Forwarded iff the raw strategy highWaterMark is a non-zero, non-NaN number. + if (stream->m_bunHighWaterMarkIsNumber && highWaterMark != 0 && !std::isnan(highWaterMark)) + options->putDirect(vm, builtinNames(vm).highWaterMarkPublicName(), jsNumber(highWaterMark), 0); + options->putDirect(vm, builtinNames(vm).streamPublicName(), jsBoolean(true), 0); + options->putDirect(vm, builtinNames(vm).asUint8ArrayPublicName(), jsBoolean(true), 0); + MarkedArgumentBuffer startArgs; + startArgs.append(options); + WebCore::callArrayBufferSinkMethod(vm, globalObject, sink, builtinNames(vm).startPublicName(), startArgs); + RETURN_IF_EXCEPTION(scope, ); + break; + } + case DirectSinkKind::Text: { + controller->m_closingPromise.set(vm, controller, JSPromise::create(vm, globalObject->promiseStructure())); + break; + } + case DirectSinkKind::Array: { + JSArray* array = constructEmptyArray(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, ); + controller->m_array.set(vm, controller, array); + controller->m_closingPromise.set(vm, controller, JSPromise::create(vm, globalObject->promiseStructure())); + break; + } + } + + WebCore::installDirectControllerMethods(vm, globalObject, controller); + RETURN_IF_EXCEPTION(scope, ); + + stream->m_controller.set(vm, stream, controller); + stream->m_controllerKind = ControllerKind::Direct; + stream->m_directUnderlyingSource.clear(); + stream->m_bunMode = BunStreamMode::Default; +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSDirectStreamController.h b/src/jsc/bindings/webcore/streams/JSDirectStreamController.h new file mode 100644 index 000000000000..a8e801bd3d28 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSDirectStreamController.h @@ -0,0 +1,115 @@ +// JSDirectStreamController — the Bun `type:"direct"` controller for JS consumption. ONE +// class, three sink flavors (DirectSinkKind). It is NOT a spec controller: no enqueue, no +// desiredSize, no byobRequest; its five public methods (write, end, close, flush, error) are +// per-controller OWN JSBoundFunction properties ([bound-convention]) — there is no prototype +// method table and no constructor class. The stream's m_controllerKind is +// ControllerKind::Direct. +// DESTRUCTIBLE: owns a WTF::StringBuilder + a Vector of barriers. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +// The ONE shared BunTextAccumulator value type ("one implementation, two owners" — the +// other owner is the standalone JSBunStandaloneTextSink). Not a cycle: +// BunStandaloneTextSink.h does not include this header. +#include "BunStandaloneTextSink.h" +#include +#include +#include + +namespace WebCore { + +class JSDirectStreamController final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + static JSDirectStreamController* create(JSC::VM&, JSC::Structure*, Bun::WebStreams::DirectSinkKind); + static void destroy(JSC::JSCell*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream, m_underlyingSource, m_pendingRead, + // m_deferCloseReason, m_arrayBufferSink, m_array, m_closingPromise, m_finalChunk, and + // the barrier container m_textAccumulator.pieces (via + // m_textAccumulator.visit(locker, visitor) inside ONE `Locker { cellLock() }` scope + // taken by THIS visitChildrenImpl — cellLock() is non-recursive; see StreamQueue.h). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Core state + // $controlledReadableStream + JSC::WriteBarrier m_stream; + // the USER underlyingSource object; `pull` / `close` are re-[[Get]] on each use + // (deliberate: the direct protocol is NOT the spec's captured-once protocol). + JSC::WriteBarrier m_underlyingSource; + // _pendingRead — the promise the in-flight read() is waiting on. handleError rejects + // AND CLEARS it. + JSC::WriteBarrier m_pendingRead; + // _deferCloseReason + JSC::WriteBarrier m_deferCloseReason; + // -1 = pull in progress (reentrancy guard), 0 = idle, 1 = close deferred + int8_t m_deferClose { 0 }; + // -1 = pull in progress, 0 = idle, 1 = flush deferred + int8_t m_deferFlush { 0 }; + // Once closed, the five methods are no-ops (there is NO "swap all 5 methods to a + // throwing stub" trick). + bool m_closed { false }; + // which of the 3 sink flavors this controller runs. + DirectSinkKind m_sinkKind { DirectSinkKind::ArrayBuffer }; + + // ArrayBuffer sink: a real Bun.ArrayBufferSink cell (ArrayBuffer kind only). + JSC::WriteBarrier m_arrayBufferSink; + + // Text sink: the ONE shared createTextStream accumulator value type + // (BunStandaloneTextSink.h), also owned by the standalone JSBunStandaloneTextSink — one + // implementation, two owners. Its `pieces` barrier container is mutated AND visited + // under THIS cell's cellLock() (see the visit-list comment above). This arm does NOT + // BOM-strip. + Bun::WebStreams::BunTextAccumulator m_textAccumulator; + + // Array sink. + JSC::WriteBarrier m_array; + + // Text/Array closing capability. + JSC::WriteBarrier m_closingPromise; + bool m_calledDone { false }; + + // End-of-tick auto-flush (the JS-facing analogue of the HTTP sink's AutoFlusher): + // armed by write() when data is buffered below the HWM while a consumer waits; the + // deferred task runs right after the current microtask drain and delivers it. + bool m_endOfTickFlushArmed { false }; + void armEndOfTickFlush(JSC::JSGlobalObject*); + + // Final-chunk-on-close: the NEXT read() delivers m_finalChunk then closes. onPull checks + // m_finalChunkArmed FIRST. + JSC::WriteBarrier m_finalChunk; + bool m_finalChunkArmed { false }; + + // The state machine. All userJS: YES. + // the READ pump: the default reader's read()/readMany() on a Direct stream lands here. + JSC::JSValue onPull(JSC::JSGlobalObject*); + // `end()` / `close(reason)` — reason may be the empty JSValue (absent). + void onClose(JSC::JSGlobalObject*, JSC::JSValue reason); + // `flush()` — BRANCH ORDER IS LOAD-BEARING. + void onFlush(JSC::JSGlobalObject*); + // handleDirectStreamError. + void handleError(JSC::JSGlobalObject*, JSC::JSValue error); + +private: + JSDirectStreamController(JSC::VM&, JSC::Structure*, Bun::WebStreams::DirectSinkKind); + ~JSDirectStreamController(); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSOneShotDirectSink.h b/src/jsc/bindings/webcore/streams/JSOneShotDirectSink.h new file mode 100644 index 000000000000..e6384e68aef5 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSOneShotDirectSink.h @@ -0,0 +1,69 @@ +// JSOneShotDirectSink — the one-shot direct consumer's throwaway controller +// (`consumeDirectStreamToArrayBuffer` / readableStreamToArrayBufferDirect). +// +// This path does NOT build a persistent controller or a reader, and shares no state machine +// with JSDirectStreamController — do not force it into one. It hand-rolls a +// `{start, close, end, flush, write}` object over a real `Bun.ArrayBufferSink`, calls the +// user's `pull(controller)` EXACTLY ONCE, and settles the capability promise from the pull's +// outcome. This cell IS that `controller`: it roots the ArrayBufferSink, the capability +// promise, and the source stream across the pull, and carries the `closed` flag. +// Its start/write/end/close/flush are OWN JSBoundFunctions over the shared +// boundOneShotStart / boundOneShotDirect{Write,Close,Flush} [bound-convention] targets +// (JSStreamsRuntime.h), with THIS cell as the bound context at argument(0): +// - `start` is bound to boundOneShotStart, a no-op target that returns undefined; +// - `end` and `close` are two bound cells over the ONE boundOneShotDirectClose target. +// Internal cell: no prototype, no constructor, never exposed to JS beyond `pull(controller)`. +// Non-destructible: WriteBarrier + scalar members only. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSOneShotDirectSink final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSOneShotDirectSink* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit ALL FOUR barriers: m_stream, m_arrayBufferSink, + // m_capabilityPromise, m_closeFunction. No barrier container ⇒ no cellLock needed. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The consumed DirectPending stream (already marked locked + disturbed before the pull). + JSC::WriteBarrier m_stream; + // The real Bun.ArrayBufferSink cell every write() lands in. + JSC::WriteBarrier m_arrayBufferSink; + // The capability promise consumeDirectStreamToArrayBuffer returned; end()/close() settle + // it (and the onConsumeDirectToArrayBufferPull* reactions settle it on the pull's promise). + JSC::WriteBarrier m_capabilityPromise; + // The underlying source's optional close() method, invoked by end()/close(). + JSC::WriteBarrier m_closeFunction; + // Set by end()/close(): later write()/end()/close()/flush() calls are no-ops. + bool m_closed { false }; + // true ⇒ resolve with a Uint8Array (toBytes); false ⇒ an ArrayBuffer (toArrayBuffer). + bool m_asUint8Array { false }; + +private: + JSOneShotDirectSink(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSPullIntoDescriptor.cpp b/src/jsc/bindings/webcore/streams/JSPullIntoDescriptor.cpp new file mode 100644 index 000000000000..a24deb0fc251 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSPullIntoDescriptor.cpp @@ -0,0 +1,60 @@ +#include "config.h" +#include "JSPullIntoDescriptor.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +const ClassInfo JSPullIntoDescriptor::s_info = { "PullIntoDescriptor"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSPullIntoDescriptor) }; + +JSPullIntoDescriptor::JSPullIntoDescriptor(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSPullIntoDescriptor::~JSPullIntoDescriptor() = default; + +void JSPullIntoDescriptor::destroy(JSCell* cell) +{ + static_cast(cell)->~JSPullIntoDescriptor(); +} + +void JSPullIntoDescriptor::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSPullIntoDescriptor* JSPullIntoDescriptor::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSPullIntoDescriptor(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSPullIntoDescriptor::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSPullIntoDescriptor::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForPullIntoDescriptor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForPullIntoDescriptor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForPullIntoDescriptor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForPullIntoDescriptor = std::forward(space); }); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSPullIntoDescriptor.h b/src/jsc/bindings/webcore/streams/JSPullIntoDescriptor.h new file mode 100644 index 000000000000..1d866ded20df --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSPullIntoDescriptor.h @@ -0,0 +1,69 @@ +// JSPullIntoDescriptor — the spec's pull-into descriptor as a small, destructible GC cell +// (its buffer is a RefPtr to the ArrayBuffer impl). It is a cell (not a plain struct in a Vector) because user code can mutate +// [[pendingPullIntos]] reentrantly from inside respond()/respondWithNewView()/enqueue(); +// holding a JSPullIntoDescriptor* across user JS is never a UAF — but the code must still +// RE-VALIDATE that the descriptor is still relevant afterward. +// Internal cell: no prototype, no constructor, never exposed to JS. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include +#include +#include + +namespace WebCore { + +class JSPullIntoDescriptor final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + static JSPullIntoDescriptor* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + static void destroy(JSC::JSCell*); + + DECLARE_INFO; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // "element size" (1..8) — DERIVED from m_viewConstructor, never stored separately. + size_t elementSize() const { return JSC::elementSize(m_viewConstructor); } + + // "buffer" — the ArrayBuffer IMPL, not a JSArrayBuffer wrapper cell: internal transfers + // move the contents without allocating GC cells or re-reporting extra memory; a wrapper + // only ever exists lazily if user code reads `.buffer` off a view we hand out. + RefPtr m_buffer; + // "buffer byte length" + size_t m_bufferByteLength { 0 }; + // "byte offset" + size_t m_byteOffset { 0 }; + // "byte length" + size_t m_byteLength { 0 }; + // "bytes filled" + size_t m_bytesFilled { 0 }; + // "minimum fill" + size_t m_minimumFill { 0 }; + // "view constructor" — an INTRINSIC constructor identity (a closed set), never a user + // constructor. + JSC::TypedArrayType m_viewConstructor { JSC::TypeUint8 }; + // "reader type": "default" / "byob" / "none" (None after release). + ReaderType m_readerType { ReaderType::None }; + +private: + JSPullIntoDescriptor(JSC::VM&, JSC::Structure*); + ~JSPullIntoDescriptor(); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadRequest.cpp b/src/jsc/bindings/webcore/streams/JSReadRequest.cpp new file mode 100644 index 000000000000..e487d576b07e --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadRequest.cpp @@ -0,0 +1,353 @@ +#include "config.h" +#include "JSReadRequest.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSReadableByteStreamController.h" +#include "JSReadableStream.h" +#include "JSReadableStreamAsyncIterator.h" +#include "JSReadableStreamDefaultController.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSStreamPipeToOperation.h" +#include "JSStreamTeeState.h" +#include "JSStreamsRuntime.h" +#include "WebStreamsInternals.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +// The tee state's branches always carry the controller kind their tee installed. +static JSReadableStreamDefaultController* defaultControllerOf(JSReadableStream* stream) +{ + ASSERT(stream->m_controllerKind == ControllerKind::Default); + return uncheckedDowncast(stream->m_controller.get()); +} + +static JSReadableByteStreamController* byteControllerOf(JSReadableStream* stream) +{ + ASSERT(stream->m_controllerKind == ControllerKind::Byte); + return uncheckedDowncast(stream->m_controller.get()); +} + +// [reaction-convention] deferral: runs handler(value, context) as its own microtask, +// carrying the current async context, without allocating a promise. +static void queueReactionJob(JSC::VM& vm, JSGlobalObject* globalObject, JSFunction* handler, JSValue value, JSValue context) +{ + JSValue asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0); + if (asyncContext.isEmpty()) + asyncContext = jsUndefined(); + QueuedTask task { nullptr, InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, handler, asyncContext, value, context }; + vm.queueMicrotask(WTF::move(task)); +} + +const ClassInfo JSReadRequest::s_info = { "ReadRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadRequest) }; + +JSReadRequest::JSReadRequest(VM& vm, Structure* structure, ReadRequestKind kind) + : Base(vm, structure) + , m_kind(kind) +{ +} + +void JSReadRequest::finishCreation(VM& vm, JSValue context) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + m_context.set(vm, this, context); +} + +JSReadRequest* JSReadRequest::create(VM& vm, Structure* structure, ReadRequestKind kind, JSValue context) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSReadRequest(vm, structure, kind); + cell->finishCreation(vm, context); + return cell; +} + +Structure* JSReadRequest::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSReadRequest::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadRequest.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadRequest = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadRequest.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadRequest = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadRequest); + +template +void JSReadRequest::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_context); +} + +void JSReadRequest::chunkSteps(JSGlobalObject* globalObject, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + switch (m_kind) { + case ReadRequestKind::Promise: { + auto* promise = uncheckedDowncast(m_context.get()); + auto* result = createIteratorResultObject(globalObject, chunk, false); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, promise, result)); + } + case ReadRequestKind::PipeTo: + RELEASE_AND_RETURN(scope, pipeToReadRequestChunkSteps(globalObject, uncheckedDowncast(m_context.get()), chunk)); + case ReadRequestKind::DefaultTee: + return queueReactionJob(vm, globalObject, JSStreamsRuntime::from(globalObject)->onDefaultTeeReadChunkMicrotask(), chunk, m_context.get()); + case ReadRequestKind::ByteTee: + return queueReactionJob(vm, globalObject, JSStreamsRuntime::from(globalObject)->onByteTeeReadChunkMicrotask(), chunk, m_context.get()); + case ReadRequestKind::AsyncIterator: { + auto* context = uncheckedDowncast(m_context.get()); + auto* promise = uncheckedDowncast(context->getInternalField(1)); + auto* result = createIteratorResultObject(globalObject, chunk, false); + RETURN_IF_EXCEPTION(scope, void()); + // Per spec, next()'s promise resolves from a queued microtask. + queueStreamsMicrotask(globalObject, JSStreamsRuntime::from(globalObject)->onAsyncIteratorResolveMicrotask(), result, promise); + return; + } + } + RELEASE_ASSERT_NOT_REACHED(); +} + +void JSReadRequest::closeSteps(JSGlobalObject* globalObject) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + switch (m_kind) { + case ReadRequestKind::Promise: { + auto* promise = uncheckedDowncast(m_context.get()); + auto* result = createIteratorResultObject(globalObject, jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, promise, result)); + } + case ReadRequestKind::PipeTo: + RELEASE_AND_RETURN(scope, pipeToReadRequestCloseSteps(globalObject, uncheckedDowncast(m_context.get()))); + case ReadRequestKind::DefaultTee: { + auto* teeState = uncheckedDowncast(m_context.get()); + teeState->m_reading = false; + if (!teeState->m_canceled1) { + readableStreamDefaultControllerClose(globalObject, defaultControllerOf(teeState->m_branch1.get())); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!teeState->m_canceled2) { + readableStreamDefaultControllerClose(globalObject, defaultControllerOf(teeState->m_branch2.get())); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!teeState->m_canceled1 || !teeState->m_canceled2) + resolvePromise(globalObject, teeState->m_cancelPromise.get(), jsUndefined()); + return; + } + case ReadRequestKind::ByteTee: { + auto* teeState = uncheckedDowncast(m_context.get()); + teeState->m_reading = false; + if (!teeState->m_canceled1) { + readableByteStreamControllerClose(globalObject, byteControllerOf(teeState->m_branch1.get())); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!teeState->m_canceled2) { + readableByteStreamControllerClose(globalObject, byteControllerOf(teeState->m_branch2.get())); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!byteControllerOf(teeState->m_branch1.get())->m_pendingPullIntos.isEmpty()) { + readableByteStreamControllerRespond(globalObject, byteControllerOf(teeState->m_branch1.get()), 0); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!byteControllerOf(teeState->m_branch2.get())->m_pendingPullIntos.isEmpty()) { + readableByteStreamControllerRespond(globalObject, byteControllerOf(teeState->m_branch2.get()), 0); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!teeState->m_canceled1 || !teeState->m_canceled2) + resolvePromise(globalObject, teeState->m_cancelPromise.get(), jsUndefined()); + return; + } + case ReadRequestKind::AsyncIterator: { + auto* context = uncheckedDowncast(m_context.get()); + auto* iterator = uncheckedDowncast(context->getInternalField(0)); + auto* promise = uncheckedDowncast(context->getInternalField(1)); + iterator->m_isFinished = true; + readableStreamDefaultReaderRelease(globalObject, iterator->m_reader.get()); + RETURN_IF_EXCEPTION(scope, void()); + auto* result = createIteratorResultObject(globalObject, jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, void()); + queueStreamsMicrotask(globalObject, JSStreamsRuntime::from(globalObject)->onAsyncIteratorResolveMicrotask(), result, promise); + return; + } + } + RELEASE_ASSERT_NOT_REACHED(); +} + +void JSReadRequest::errorSteps(JSGlobalObject* globalObject, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + switch (m_kind) { + case ReadRequestKind::Promise: + RELEASE_AND_RETURN(scope, rejectPromise(globalObject, uncheckedDowncast(m_context.get()), error)); + case ReadRequestKind::PipeTo: + RELEASE_AND_RETURN(scope, pipeToReadRequestErrorSteps(globalObject, uncheckedDowncast(m_context.get()), error)); + case ReadRequestKind::DefaultTee: + case ReadRequestKind::ByteTee: + uncheckedDowncast(m_context.get())->m_reading = false; + return; + case ReadRequestKind::AsyncIterator: { + auto* context = uncheckedDowncast(m_context.get()); + auto* iterator = uncheckedDowncast(context->getInternalField(0)); + auto* promise = uncheckedDowncast(context->getInternalField(1)); + iterator->m_isFinished = true; + readableStreamDefaultReaderRelease(globalObject, iterator->m_reader.get()); + RETURN_IF_EXCEPTION(scope, void()); + queueStreamsMicrotask(globalObject, JSStreamsRuntime::from(globalObject)->onAsyncIteratorRejectMicrotask(), error, promise); + return; + } + } + RELEASE_ASSERT_NOT_REACHED(); +} + +const ClassInfo JSReadIntoRequest::s_info = { "ReadIntoRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadIntoRequest) }; + +JSReadIntoRequest::JSReadIntoRequest(VM& vm, Structure* structure, ReadIntoRequestKind kind) + : Base(vm, structure) + , m_kind(kind) +{ +} + +void JSReadIntoRequest::finishCreation(VM& vm, JSValue context) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + m_context.set(vm, this, context); +} + +JSReadIntoRequest* JSReadIntoRequest::create(VM& vm, Structure* structure, ReadIntoRequestKind kind, JSValue context) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSReadIntoRequest(vm, structure, kind); + cell->finishCreation(vm, context); + return cell; +} + +Structure* JSReadIntoRequest::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSReadIntoRequest::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadIntoRequest.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadIntoRequest = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadIntoRequest.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadIntoRequest = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadIntoRequest); + +template +void JSReadIntoRequest::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_context); +} + +void JSReadIntoRequest::chunkSteps(JSGlobalObject* globalObject, JSArrayBufferView* chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + switch (m_kind) { + case ReadIntoRequestKind::Promise: { + auto* promise = uncheckedDowncast(m_context.get()); + auto* result = createIteratorResultObject(globalObject, chunk, false); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, promise, result)); + } + case ReadIntoRequestKind::ByteTee: + return queueReactionJob(vm, globalObject, JSStreamsRuntime::from(globalObject)->onByteTeeReadIntoChunkMicrotask(), chunk, m_context.get()); + } + RELEASE_ASSERT_NOT_REACHED(); +} + +void JSReadIntoRequest::closeSteps(JSGlobalObject* globalObject, JSArrayBufferView* chunkOrNull) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + switch (m_kind) { + case ReadIntoRequestKind::Promise: { + auto* promise = uncheckedDowncast(m_context.get()); + auto* result = createIteratorResultObject(globalObject, chunkOrNull ? JSValue(chunkOrNull) : jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, promise, result)); + } + case ReadIntoRequestKind::ByteTee: { + auto* context = uncheckedDowncast(m_context.get()); + auto* teeState = uncheckedDowncast(context->getInternalField(0)); + bool forBranch2 = context->getInternalField(1).asBoolean(); + teeState->m_reading = false; + auto* byobBranch = forBranch2 ? teeState->m_branch2.get() : teeState->m_branch1.get(); + auto* otherBranch = forBranch2 ? teeState->m_branch1.get() : teeState->m_branch2.get(); + bool byobCanceled = forBranch2 ? teeState->m_canceled2 : teeState->m_canceled1; + bool otherCanceled = forBranch2 ? teeState->m_canceled1 : teeState->m_canceled2; + if (!byobCanceled) { + readableByteStreamControllerClose(globalObject, byteControllerOf(byobBranch)); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!otherCanceled) { + readableByteStreamControllerClose(globalObject, byteControllerOf(otherBranch)); + RETURN_IF_EXCEPTION(scope, void()); + } + if (chunkOrNull) { + ASSERT(!chunkOrNull->byteLength()); + if (!byobCanceled) { + readableByteStreamControllerRespondWithNewView(globalObject, byteControllerOf(byobBranch), chunkOrNull); + RETURN_IF_EXCEPTION(scope, void()); + } + if (!otherCanceled && !byteControllerOf(otherBranch)->m_pendingPullIntos.isEmpty()) { + readableByteStreamControllerRespond(globalObject, byteControllerOf(otherBranch), 0); + RETURN_IF_EXCEPTION(scope, void()); + } + } + if (!byobCanceled || !otherCanceled) + resolvePromise(globalObject, teeState->m_cancelPromise.get(), jsUndefined()); + return; + } + } + RELEASE_ASSERT_NOT_REACHED(); +} + +void JSReadIntoRequest::errorSteps(JSGlobalObject* globalObject, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + switch (m_kind) { + case ReadIntoRequestKind::Promise: + RELEASE_AND_RETURN(scope, rejectPromise(globalObject, uncheckedDowncast(m_context.get()), error)); + case ReadIntoRequestKind::ByteTee: + uncheckedDowncast(uncheckedDowncast(m_context.get())->getInternalField(0))->m_reading = false; + return; + } + RELEASE_ASSERT_NOT_REACHED(); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadRequest.h b/src/jsc/bindings/webcore/streams/JSReadRequest.h new file mode 100644 index 000000000000..1c898c5299a6 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadRequest.h @@ -0,0 +1,105 @@ +// JSReadRequest / JSReadIntoRequest — the spec's read request / read-into request +// "structs with steps", each as ONE concrete, NON-polymorphic GC cell with a kind tag. +// A C++ `virtual` on any JSCell is memory corruption and is BANNED. +// Internal cells: no prototype, no constructor, never exposed to JS. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include + +namespace WebCore { + +// A read request: chunk steps / close steps / error steps. +class JSReadRequest final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSReadRequest* create(JSC::VM&, JSC::Structure*, Bun::WebStreams::ReadRequestKind, JSC::JSValue context); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_context. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + ReadRequestKind kind() const { return m_kind; } + + // Every body is `switch (m_kind)` over ALL arms — no default. Every dispatch through + // these is userJS: YES (transitive): the Promise kind resolves its promise with a USER + // chunk; the other kinds re-enter controller ops. + // "chunk steps, given chunk" + void chunkSteps(JSC::JSGlobalObject*, JSC::JSValue chunk); + // "close steps" + void closeSteps(JSC::JSGlobalObject*); + // "error steps, given e" + void errorSteps(JSC::JSGlobalObject*, JSC::JSValue error); + + // Promise kind: the JSPromise reader.read() returned. + // PipeTo: the JSStreamPipeToOperation. DefaultTee/ByteTee: the JSStreamTeeState. + // AsyncIterator: the JSReadableStreamAsyncIterator. + JSC::WriteBarrier m_context; + +private: + JSReadRequest(JSC::VM&, JSC::Structure*, Bun::WebStreams::ReadRequestKind); + void finishCreation(JSC::VM&, JSC::JSValue context); + + const ReadRequestKind m_kind; +}; + +// A read-into request. NOTE: its close steps take a chunk (or undefined). +class JSReadIntoRequest final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSReadIntoRequest* create(JSC::VM&, JSC::Structure*, Bun::WebStreams::ReadIntoRequestKind, JSC::JSValue context); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_context. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + ReadIntoRequestKind kind() const { return m_kind; } + + // userJS: YES (transitive) for every dispatch site (see JSReadRequest above). + // "chunk steps, given chunk" + void chunkSteps(JSC::JSGlobalObject*, JSC::JSArrayBufferView* chunk); + // "close steps, given chunk" — chunk may be null (the spec's `undefined`). + void closeSteps(JSC::JSGlobalObject*, JSC::JSArrayBufferView* chunkOrNull); + // "error steps, given e" + void errorSteps(JSC::JSGlobalObject*, JSC::JSValue error); + + // Promise kind: the JSPromise byobReader.read(view) returned. + // ByteTee: the JSStreamTeeState. + JSC::WriteBarrier m_context; + +private: + JSReadIntoRequest(JSC::VM&, JSC::Structure*, Bun::WebStreams::ReadIntoRequestKind); + void finishCreation(JSC::VM&, JSC::JSValue context); + + const ReadIntoRequestKind m_kind; +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadStreamIntoSinkOperation.h b/src/jsc/bindings/webcore/streams/JSReadStreamIntoSinkOperation.h new file mode 100644 index 000000000000..0c9465366977 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadStreamIntoSinkOperation.h @@ -0,0 +1,56 @@ +// JSReadStreamIntoSinkOperation — the readStreamIntoSink async pump's state cell. Driven +// entirely by [reaction-convention] reactions. +// ROOTING: the acquired reader's visited m_pipeOperation back-edge points HERE (set at +// acquire, cleared at teardown), so `Rust Strong → stream → reader → this → +// m_sink / m_result` holds across the backpressure await. +// Internal cell: no prototype, no constructor. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSReadStreamIntoSinkOperation final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSReadStreamIntoSinkOperation* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit ALL FOUR barriers: m_stream, m_reader, m_sink, m_result. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + JSC::WriteBarrier m_stream; + // the acquired default reader. The error path CLEARS this FIRST, so the final + // releaseLock is deliberately skipped there. + JSC::WriteBarrier m_reader; + // ERASED: the native JSSink controller the pump writes into. + JSC::WriteBarrier m_sink; + // the JSPromise readStreamIntoSink returned (what Rust's Signal protocol awaits). + JSC::WriteBarrier m_result; + bool m_didThrow { false }; + bool m_didClose { false }; + bool m_started { false }; + +private: + JSReadStreamIntoSinkOperation(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.cpp b/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.cpp new file mode 100644 index 000000000000..21935724bd16 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.cpp @@ -0,0 +1,1207 @@ +#include "config.h" +#include "JSReadableByteStreamController.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSPullIntoDescriptor.h" +#include "JSReadRequest.h" +#include "JSReadableStream.h" +#include "JSReadableStreamBYOBRequest.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSStreamTeeState.h" +#include "JSStreamsRuntime.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +// CloneArrayBuffer(buffer, byteOffset, byteLength, %ArrayBuffer%): null ⇒ exception pending. +static RefPtr cloneArrayBuffer(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::ArrayBuffer& buffer, size_t byteOffset, size_t byteLength) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + RefPtr cloned = JSC::ArrayBuffer::tryCreate(buffer.span().subspan(byteOffset, byteLength)); + if (!cloned) [[unlikely]] + JSC::throwRangeError(globalObject, scope, "Cannot allocate the cloned ArrayBuffer required by the readable byte stream"_s); + return cloned; +} + +// Construct(viewConstructor, « buffer, byteOffset, length »). `length` is an element count for +// typed arrays and a byte length for %DataView% (elementSize(TypeDataView) == 1). +static JSC::JSArrayBufferView* constructViewOfType(JSC::JSGlobalObject* globalObject, JSC::TypedArrayType type, RefPtr buffer, size_t byteOffset, size_t length) +{ + JSC::Structure* structure = globalObject->typedArrayStructure(type, buffer->isResizableOrGrowableShared()); + switch (type) { + case JSC::TypeInt8: + return JSC::JSInt8Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeUint8: + return JSC::JSUint8Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeUint8Clamped: + return JSC::JSUint8ClampedArray::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeInt16: + return JSC::JSInt16Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeUint16: + return JSC::JSUint16Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeInt32: + return JSC::JSInt32Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeUint32: + return JSC::JSUint32Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeFloat16: + return JSC::JSFloat16Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeFloat32: + return JSC::JSFloat32Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeFloat64: + return JSC::JSFloat64Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeBigInt64: + return JSC::JSBigInt64Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeBigUint64: + return JSC::JSBigUint64Array::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::TypeDataView: + return JSC::JSDataView::create(globalObject, structure, WTF::move(buffer), byteOffset, length); + case JSC::NotTypedArray: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +// WebIDL "invoke a callback function" with a Promise return type: an abrupt completion is +// converted into a rejected promise (a completion-record conversion), never a synchronous throw. +static JSC::JSPromise* invokePromiseReturningMethod(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* method, JSC::JSValue thisValue, const JSC::MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSValue result; + JSC::JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = JSC::getCallData(method); + ASSERT(callData.type != JSC::CallData::Type::None); + result = JSC::call(globalObject, method, callData, thisValue, args); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (result.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseResolvedWith(globalObject, result)); +} + +// The [[pullAlgorithm]] dispatch. The reachable kind set on a byte controller is exactly +// {JavaScript, Nothing, ByteTeeBranch}; the switch is total over SourceKind. +static JSC::JSPromise* performByteControllerPullAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSReadableByteStreamController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_algorithms.kind) { + case SourceKind::JavaScript: { + JSC::JSObject* pullMethod = controller->m_algorithms.method1.get(); + if (!pullMethod) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + JSC::MarkedArgumentBuffer args; + args.append(controller); + if (args.hasOverflowed()) [[unlikely]] { + JSC::throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + StreamAsyncContextScope asyncContextScope(globalObject, controller->m_stream.get()); + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, pullMethod, controller->m_algorithms.underlyingObject.get(), args)); + } + case SourceKind::Nothing: + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + case SourceKind::ByteTeeBranch: + RELEASE_AND_RETURN(scope, byteTeePullAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()), controller->m_algorithms.teeBranchIndex)); + case SourceKind::Transform: + case SourceKind::TeeBranch: + case SourceKind::FromIterable: + case SourceKind::CrossRealm: + case SourceKind::Native: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +// The [[cancelAlgorithm]] dispatch. Same reachable kind set as the pull dispatch. +static JSC::JSPromise* performByteControllerCancelAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSC::JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_algorithms.kind) { + case SourceKind::JavaScript: { + JSC::JSObject* cancelMethod = controller->m_algorithms.method2.get(); + if (!cancelMethod) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + JSC::MarkedArgumentBuffer args; + args.append(reason); + if (args.hasOverflowed()) [[unlikely]] { + JSC::throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + StreamAsyncContextScope asyncContextScope(globalObject, controller->m_stream.get()); + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, cancelMethod, controller->m_algorithms.underlyingObject.get(), args)); + } + case SourceKind::Nothing: + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + case SourceKind::ByteTeeBranch: + RELEASE_AND_RETURN(scope, byteTeeCancelAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()), controller->m_algorithms.teeBranchIndex, reason)); + case SourceKind::Transform: + case SourceKind::TeeBranch: + case SourceKind::FromIterable: + case SourceKind::CrossRealm: + case SourceKind::Native: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsReadableByteStreamControllerConstructorGetter); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableByteStreamControllerPrototypeGetter_byobRequest); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableByteStreamControllerPrototypeGetter_desiredSize); +static JSC_DECLARE_HOST_FUNCTION(jsReadableByteStreamControllerPrototypeFunction_close); +static JSC_DECLARE_HOST_FUNCTION(jsReadableByteStreamControllerPrototypeFunction_enqueue); +static JSC_DECLARE_HOST_FUNCTION(jsReadableByteStreamControllerPrototypeFunction_error); + +class JSReadableByteStreamControllerPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSReadableByteStreamControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSReadableByteStreamControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableByteStreamControllerPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableByteStreamControllerPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSReadableByteStreamControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableByteStreamControllerPrototype, JSReadableByteStreamControllerPrototype::Base); + +static const HashTableValue JSReadableByteStreamControllerPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableByteStreamControllerConstructorGetter, 0 } }, + { "byobRequest"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor, NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableByteStreamControllerPrototypeGetter_byobRequest, 0 } }, + { "desiredSize"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor, NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableByteStreamControllerPrototypeGetter_desiredSize, 0 } }, + { "close"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableByteStreamControllerPrototypeFunction_close, 0 } }, + { "enqueue"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableByteStreamControllerPrototypeFunction_enqueue, 1 } }, + { "error"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableByteStreamControllerPrototypeFunction_error, 0 } }, +}; + +const ClassInfo JSReadableByteStreamControllerPrototype::s_info = { "ReadableByteStreamController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableByteStreamControllerPrototype) }; + +void JSReadableByteStreamControllerPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSReadableByteStreamController::info(), JSReadableByteStreamControllerPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +template<> const ClassInfo JSReadableByteStreamControllerConstructor::s_info = { "ReadableByteStreamController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableByteStreamControllerConstructor) }; + +template<> JSValue JSReadableByteStreamControllerConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + UNUSED_PARAM(vm); + return globalObject.functionPrototype(); +} + +template<> void JSReadableByteStreamControllerConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "ReadableByteStreamController"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSReadableByteStreamController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +const ClassInfo JSReadableByteStreamController::s_info = { "ReadableByteStreamController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableByteStreamController) }; + +JSReadableByteStreamController::JSReadableByteStreamController(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSReadableByteStreamController::~JSReadableByteStreamController() = default; + +void JSReadableByteStreamController::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSReadableByteStreamController* JSReadableByteStreamController::create(VM& vm, Structure* structure) +{ + JSReadableByteStreamController* controller = new (NotNull, JSC::allocateCell(vm)) JSReadableByteStreamController(vm, structure); + controller->finishCreation(vm); + return controller; +} + +void JSReadableByteStreamController::destroy(JSCell* cell) +{ + static_cast(cell)->JSReadableByteStreamController::~JSReadableByteStreamController(); +} + +Structure* JSReadableByteStreamController::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSReadableByteStreamController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSReadableByteStreamControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSReadableByteStreamControllerPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSReadableByteStreamController::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSReadableByteStreamController::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSReadableByteStreamController::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableByteStreamController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableByteStreamController = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableByteStreamController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableByteStreamController = std::forward(space); }); +} + +template +void JSReadableByteStreamController::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_byobRequest); + visitor.append(thisObject->m_algorithms.underlyingObject); + visitor.append(thisObject->m_algorithms.method1); + visitor.append(thisObject->m_algorithms.method2); + visitor.append(thisObject->m_algorithms.algorithmContext); + // ONE non-recursive cellLock scope covers BOTH barrier containers (StreamQueue.h). + WTF::Locker locker { thisObject->cellLock() }; + thisObject->m_queue.visit(locker, visitor); + for (auto& descriptor : thisObject->m_pendingPullIntos) + visitor.append(descriptor); +} + +DEFINE_VISIT_CHILDREN(JSReadableByteStreamController); + +// [[CancelSteps]](reason) +JSPromise* JSReadableByteStreamController::cancelSteps(JSGlobalObject* globalObject, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + readableByteStreamControllerClearPendingPullIntos(this); + { + WTF::Locker locker { cellLock() }; + m_queue.resetQueue(locker); + } + JSPromise* result = performByteControllerCancelAlgorithm(vm, globalObject, this, reason); + RETURN_IF_EXCEPTION(scope, nullptr); + readableByteStreamControllerClearAlgorithms(this); + return result; +} + +// [[PullSteps]](readRequest) +void JSReadableByteStreamController::pullSteps(JSGlobalObject* globalObject, JSReadRequest* readRequest) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSReadableStream* stream = m_stream.get(); + ASSERT(readableStreamHasDefaultReader(stream)); + if (m_queue.totalSize() > 0) { + ASSERT(!readableStreamGetNumReadRequests(stream)); + RELEASE_AND_RETURN(scope, readableByteStreamControllerFillReadRequestFromQueue(globalObject, this, readRequest)); + } + if (m_autoAllocateChunkSize) { + // "Let buffer be Construct(%ArrayBuffer%, « autoAllocateChunkSize »)" is interpreted + // as a completion record: an allocation failure goes to the error steps. The impl is + // allocated directly (no JSArrayBuffer wrapper cell); user-visible views over it wrap + // it lazily. + RefPtr buffer = JSC::ArrayBuffer::tryCreate(static_cast(m_autoAllocateChunkSize), 1); + if (!buffer) [[unlikely]] { + auto* error = JSC::createOutOfMemoryError(globalObject); + RELEASE_AND_RETURN(scope, readRequest->errorSteps(globalObject, error)); + } + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSPullIntoDescriptor* pullIntoDescriptor = JSPullIntoDescriptor::create(vm, JSStreamsRuntime::from(globalObject)->pullIntoDescriptorStructure(zigGlobalObject)); + RETURN_IF_EXCEPTION(scope, void()); + pullIntoDescriptor->m_buffer = WTF::move(buffer); + pullIntoDescriptor->m_bufferByteLength = static_cast(m_autoAllocateChunkSize); + pullIntoDescriptor->m_byteOffset = 0; + pullIntoDescriptor->m_byteLength = static_cast(m_autoAllocateChunkSize); + pullIntoDescriptor->m_bytesFilled = 0; + pullIntoDescriptor->m_minimumFill = 1; + pullIntoDescriptor->m_viewConstructor = JSC::TypeUint8; + pullIntoDescriptor->m_readerType = ReaderType::Default; + { + WTF::Locker locker { cellLock() }; + m_pendingPullIntos.append(WriteBarrier(vm, this, pullIntoDescriptor)); + } + } + readableStreamAddReadRequest(vm, stream, readRequest); + RELEASE_AND_RETURN(scope, readableByteStreamControllerCallPullIfNeeded(globalObject, this)); +} + +// [[ReleaseSteps]]() +void JSReadableByteStreamController::releaseSteps() +{ + if (m_pendingPullIntos.isEmpty()) + return; + JSPullIntoDescriptor* firstPendingPullInto = m_pendingPullIntos.first().get(); + firstPendingPullInto->m_readerType = ReaderType::None; + WTF::Locker locker { cellLock() }; + while (m_pendingPullIntos.size() > 1) + m_pendingPullIntos.removeLast(); +} + +// The shared start/pull reaction handlers ([reaction-convention]; context at argument(1)). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSByteControllerStartFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + controller->m_started = true; + ASSERT(!controller->m_pulling); + ASSERT(!controller->m_pullAgain); + readableByteStreamControllerCallPullIfNeeded(globalObject, controller); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSByteControllerStartRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + readableByteStreamControllerError(globalObject, controller, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSByteControllerPullFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + controller->m_pulling = false; + if (controller->m_pullAgain) { + controller->m_pullAgain = false; + readableByteStreamControllerCallPullIfNeeded(globalObject, controller); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSByteControllerPullRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + readableByteStreamControllerError(globalObject, controller, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// Prototype accessors & methods. + +JSC_DEFINE_CUSTOM_GETTER(jsReadableByteStreamControllerConstructorGetter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(globalObject, scope); + return JSValue::encode(JSReadableByteStreamController::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableByteStreamControllerPrototypeGetter_byobRequest, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableByteStreamController"_s); + JSReadableStreamBYOBRequest* byobRequest = readableByteStreamControllerGetBYOBRequest(globalObject, thisObject); + RETURN_IF_EXCEPTION(scope, {}); + if (!byobRequest) + return JSValue::encode(jsNull()); + return JSValue::encode(byobRequest); +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableByteStreamControllerPrototypeGetter_desiredSize, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableByteStreamController"_s); + std::optional desiredSize = readableByteStreamControllerGetDesiredSize(thisObject); + if (!desiredSize) + return JSValue::encode(jsNull()); + return JSValue::encode(jsNumber(*desiredSize)); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableByteStreamControllerPrototypeFunction_close, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableByteStreamController"_s); + if (thisObject->m_closeRequested) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream is already closed"_s); + if (!thisObject->m_stream || thisObject->m_stream->m_state != ReadableStreamState::Readable) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream is already closed"_s); + readableByteStreamControllerClose(globalObject, thisObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableByteStreamControllerPrototypeFunction_enqueue, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableByteStreamController"_s); + if (callFrame->argumentCount() < 1) [[unlikely]] + return throwVMError(globalObject, scope, createNotEnoughArgumentsError(globalObject)); + auto* chunk = dynamicDowncast(callFrame->uncheckedArgument(0)); + if (!chunk) [[unlikely]] + return Bun::ERR::INVALID_ARG_INSTANCE(scope, globalObject, "buffer"_s, "Buffer, TypedArray, or DataView"_s, callFrame->uncheckedArgument(0)); + JSC::ArrayBuffer* viewedBuffer = chunk->possiblySharedBuffer(); + if (viewedBuffer && viewedBuffer->isShared()) [[unlikely]] + return throwVMTypeError(globalObject, scope, "ReadableByteStreamController.enqueue does not accept a view over a SharedArrayBuffer"_s); + if (!chunk->byteLength()) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: chunk ArrayBuffer is zero-length or detached"_s); + if (!viewedBuffer || !viewedBuffer->byteLength()) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: chunk ArrayBuffer is zero-length or detached"_s); + if (thisObject->m_closeRequested) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream is already closed"_s); + if (!thisObject->m_stream || thisObject->m_stream->m_state != ReadableStreamState::Readable) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream is already closed"_s); + readableByteStreamControllerEnqueue(globalObject, thisObject, chunk); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableByteStreamControllerPrototypeFunction_error, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableByteStreamController"_s); + readableByteStreamControllerError(globalObject, thisObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using namespace WebCore; + +void readableByteStreamControllerCallPullIfNeeded(JSGlobalObject* globalObject, JSReadableByteStreamController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!readableByteStreamControllerShouldCallPull(controller)) + return; + if (controller->m_pulling) { + controller->m_pullAgain = true; + return; + } + ASSERT(!controller->m_pullAgain); + controller->m_pulling = true; + JSPromise* pullPromise = performByteControllerPullAlgorithm(vm, globalObject, controller); + RETURN_IF_EXCEPTION(scope, void()); + auto* runtime = JSStreamsRuntime::from(globalObject); + pullPromise->performPromiseThenWithContext(vm, globalObject, runtime->onRSByteControllerPullFulfilled(), runtime->onRSByteControllerPullRejected(), jsUndefined(), controller); +} + +bool readableByteStreamControllerShouldCallPull(JSReadableByteStreamController* controller) +{ + JSReadableStream* stream = controller->m_stream.get(); + if (stream->m_state != ReadableStreamState::Readable) + return false; + if (controller->m_closeRequested) + return false; + if (!controller->m_started) + return false; + if (readableStreamHasDefaultReader(stream) && readableStreamGetNumReadRequests(stream) > 0) + return true; + if (readableStreamHasBYOBReader(stream) && readableStreamGetNumReadIntoRequests(stream) > 0) + return true; + std::optional desiredSize = readableByteStreamControllerGetDesiredSize(controller); + ASSERT(desiredSize); + return *desiredSize > 0; +} + +void readableByteStreamControllerClearAlgorithms(JSReadableByteStreamController* controller) +{ + controller->m_algorithms.kind = SourceKind::Nothing; + controller->m_algorithms.underlyingObject.clear(); + controller->m_algorithms.method1.clear(); + controller->m_algorithms.method2.clear(); + controller->m_algorithms.algorithmContext.clear(); +} + +void readableByteStreamControllerClearPendingPullIntos(JSReadableByteStreamController* controller) +{ + readableByteStreamControllerInvalidateBYOBRequest(controller); + WTF::Locker locker { controller->cellLock() }; + controller->m_pendingPullIntos.clear(); +} + +void readableByteStreamControllerClose(JSGlobalObject* globalObject, JSReadableByteStreamController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSReadableStream* stream = controller->m_stream.get(); + if (controller->m_closeRequested || stream->m_state != ReadableStreamState::Readable) + return; + if (controller->m_queue.totalSize() > 0) { + controller->m_closeRequested = true; + return; + } + if (!controller->m_pendingPullIntos.isEmpty()) { + JSPullIntoDescriptor* firstPendingPullInto = controller->m_pendingPullIntos.first().get(); + if (firstPendingPullInto->m_bytesFilled % firstPendingPullInto->elementSize()) { + JSObject* error = createTypeError(globalObject, "Cannot close a ReadableByteStreamController while a BYOB read request is partially filled"_s); + readableByteStreamControllerError(globalObject, controller, error); + RETURN_IF_EXCEPTION(scope, void()); + throwException(globalObject, scope, error); + return; + } + } + readableByteStreamControllerClearAlgorithms(controller); + RELEASE_AND_RETURN(scope, readableStreamClose(globalObject, stream)); +} + +void readableByteStreamControllerCommitPullIntoDescriptor(JSGlobalObject* globalObject, JSReadableStream* stream, JSPullIntoDescriptor* pullIntoDescriptor) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_state != ReadableStreamState::Errored); + ASSERT(pullIntoDescriptor->m_readerType != ReaderType::None); + bool done = false; + if (stream->m_state == ReadableStreamState::Closed) { + ASSERT(!(pullIntoDescriptor->m_bytesFilled % pullIntoDescriptor->elementSize())); + done = true; + } + JSArrayBufferView* filledView = readableByteStreamControllerConvertPullIntoDescriptor(globalObject, pullIntoDescriptor); + RETURN_IF_EXCEPTION(scope, void()); + if (pullIntoDescriptor->m_readerType == ReaderType::Default) + RELEASE_AND_RETURN(scope, readableStreamFulfillReadRequest(globalObject, stream, filledView, done)); + ASSERT(pullIntoDescriptor->m_readerType == ReaderType::Byob); + RELEASE_AND_RETURN(scope, readableStreamFulfillReadIntoRequest(globalObject, stream, filledView, done)); +} + +JSArrayBufferView* readableByteStreamControllerConvertPullIntoDescriptor(JSGlobalObject* globalObject, JSPullIntoDescriptor* pullIntoDescriptor) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + size_t bytesFilled = pullIntoDescriptor->m_bytesFilled; + size_t elementSize = pullIntoDescriptor->elementSize(); + ASSERT(bytesFilled <= pullIntoDescriptor->m_byteLength); + ASSERT(!(bytesFilled % elementSize)); + RefPtr buffer = transferArrayBufferImpl(globalObject, *pullIntoDescriptor->m_buffer); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, constructViewOfType(globalObject, pullIntoDescriptor->m_viewConstructor, WTF::move(buffer), pullIntoDescriptor->m_byteOffset, bytesFilled / elementSize)); +} + +void readableByteStreamControllerEnqueue(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSArrayBufferView* chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSReadableStream* stream = controller->m_stream.get(); + if (controller->m_closeRequested || stream->m_state != ReadableStreamState::Readable) + return; + RefPtr buffer = chunk->possiblySharedBuffer(); + size_t byteOffset = chunk->byteOffset(); + size_t byteLength = chunk->byteLength(); + if (!buffer || buffer->isDetached()) { + Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: chunk ArrayBuffer is zero-length or detached"_s); + return; + } + RefPtr transferredBuffer = transferArrayBufferImpl(globalObject, *buffer); + RETURN_IF_EXCEPTION(scope, void()); + if (!controller->m_pendingPullIntos.isEmpty()) { + JSPullIntoDescriptor* firstPendingPullInto = controller->m_pendingPullIntos.first().get(); + if (firstPendingPullInto->m_buffer->isDetached()) { + throwTypeError(globalObject, scope, "Cannot enqueue after the pending BYOB request's buffer has been detached"_s); + return; + } + readableByteStreamControllerInvalidateBYOBRequest(controller); + RefPtr transferredHeadBuffer = transferArrayBufferImpl(globalObject, *firstPendingPullInto->m_buffer); + RETURN_IF_EXCEPTION(scope, void()); + firstPendingPullInto->m_buffer = WTF::move(transferredHeadBuffer); + if (firstPendingPullInto->m_readerType == ReaderType::None) { + readableByteStreamControllerEnqueueDetachedPullIntoToQueue(globalObject, controller, firstPendingPullInto); + RETURN_IF_EXCEPTION(scope, void()); + } + } + if (readableStreamHasDefaultReader(stream)) { + readableByteStreamControllerProcessReadRequestsUsingQueue(globalObject, controller); + RETURN_IF_EXCEPTION(scope, void()); + if (!readableStreamGetNumReadRequests(stream)) { + ASSERT(controller->m_pendingPullIntos.isEmpty()); + readableByteStreamControllerEnqueueChunkToQueue(controller, WTF::move(transferredBuffer), byteOffset, byteLength); + } else { + ASSERT(controller->m_queue.isEmpty()); + if (!controller->m_pendingPullIntos.isEmpty()) { + ASSERT(controller->m_pendingPullIntos.first()->m_readerType == ReaderType::Default); + readableByteStreamControllerShiftPendingPullInto(controller); + } + JSArrayBufferView* transferredView = constructViewOfType(globalObject, JSC::TypeUint8, WTF::move(transferredBuffer), byteOffset, byteLength); + RETURN_IF_EXCEPTION(scope, void()); + readableStreamFulfillReadRequest(globalObject, stream, transferredView, false); + RETURN_IF_EXCEPTION(scope, void()); + } + } else if (readableStreamHasBYOBReader(stream)) { + readableByteStreamControllerEnqueueChunkToQueue(controller, WTF::move(transferredBuffer), byteOffset, byteLength); + MarkedArgumentBuffer filledPullIntos; + readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller, filledPullIntos); + if (filledPullIntos.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return; + } + for (size_t i = 0; i < filledPullIntos.size(); ++i) { + readableByteStreamControllerCommitPullIntoDescriptor(globalObject, stream, uncheckedDowncast(filledPullIntos.at(i))); + RETURN_IF_EXCEPTION(scope, void()); + } + } else { + ASSERT(!isReadableStreamLocked(stream)); + readableByteStreamControllerEnqueueChunkToQueue(controller, WTF::move(transferredBuffer), byteOffset, byteLength); + } + RELEASE_AND_RETURN(scope, readableByteStreamControllerCallPullIfNeeded(globalObject, controller)); +} + +void readableByteStreamControllerEnqueueChunkToQueue(JSReadableByteStreamController* controller, RefPtr&& buffer, size_t byteOffset, size_t byteLength) +{ + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.append(locker, ByteQueueEntry { WTF::move(buffer), byteOffset, byteLength }); + } + controller->m_queue.adjustTotalSize(static_cast(byteLength)); +} + +void readableByteStreamControllerEnqueueClonedChunkToQueue(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSC::ArrayBuffer& buffer, size_t byteOffset, size_t byteLength) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + RefPtr cloneResult; + { + // CloneArrayBuffer is interpreted as a completion record: an abrupt completion errors + // the controller and is then rethrown. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + cloneResult = cloneArrayBuffer(vm, globalObject, buffer, byteOffset, byteLength); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) [[unlikely]] + return; + readableByteStreamControllerError(globalObject, controller, thrown); + RETURN_IF_EXCEPTION(scope, void()); + throwException(globalObject, scope, thrown); + return; + } + } + readableByteStreamControllerEnqueueChunkToQueue(controller, WTF::move(cloneResult), 0, byteLength); +} + +void readableByteStreamControllerEnqueueDetachedPullIntoToQueue(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSPullIntoDescriptor* pullIntoDescriptor) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(pullIntoDescriptor->m_readerType == ReaderType::None); + if (pullIntoDescriptor->m_bytesFilled > 0) { + readableByteStreamControllerEnqueueClonedChunkToQueue(globalObject, controller, *pullIntoDescriptor->m_buffer, pullIntoDescriptor->m_byteOffset, pullIntoDescriptor->m_bytesFilled); + RETURN_IF_EXCEPTION(scope, void()); + } + readableByteStreamControllerShiftPendingPullInto(controller); +} + +void readableByteStreamControllerError(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSReadableStream* stream = controller->m_stream.get(); + if (stream->m_state != ReadableStreamState::Readable) + return; + readableByteStreamControllerClearPendingPullIntos(controller); + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.resetQueue(locker); + } + readableByteStreamControllerClearAlgorithms(controller); + RELEASE_AND_RETURN(scope, readableStreamError(globalObject, stream, error)); +} + +void readableByteStreamControllerFillHeadPullIntoDescriptor(JSReadableByteStreamController* controller, size_t size, JSPullIntoDescriptor* pullIntoDescriptor) +{ + ASSERT(controller->m_pendingPullIntos.isEmpty() || controller->m_pendingPullIntos.first().get() == pullIntoDescriptor); + ASSERT(!controller->m_byobRequest); + UNUSED_PARAM(controller); + pullIntoDescriptor->m_bytesFilled += size; +} + +bool readableByteStreamControllerFillPullIntoDescriptorFromQueue(JSReadableByteStreamController* controller, JSPullIntoDescriptor* pullIntoDescriptor) +{ + size_t elementSize = pullIntoDescriptor->elementSize(); + size_t maxBytesToCopy = std::min(static_cast(controller->m_queue.totalSize()), pullIntoDescriptor->m_byteLength - pullIntoDescriptor->m_bytesFilled); + size_t maxBytesFilled = pullIntoDescriptor->m_bytesFilled + maxBytesToCopy; + size_t totalBytesToCopyRemaining = maxBytesToCopy; + bool ready = false; + ASSERT(!pullIntoDescriptor->m_buffer->isDetached()); + ASSERT(pullIntoDescriptor->m_bytesFilled < pullIntoDescriptor->m_minimumFill); + size_t remainderBytes = maxBytesFilled % elementSize; + size_t maxAlignedBytes = maxBytesFilled - remainderBytes; + if (maxAlignedBytes >= pullIntoDescriptor->m_minimumFill) { + totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor->m_bytesFilled; + ready = true; + } + auto& queue = controller->m_queue; + while (totalBytesToCopyRemaining > 0) { + ByteQueueEntry& headOfQueue = queue.first(); + size_t bytesToCopy = std::min(totalBytesToCopyRemaining, headOfQueue.byteLength); + size_t destStart = pullIntoDescriptor->m_byteOffset + pullIntoDescriptor->m_bytesFilled; + JSC::ArrayBuffer* descriptorBuffer = pullIntoDescriptor->m_buffer.get(); + JSC::ArrayBuffer* queueBuffer = headOfQueue.buffer.get(); + size_t queueByteOffset = headOfQueue.byteOffset; + RELEASE_ASSERT(canCopyDataBlockBytes(*descriptorBuffer, destStart, *queueBuffer, queueByteOffset, bytesToCopy)); + memcpy(static_cast(descriptorBuffer->data()) + destStart, static_cast(queueBuffer->data()) + queueByteOffset, bytesToCopy); + bool consumedHead = headOfQueue.byteLength == bytesToCopy; + if (consumedHead) { + WTF::Locker locker { controller->cellLock() }; + queue.removeFirst(locker); + } else { + headOfQueue.byteOffset += bytesToCopy; + headOfQueue.byteLength -= bytesToCopy; + } + queue.adjustTotalSize(-static_cast(bytesToCopy)); + readableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor); + totalBytesToCopyRemaining -= bytesToCopy; + } + if (!ready) { + ASSERT(!controller->m_queue.totalSize()); + ASSERT(pullIntoDescriptor->m_bytesFilled > 0); + ASSERT(pullIntoDescriptor->m_bytesFilled < pullIntoDescriptor->m_minimumFill); + } + return ready; +} + +void readableByteStreamControllerFillReadRequestFromQueue(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSReadRequest* readRequest) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(controller->m_queue.totalSize() > 0); + RefPtr buffer; + size_t byteOffset; + size_t byteLength; + { + WTF::Locker locker { controller->cellLock() }; + ByteQueueEntry& entry = controller->m_queue.first(); + buffer = WTF::move(entry.buffer); + byteOffset = entry.byteOffset; + byteLength = entry.byteLength; + controller->m_queue.removeFirst(locker); + } + controller->m_queue.adjustTotalSize(-static_cast(byteLength)); + readableByteStreamControllerHandleQueueDrain(globalObject, controller); + RETURN_IF_EXCEPTION(scope, void()); + JSArrayBufferView* view = constructViewOfType(globalObject, JSC::TypeUint8, WTF::move(buffer), byteOffset, byteLength); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readRequest->chunkSteps(globalObject, view)); +} + +JSReadableStreamBYOBRequest* readableByteStreamControllerGetBYOBRequest(JSGlobalObject* globalObject, JSReadableByteStreamController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!controller->m_byobRequest && !controller->m_pendingPullIntos.isEmpty()) { + JSPullIntoDescriptor* firstDescriptor = controller->m_pendingPullIntos.first().get(); + JSArrayBufferView* view = constructViewOfType(globalObject, JSC::TypeUint8, firstDescriptor->m_buffer, firstDescriptor->m_byteOffset + firstDescriptor->m_bytesFilled, firstDescriptor->m_byteLength - firstDescriptor->m_bytesFilled); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSReadableStreamBYOBRequest* byobRequest = JSReadableStreamBYOBRequest::create(vm, getDOMStructure(vm, *zigGlobalObject)); + byobRequest->m_controller.set(vm, byobRequest, controller); + byobRequest->m_view.set(vm, byobRequest, view); + controller->m_byobRequest.set(vm, controller, byobRequest); + } + return controller->m_byobRequest.get(); +} + +std::optional readableByteStreamControllerGetDesiredSize(JSReadableByteStreamController* controller) +{ + switch (controller->m_stream->m_state) { + case ReadableStreamState::Errored: + return std::nullopt; + case ReadableStreamState::Closed: + return 0; + case ReadableStreamState::Readable: + break; + } + return controller->m_strategyHWM - controller->m_queue.totalSize(); +} + +void readableByteStreamControllerHandleQueueDrain(JSGlobalObject* globalObject, JSReadableByteStreamController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(controller->m_stream->m_state == ReadableStreamState::Readable); + if (!controller->m_queue.totalSize() && controller->m_closeRequested) { + readableByteStreamControllerClearAlgorithms(controller); + RELEASE_AND_RETURN(scope, readableStreamClose(globalObject, controller->m_stream.get())); + } + RELEASE_AND_RETURN(scope, readableByteStreamControllerCallPullIfNeeded(globalObject, controller)); +} + +void readableByteStreamControllerInvalidateBYOBRequest(JSReadableByteStreamController* controller) +{ + JSReadableStreamBYOBRequest* byobRequest = controller->m_byobRequest.get(); + if (!byobRequest) + return; + byobRequest->m_controller.clear(); + byobRequest->m_view.clear(); + controller->m_byobRequest.clear(); +} + +void readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(JSReadableByteStreamController* controller, MarkedArgumentBuffer& filledPullIntos) +{ + ASSERT(!controller->m_closeRequested); + while (!controller->m_pendingPullIntos.isEmpty()) { + if (!controller->m_queue.totalSize()) + break; + JSPullIntoDescriptor* pullIntoDescriptor = controller->m_pendingPullIntos.first().get(); + if (readableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) { + readableByteStreamControllerShiftPendingPullInto(controller); + filledPullIntos.append(pullIntoDescriptor); + } + } +} + +void readableByteStreamControllerProcessReadRequestsUsingQueue(JSGlobalObject* globalObject, JSReadableByteStreamController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = uncheckedDowncast(controller->m_stream->m_reader.get()); + ASSERT(reader); + while (!reader->m_readRequests.isEmpty()) { + if (!controller->m_queue.totalSize()) + return; + JSReadRequest* readRequest = nullptr; + { + WTF::Locker locker { reader->cellLock() }; + readRequest = reader->m_readRequests.takeFirst().get(); + } + readableByteStreamControllerFillReadRequestFromQueue(globalObject, controller, readRequest); + RETURN_IF_EXCEPTION(scope, void()); + } +} + +void readableByteStreamControllerPullInto(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSArrayBufferView* view, uint64_t min, JSReadIntoRequest* readIntoRequest) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSReadableStream* stream = controller->m_stream.get(); + TypedArrayType ctor = typedArrayType(view->type()); + size_t elementSize = JSC::elementSize(ctor); + size_t minimumFill = static_cast(min) * elementSize; + ASSERT(minimumFill <= view->byteLength()); + ASSERT(!(minimumFill % elementSize)); + size_t byteOffset = view->byteOffset(); + size_t byteLength = view->byteLength(); + RefPtr viewedBuffer = view->possiblySharedBuffer(); + RefPtr buffer; + JSValue transferAbruptCompletion; + { + // "If bufferResult is an abrupt completion", route it to the read-into request's error steps. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + buffer = transferArrayBufferImpl(globalObject, *viewedBuffer); + if (catchScope.exception()) [[unlikely]] { + transferAbruptCompletion = takeAbruptCompletion(globalObject, catchScope); + if (transferAbruptCompletion.isEmpty()) [[unlikely]] + return; + } + } + if (!transferAbruptCompletion.isEmpty()) [[unlikely]] + RELEASE_AND_RETURN(scope, readIntoRequest->errorSteps(globalObject, transferAbruptCompletion)); + auto* zigGlobalObject = defaultGlobalObject(globalObject); + JSPullIntoDescriptor* pullIntoDescriptor = JSPullIntoDescriptor::create(vm, JSStreamsRuntime::from(globalObject)->pullIntoDescriptorStructure(zigGlobalObject)); + pullIntoDescriptor->m_bufferByteLength = buffer->byteLength(); + pullIntoDescriptor->m_buffer = WTF::move(buffer); + pullIntoDescriptor->m_byteOffset = byteOffset; + pullIntoDescriptor->m_byteLength = byteLength; + pullIntoDescriptor->m_bytesFilled = 0; + pullIntoDescriptor->m_minimumFill = minimumFill; + pullIntoDescriptor->m_viewConstructor = ctor; + pullIntoDescriptor->m_readerType = ReaderType::Byob; + if (!controller->m_pendingPullIntos.isEmpty()) { + { + WTF::Locker locker { controller->cellLock() }; + controller->m_pendingPullIntos.append(WriteBarrier(vm, controller, pullIntoDescriptor)); + } + readableStreamAddReadIntoRequest(vm, stream, readIntoRequest); + return; + } + if (stream->m_state == ReadableStreamState::Closed) { + JSArrayBufferView* emptyView = constructViewOfType(globalObject, ctor, pullIntoDescriptor->m_buffer, pullIntoDescriptor->m_byteOffset, 0); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readIntoRequest->closeSteps(globalObject, emptyView)); + } + if (controller->m_queue.totalSize() > 0) { + if (readableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor)) { + JSArrayBufferView* filledView = readableByteStreamControllerConvertPullIntoDescriptor(globalObject, pullIntoDescriptor); + RETURN_IF_EXCEPTION(scope, void()); + readableByteStreamControllerHandleQueueDrain(globalObject, controller); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readIntoRequest->chunkSteps(globalObject, filledView)); + } + if (controller->m_closeRequested) { + JSObject* error = createTypeError(globalObject, "Cannot read into a view after close has been requested on the ReadableByteStreamController"_s); + readableByteStreamControllerError(globalObject, controller, error); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readIntoRequest->errorSteps(globalObject, error)); + } + } + { + WTF::Locker locker { controller->cellLock() }; + controller->m_pendingPullIntos.append(WriteBarrier(vm, controller, pullIntoDescriptor)); + } + readableStreamAddReadIntoRequest(vm, stream, readIntoRequest); + RELEASE_AND_RETURN(scope, readableByteStreamControllerCallPullIfNeeded(globalObject, controller)); +} + +void readableByteStreamControllerRespond(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, uint64_t bytesWritten) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(!controller->m_pendingPullIntos.isEmpty()); + JSPullIntoDescriptor* firstDescriptor = controller->m_pendingPullIntos.first().get(); + ReadableStreamState state = controller->m_stream->m_state; + if (state == ReadableStreamState::Closed) { + if (bytesWritten) { + throwTypeError(globalObject, scope, "A closed byte stream's BYOB request can only be responded to with 0 bytes written"_s); + return; + } + } else { + ASSERT(state == ReadableStreamState::Readable); + if (!bytesWritten) { + throwTypeError(globalObject, scope, "A readable byte stream's BYOB request cannot be responded to with 0 bytes written"_s); + return; + } + if (static_cast(firstDescriptor->m_bytesFilled) + bytesWritten > static_cast(firstDescriptor->m_byteLength)) { + throwRangeError(globalObject, scope, "The number of bytes written exceeds the remaining length of the BYOB request's view"_s); + return; + } + } + RefPtr transferredBuffer = transferArrayBufferImpl(globalObject, *firstDescriptor->m_buffer); + RETURN_IF_EXCEPTION(scope, void()); + firstDescriptor->m_buffer = WTF::move(transferredBuffer); + RELEASE_AND_RETURN(scope, readableByteStreamControllerRespondInternal(globalObject, controller, bytesWritten)); +} + +void readableByteStreamControllerRespondInClosedState(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSPullIntoDescriptor* firstDescriptor) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(!(firstDescriptor->m_bytesFilled % firstDescriptor->elementSize())); + if (firstDescriptor->m_readerType == ReaderType::None) + readableByteStreamControllerShiftPendingPullInto(controller); + JSReadableStream* stream = controller->m_stream.get(); + if (readableStreamHasBYOBReader(stream)) { + MarkedArgumentBuffer filledPullIntos; + while (filledPullIntos.size() < readableStreamGetNumReadIntoRequests(stream)) + filledPullIntos.append(readableByteStreamControllerShiftPendingPullInto(controller)); + if (filledPullIntos.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return; + } + for (size_t i = 0; i < filledPullIntos.size(); ++i) { + readableByteStreamControllerCommitPullIntoDescriptor(globalObject, stream, uncheckedDowncast(filledPullIntos.at(i))); + RETURN_IF_EXCEPTION(scope, void()); + } + } +} + +void readableByteStreamControllerRespondInReadableState(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, uint64_t bytesWritten, JSPullIntoDescriptor* pullIntoDescriptor) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(pullIntoDescriptor->m_bytesFilled + bytesWritten <= pullIntoDescriptor->m_byteLength); + readableByteStreamControllerFillHeadPullIntoDescriptor(controller, static_cast(bytesWritten), pullIntoDescriptor); + if (pullIntoDescriptor->m_readerType == ReaderType::None) { + readableByteStreamControllerEnqueueDetachedPullIntoToQueue(globalObject, controller, pullIntoDescriptor); + RETURN_IF_EXCEPTION(scope, void()); + MarkedArgumentBuffer filledPullIntos; + readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller, filledPullIntos); + if (filledPullIntos.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return; + } + for (size_t i = 0; i < filledPullIntos.size(); ++i) { + readableByteStreamControllerCommitPullIntoDescriptor(globalObject, controller->m_stream.get(), uncheckedDowncast(filledPullIntos.at(i))); + RETURN_IF_EXCEPTION(scope, void()); + } + return; + } + if (pullIntoDescriptor->m_bytesFilled < pullIntoDescriptor->m_minimumFill) + return; + readableByteStreamControllerShiftPendingPullInto(controller); + size_t remainderSize = pullIntoDescriptor->m_bytesFilled % pullIntoDescriptor->elementSize(); + if (remainderSize > 0) { + size_t end = pullIntoDescriptor->m_byteOffset + pullIntoDescriptor->m_bytesFilled; + readableByteStreamControllerEnqueueClonedChunkToQueue(globalObject, controller, *pullIntoDescriptor->m_buffer, end - remainderSize, remainderSize); + RETURN_IF_EXCEPTION(scope, void()); + } + pullIntoDescriptor->m_bytesFilled -= remainderSize; + MarkedArgumentBuffer filledPullIntos; + readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller, filledPullIntos); + if (filledPullIntos.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return; + } + readableByteStreamControllerCommitPullIntoDescriptor(globalObject, controller->m_stream.get(), pullIntoDescriptor); + RETURN_IF_EXCEPTION(scope, void()); + for (size_t i = 0; i < filledPullIntos.size(); ++i) { + readableByteStreamControllerCommitPullIntoDescriptor(globalObject, controller->m_stream.get(), uncheckedDowncast(filledPullIntos.at(i))); + RETURN_IF_EXCEPTION(scope, void()); + } +} + +void readableByteStreamControllerRespondInternal(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, uint64_t bytesWritten) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSPullIntoDescriptor* firstDescriptor = controller->m_pendingPullIntos.first().get(); + ASSERT(canTransferArrayBuffer(*firstDescriptor->m_buffer)); + readableByteStreamControllerInvalidateBYOBRequest(controller); + ReadableStreamState state = controller->m_stream->m_state; + if (state == ReadableStreamState::Closed) { + ASSERT(!bytesWritten); + readableByteStreamControllerRespondInClosedState(globalObject, controller, firstDescriptor); + RETURN_IF_EXCEPTION(scope, void()); + } else { + ASSERT(state == ReadableStreamState::Readable); + ASSERT(bytesWritten > 0); + readableByteStreamControllerRespondInReadableState(globalObject, controller, bytesWritten, firstDescriptor); + RETURN_IF_EXCEPTION(scope, void()); + } + RELEASE_AND_RETURN(scope, readableByteStreamControllerCallPullIfNeeded(globalObject, controller)); +} + +void readableByteStreamControllerRespondWithNewView(JSGlobalObject* globalObject, JSReadableByteStreamController* controller, JSArrayBufferView* view) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(!controller->m_pendingPullIntos.isEmpty()); + ASSERT(!view->isDetached()); + JSPullIntoDescriptor* firstDescriptor = controller->m_pendingPullIntos.first().get(); + ReadableStreamState state = controller->m_stream->m_state; + size_t viewByteLength = view->byteLength(); + if (state == ReadableStreamState::Closed) { + if (viewByteLength) { + throwTypeError(globalObject, scope, "A closed byte stream's BYOB request can only be responded to with a zero-length view"_s); + return; + } + } else { + ASSERT(state == ReadableStreamState::Readable); + if (!viewByteLength) { + throwTypeError(globalObject, scope, "A readable byte stream's BYOB request cannot be responded to with a zero-length view"_s); + return; + } + } + if (firstDescriptor->m_byteOffset + firstDescriptor->m_bytesFilled != view->byteOffset()) { + throwRangeError(globalObject, scope, "The view's byte offset does not match the BYOB request's current write position"_s); + return; + } + RefPtr viewedBuffer = view->possiblySharedBuffer(); + if (firstDescriptor->m_bufferByteLength != viewedBuffer->byteLength()) { + throwRangeError(globalObject, scope, "The view's buffer length does not match the BYOB request's buffer length"_s); + return; + } + if (firstDescriptor->m_bytesFilled + viewByteLength > firstDescriptor->m_byteLength) { + throwRangeError(globalObject, scope, "The view's byte length exceeds the remaining length of the BYOB request"_s); + return; + } + RefPtr transferredBuffer = transferArrayBufferImpl(globalObject, *viewedBuffer); + RETURN_IF_EXCEPTION(scope, void()); + firstDescriptor->m_buffer = WTF::move(transferredBuffer); + RELEASE_AND_RETURN(scope, readableByteStreamControllerRespondInternal(globalObject, controller, viewByteLength)); +} + +JSPullIntoDescriptor* readableByteStreamControllerShiftPendingPullInto(JSReadableByteStreamController* controller) +{ + ASSERT(!controller->m_byobRequest); + WTF::Locker locker { controller->cellLock() }; + return controller->m_pendingPullIntos.takeFirst().get(); +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.h b/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.h new file mode 100644 index 000000000000..7ff6d9ca1a60 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableByteStreamController.h @@ -0,0 +1,104 @@ +// JSReadableByteStreamController — the ReadableByteStreamController instance cell. +// Not user-constructible. DESTRUCTIBLE (owns the byte [[queue]] + [[pendingPullIntos]] +// deques). +#pragma once + +#include "root.h" +#include "StreamsForward.h" +#include "StreamQueue.h" + +#include "JSDOMConstructorNotConstructable.h" +#include "JSDOMGlobalObject.h" +#include +#include + +namespace WebCore { + +class JSReadableByteStreamController final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + // Internal allocation entry point (setUpReadableByteStreamController*). + static JSReadableByteStreamController* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream, m_byobRequest, every barrier inside + // m_algorithms, and the barrier container m_pendingPullIntos (m_queue entries hold + // ArrayBuffer impls via RefPtr, so the queue has nothing for the GC). + // cellLock() is NON-RECURSIVE (StreamQueue.h). This visitChildrenImpl takes + // `Locker locker { cellLock() }` exactly ONCE, and inside that ONE scope both + // iterates m_pendingPullIntos and calls m_queue.visit(locker, visitor) (StreamQueue + // never re-acquires the lock). Never visit either container outside that scope, and + // never take a second Locker. Mutating ops that touch BOTH containers do the same. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Spec internal slots + + // [[queue]] (list of readable byte stream queue entries) + [[queueTotalSize]] + Bun::WebStreams::StreamQueue m_queue; + // [[pendingPullIntos]] — mutated AND visited under cellLock(). + WTF::Deque, 4> m_pendingPullIntos; + // [[stream]] + JSC::WriteBarrier m_stream; + // [[byobRequest]] — null after invalidation / when none is pending. + JSC::WriteBarrier m_byobRequest; + // [[autoAllocateChunkSize]] — 0 = the spec's `undefined` (the spec rejects an explicit 0 + // with TypeError at set-up, so 0 is a safe sentinel). + uint64_t m_autoAllocateChunkSize { 0 }; + // [[strategyHWM]] + double m_strategyHWM { 0 }; + // [[started]] + bool m_started { false }; + // [[pulling]] + bool m_pulling { false }; + // [[pullAgain]] + bool m_pullAgain { false }; + // [[closeRequested]] + bool m_closeRequested { false }; + + // The algorithm machinery — replaces [[pullAlgorithm]] and [[cancelAlgorithm]]. A byte + // stream has NO size algorithm (a byte stream given a size strategy is a RangeError at + // construction). See SourceAlgorithmSlots (StreamQueue.h). + // The reachable m_algorithms.kind set on a BYTE controller is EXACTLY + // {JavaScript, Nothing, ByteTeeBranch}. CrossRealm is impossible (the cross-realm + // readable endpoint is always a DEFAULT controller — JSCrossRealmTransformState's + // back-pointer is exact-typed to one) and Native always uses a DEFAULT controller. + Bun::WebStreams::SourceAlgorithmSlots m_algorithms; + + // Internal methods + + // [[CancelSteps]](reason) — userJS: YES (performs the user cancel algorithm). + JSC::JSPromise* cancelSteps(JSC::JSGlobalObject*, JSC::JSValue reason); + // [[PullSteps]](readRequest) — userJS: YES (transitive). + void pullSteps(JSC::JSGlobalObject*, JSReadRequest*); + // [[ReleaseSteps]]() — truncates [[pendingPullIntos]] to its head w/ readerType=None. userJS: no. + void releaseSteps(); + +private: + JSReadableByteStreamController(JSC::VM&, JSC::Structure*); + ~JSReadableByteStreamController(); + void finishCreation(JSC::VM&); +}; + +// Construct throws `TypeError: Illegal constructor`; the constructor object is still +// installed on globalThis so instanceof / .prototype work. +using JSReadableByteStreamControllerConstructor = JSDOMConstructorNotConstructable; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStream.cpp b/src/jsc/bindings/webcore/streams/JSReadableStream.cpp new file mode 100644 index 000000000000..3834bca12e08 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStream.cpp @@ -0,0 +1,800 @@ +#include "config.h" +#include "JSReadableStream.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "ErrorCode.h" +#include "JSAbortSignal.h" +#include "JSDOMBinding.h" +#include "JSDOMConvertNumbers.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStreamAsyncIterator.h" +#include "JSReadableStreamBYOBReader.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSWritableStream.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_cancel); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_getReader); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_pipeThrough); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_pipeTo); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_tee); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_values); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_text); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_json); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_bytes); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_blob); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamStaticFunction_from); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamPrototypeGetter_locked); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamPrototypeGetter_constructor); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamPrototype_nativePtrGetter); +static JSC_DECLARE_CUSTOM_SETTER(jsReadableStreamPrototype_nativePtrSetter); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamPrototype_nativeTypeGetter); +static JSC_DECLARE_CUSTOM_SETTER(jsReadableStreamPrototype_nativeTypeSetter); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamPrototype_disturbedGetter); +static JSC_DECLARE_CUSTOM_SETTER(jsReadableStreamPrototype_disturbedSetter); + +class JSReadableStreamPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSReadableStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSReadableStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSReadableStreamPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamPrototype, JSReadableStreamPrototype::Base); + +// WebIDL dictionary conversions. Each [[Get]] is observable and happens in alphabetical +// member order; a present, non-callable callback member throws during conversion. + +struct ConvertedQueuingStrategy { + QueuingStrategyDict dict {}; + // Bun deviation from WebIDL: `typeof rawHighWaterMark === "number"` before the ToNumber. + bool rawHighWaterMarkIsNumber { false }; +}; + +static ConvertedQueuingStrategy convertQueuingStrategy(JSC::VM& vm, JSGlobalObject* globalObject, JSValue strategy) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + ConvertedQueuingStrategy result; + if (strategy.isUndefinedOrNull()) + return result; + if (!strategy.isObject()) { + throwTypeError(globalObject, scope, "ReadableStream constructor takes an object as second argument, if any"_s); + return result; + } + auto* strategyObject = asObject(strategy); + auto& names = builtinNames(vm); + + JSValue highWaterMark = strategyObject->get(globalObject, names.highWaterMarkPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!highWaterMark.isUndefined()) { + result.rawHighWaterMarkIsNumber = highWaterMark.isNumber(); + double value = highWaterMark.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, result); + result.dict.highWaterMark = value; + } + + JSValue size = strategyObject->get(globalObject, names.sizePublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!size.isUndefined()) { + if (!size.isCallable()) { + throwTypeError(globalObject, scope, "The queuing strategy's 'size' property must be a function"_s); + return result; + } + result.dict.size = size; + } + return result; +} + +// Bun extends the WebIDL `ReadableStreamType` enum with "direct". +enum class BunUnderlyingSourceType : uint8_t { None, + Bytes, + Direct }; + +struct ConvertedUnderlyingSource { + UnderlyingSourceDict dict {}; + BunUnderlyingSourceType type { BunUnderlyingSourceType::None }; +}; + +static ConvertedUnderlyingSource convertUnderlyingSource(JSC::VM& vm, JSGlobalObject* globalObject, JSValue underlyingSource) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + ConvertedUnderlyingSource result; + if (underlyingSource.isUndefinedOrNull()) + return result; + auto* sourceObject = asObject(underlyingSource); + auto& names = builtinNames(vm); + + JSValue autoAllocateChunkSize = sourceObject->get(globalObject, names.autoAllocateChunkSizePublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!autoAllocateChunkSize.isUndefined()) { + uint64_t value = convertToIntegerEnforceRange(*globalObject, autoAllocateChunkSize); + RETURN_IF_EXCEPTION(scope, result); + result.dict.autoAllocateChunkSize = value; + } + + JSValue cancel = sourceObject->get(globalObject, names.cancelPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!cancel.isUndefined()) { + if (!cancel.isCallable()) { + throwTypeError(globalObject, scope, "The underlying source's 'cancel' property must be a function"_s); + return result; + } + result.dict.cancel = cancel; + } + + JSValue pull = sourceObject->get(globalObject, names.pullPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!pull.isUndefined()) { + if (!pull.isCallable()) { + throwTypeError(globalObject, scope, "The underlying source's 'pull' property must be a function"_s); + return result; + } + result.dict.pull = pull; + } + + JSValue start = sourceObject->get(globalObject, names.startPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!start.isUndefined()) { + if (!start.isCallable()) { + throwTypeError(globalObject, scope, "The underlying source's 'start' property must be a function"_s); + return result; + } + result.dict.start = start; + } + + JSValue type = sourceObject->get(globalObject, names.typePublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!type.isUndefined()) { + auto typeString = type.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, result); + if (typeString == "bytes"_s) { + result.type = BunUnderlyingSourceType::Bytes; + result.dict.type = ReadableStreamType::Bytes; + } else if (typeString == "direct"_s) + result.type = BunUnderlyingSourceType::Direct; + else + throwTypeError(globalObject, scope, makeString("'"_s, typeString, "' is not a valid underlying source 'type'; expected \"bytes\", \"direct\", or undefined"_s)); + } + return result; +} + +struct ConvertedStreamPipeOptions { + bool preventAbort { false }; + bool preventCancel { false }; + bool preventClose { false }; + JSC::JSObject* signal { nullptr }; +}; + +static ConvertedStreamPipeOptions convertStreamPipeOptions(JSC::VM& vm, JSGlobalObject* globalObject, JSValue options) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + ConvertedStreamPipeOptions result; + if (options.isUndefinedOrNull()) + return result; + if (!options.isObject()) { + throwTypeError(globalObject, scope, "The pipe options must be an object"_s); + return result; + } + auto* optionsObject = asObject(options); + + JSValue preventAbort = optionsObject->get(globalObject, builtinNames(vm).preventAbortPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!preventAbort.isUndefined()) + result.preventAbort = preventAbort.toBoolean(globalObject); + + JSValue preventCancel = optionsObject->get(globalObject, builtinNames(vm).preventCancelPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!preventCancel.isUndefined()) + result.preventCancel = preventCancel.toBoolean(globalObject); + + JSValue preventClose = optionsObject->get(globalObject, builtinNames(vm).preventClosePublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!preventClose.isUndefined()) + result.preventClose = preventClose.toBoolean(globalObject); + + JSValue signal = optionsObject->get(globalObject, builtinNames(vm).signalPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!signal.isUndefined()) { + auto* abortSignal = dynamicDowncast(signal); + if (!abortSignal) { + throwTypeError(globalObject, scope, "The pipe options' 'signal' property must be an AbortSignal"_s); + return result; + } + result.signal = abortSignal; + } + return result; +} + +// JSReadableStreamConstructor = JSStreamConstructor. +// Every member specialization is declared before the ClassInfo (whose method table +// instantiates them). + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSReadableStreamConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSReadableStreamConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSReadableStreamConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSReadableStreamConstructor::subspaceForImpl(JSC::VM&); +template<> void JSReadableStreamConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSReadableStreamConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSReadableStreamConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSReadableStreamConstructor::s_info = { "ReadableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamConstructor) }; + +template<> JSValue JSReadableStreamConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSReadableStreamConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSReadableStreamConstructor); + +template<> GCClient::IsoSubspace* JSReadableStreamConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamConstructor = std::forward(space); }); +} + +template<> void JSReadableStreamConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "ReadableStream"_s); + 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); + + auto* fromFunction = JSFunction::create(vm, &globalObject, 1, "from"_s, jsReadableStreamStaticFunction_from, ImplementationVisibility::Public, NoIntrinsic); + putDirect(vm, vm.propertyNames->from, fromFunction, 0); + + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSReadableStreamConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSReadableStreamConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + // `optional object underlyingSource`: missing => null; a present non-object is a TypeError. + JSValue underlyingSource = callFrame->argument(0); + if (underlyingSource.isUndefined()) + underlyingSource = jsNull(); + else if (!underlyingSource.isObject()) + return throwVMTypeError(lexicalGlobalObject, scope, "ReadableStream constructor takes an object as first argument"_s); + + // WebIDL converts the strategy ARGUMENT before the constructor steps convert the source. + auto strategy = convertQueuingStrategy(vm, lexicalGlobalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* stream = JSReadableStream::create(vm, structure); + + auto source = convertUnderlyingSource(vm, lexicalGlobalObject, underlyingSource); + RETURN_IF_EXCEPTION(scope, {}); + + initializeReadableStream(stream); + stream->m_bunHighWaterMarkIsNumber = strategy.rawHighWaterMarkIsNumber; + if (strategy.dict.highWaterMark) + stream->m_bunHighWaterMark = *strategy.dict.highWaterMark; + + switch (source.type) { + case BunUnderlyingSourceType::Direct: { + // A direct stream has no controller yet; materializeIfNeeded() builds it on first use. + stream->m_bunMode = BunStreamMode::DirectPending; + stream->m_directUnderlyingSource.set(vm, stream, asObject(underlyingSource)); + break; + } + case BunUnderlyingSourceType::Bytes: { + if (strategy.dict.size) + return throwVMRangeError(lexicalGlobalObject, scope, "The queuing strategy of a readable byte stream cannot have a size function"_s); + double highWaterMark = extractHighWaterMark(lexicalGlobalObject, strategy.dict, 0); + RETURN_IF_EXCEPTION(scope, {}); + setUpReadableByteStreamControllerFromUnderlyingSource(lexicalGlobalObject, stream, underlyingSource, source.dict, highWaterMark); + RETURN_IF_EXCEPTION(scope, {}); + break; + } + case BunUnderlyingSourceType::None: { + auto* sizeAlgorithm = extractSizeAlgorithm(strategy.dict); + double highWaterMark = extractHighWaterMark(lexicalGlobalObject, strategy.dict, 1); + RETURN_IF_EXCEPTION(scope, {}); + setUpReadableStreamDefaultControllerFromUnderlyingSource(lexicalGlobalObject, stream, underlyingSource, source.dict, highWaterMark, sizeAlgorithm); + RETURN_IF_EXCEPTION(scope, {}); + break; + } + } + return JSValue::encode(stream); +} +JSC_ANNOTATE_HOST_FUNCTION(JSReadableStreamConstructorConstruct, JSReadableStreamConstructor::construct); + +// JSReadableStreamPrototype + +static const HashTableValue JSReadableStreamPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamPrototypeGetter_constructor, 0 } }, + { "locked"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamPrototypeGetter_locked, 0 } }, + { "cancel"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_cancel, 0 } }, + { "getReader"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_getReader, 0 } }, + { "pipeThrough"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_pipeThrough, 1 } }, + { "pipeTo"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_pipeTo, 1 } }, + { "tee"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_tee, 0 } }, + { "values"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_values, 0 } }, + { "blob"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_blob, 0 } }, + { "bytes"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_bytes, 0 } }, + { "json"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_json, 0 } }, + { "text"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamPrototypeFunction_text, 0 } }, +}; + +const ClassInfo JSReadableStreamPrototype::s_info = { "ReadableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamPrototype) }; + +void JSReadableStreamPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSReadableStream::info(), JSReadableStreamPrototypeTableValues, *this); + + // @@asyncIterator is the SAME function object as values() (WebIDL async_iterable). + JSValue valuesFunction = getDirect(vm, vm.propertyNames->builtinNames().valuesPublicName()); + putDirectWithoutTransition(vm, vm.propertyNames->asyncIteratorSymbol, valuesFunction, static_cast(JSC::PropertyAttribute::DontEnum)); + + // Bun private-name accessors read by surviving builtins (`stream.$bunNativePtr`, ...). + auto& names = builtinNames(vm); + putDirectCustomAccessor(vm, names.bunNativePtrPrivateName(), DOMAttributeGetterSetter::create(vm, jsReadableStreamPrototype_nativePtrGetter, jsReadableStreamPrototype_nativePtrSetter, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | JSC::PropertyAttribute::DontDelete); + putDirectCustomAccessor(vm, names.bunNativeTypePrivateName(), DOMAttributeGetterSetter::create(vm, jsReadableStreamPrototype_nativeTypeGetter, jsReadableStreamPrototype_nativeTypeSetter, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | JSC::PropertyAttribute::DontDelete); + putDirectCustomAccessor(vm, names.disturbedPrivateName(), DOMAttributeGetterSetter::create(vm, jsReadableStreamPrototype_disturbedGetter, jsReadableStreamPrototype_disturbedSetter, DOMAttributeAnnotation { JSReadableStream::info(), nullptr }), JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute | JSC::PropertyAttribute::DontDelete); + + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSReadableStream + +const ClassInfo JSReadableStream::s_info = { "ReadableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStream) }; + +JSReadableStream::JSReadableStream(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSReadableStream::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + // Bun snapshots the ambient AsyncContext at construction; source callbacks restore it. + if (auto* asyncContextData = globalObject()->m_asyncContextData.get()) + m_asyncContext.set(vm, this, asyncContextData->getInternalField(0)); +} + +JSReadableStream* JSReadableStream::create(VM& vm, Structure* structure) +{ + auto* stream = new (NotNull, allocateCell(vm)) JSReadableStream(vm, structure); + stream->finishCreation(vm); + return stream; +} + +Structure* JSReadableStream::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSReadableStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSReadableStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSReadableStreamPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSReadableStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSReadableStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSReadableStream::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStream = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStream = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadableStream); + +template +void JSReadableStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_reader); + visitor.append(thisObject->m_storedError); + visitor.append(thisObject->m_controller); + visitor.append(thisObject->m_nativePtr); + visitor.append(thisObject->m_directUnderlyingSource); + visitor.append(thisObject->m_asyncContext); +} + +void JSReadableStream::materializeIfNeeded(JSGlobalObject* globalObject) +{ + if (m_bunMode == BunStreamMode::Default) [[likely]] + return; + // Clear the mode BEFORE running the thunk so re-entrant consumers see it done. + auto mode = m_bunMode; + m_bunMode = BunStreamMode::Default; + if (mode == BunStreamMode::DirectPending) + setUpDirectStreamController(globalObject, this, DirectSinkKind::ArrayBuffer, m_bunHighWaterMark); + else + materializeNativeSource(globalObject, this); +} + +// Prototype host functions + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSReadableStream::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototypeGetter_locked, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + return JSValue::encode(jsBoolean(isReadableStreamLocked(stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_cancel, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "ReadableStream.prototype.cancel can only be called on a ReadableStream"_s)))); + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "Cannot cancel a locked ReadableStream"_s)))); + auto* promise = readableStreamCancel(lexicalGlobalObject, stream, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_getReader, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + + // ReadableStreamGetReaderOptions { ReadableStreamReaderMode mode; } + bool isBYOB = false; + JSValue options = callFrame->argument(0); + if (!options.isUndefinedOrNull()) { + if (!options.isObject()) + return Bun::ERR::INVALID_ARG_TYPE(scope, lexicalGlobalObject, "options"_s, "object"_s, options); + JSValue mode = asObject(options)->get(lexicalGlobalObject, builtinNames(vm).modePublicName()); + RETURN_IF_EXCEPTION(scope, {}); + if (!mode.isUndefined()) { + auto modeString = mode.toWTFString(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + if (modeString != "byob"_s) + return Bun::ERR::INVALID_ARG_VALUE(scope, lexicalGlobalObject, "options.mode"_s, mode); + isBYOB = true; + } + } + + if (isBYOB) { + // A BYOB reader never materializes Bun's lazy modes. + auto* reader = acquireReadableStreamBYOBReader(lexicalGlobalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(reader); + } + + stream->materializeIfNeeded(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto* reader = acquireReadableStreamDefaultReader(lexicalGlobalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(reader); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_pipeThrough, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + + // ReadableWritablePair { required ReadableStream readable; required WritableStream writable; } + JSValue transform = callFrame->argument(0); + if (!transform.isObject()) + return throwVMTypeError(lexicalGlobalObject, scope, "pipeThrough() expects an object with 'readable' and 'writable' properties"_s); + auto* transformObject = asObject(transform); + JSValue readableValue = transformObject->get(lexicalGlobalObject, builtinNames(vm).readablePublicName()); + RETURN_IF_EXCEPTION(scope, {}); + auto* transformReadable = dynamicDowncast(readableValue); + if (!transformReadable) + return throwVMTypeError(lexicalGlobalObject, scope, "The transform's 'readable' property must be a ReadableStream"_s); + JSValue writableValue = transformObject->get(lexicalGlobalObject, builtinNames(vm).writablePublicName()); + RETURN_IF_EXCEPTION(scope, {}); + auto* transformWritable = dynamicDowncast(writableValue); + if (!transformWritable) + return throwVMTypeError(lexicalGlobalObject, scope, "The transform's 'writable' property must be a WritableStream"_s); + + auto options = convertStreamPipeOptions(vm, lexicalGlobalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + + if (isReadableStreamLocked(stream)) + return throwVMTypeError(lexicalGlobalObject, scope, "Cannot pipe a locked ReadableStream"_s); + if (isWritableStreamLocked(transformWritable)) + return throwVMTypeError(lexicalGlobalObject, scope, "Cannot pipe to a locked WritableStream"_s); + + auto* promise = readableStreamPipeTo(lexicalGlobalObject, stream, transformWritable, options.preventClose, options.preventAbort, options.preventCancel, options.signal); + RETURN_IF_EXCEPTION(scope, {}); + markPromiseAsHandled(vm, promise); + return JSValue::encode(transformReadable); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_pipeTo, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "ReadableStream.prototype.pipeTo can only be called on a ReadableStream"_s)))); + auto* destination = dynamicDowncast(callFrame->argument(0)); + if (!destination) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "ReadableStream.prototype.pipeTo requires a WritableStream destination"_s)))); + + ConvertedStreamPipeOptions options; + { + // WebIDL: a promise-returning operation turns an argument-conversion failure into a rejection. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + options = convertStreamPipeOptions(vm, lexicalGlobalObject, callFrame->argument(1)); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(lexicalGlobalObject, catchScope); + if (thrown.isEmpty()) + return {}; + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, thrown))); + } + } + + if (isReadableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "Cannot pipe a locked ReadableStream"_s)))); + if (isWritableStreamLocked(destination)) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "Cannot pipe to a locked WritableStream"_s)))); + + auto* promise = readableStreamPipeTo(lexicalGlobalObject, stream, destination, options.preventClose, options.preventAbort, options.preventCancel, options.signal); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_tee, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + auto branches = readableStreamTee(lexicalGlobalObject, stream, false); + RETURN_IF_EXCEPTION(scope, {}); + auto* array = constructEmptyArray(lexicalGlobalObject, nullptr, 2); + RETURN_IF_EXCEPTION(scope, {}); + array->putDirectIndex(lexicalGlobalObject, 0, branches.first); + RETURN_IF_EXCEPTION(scope, {}); + array->putDirectIndex(lexicalGlobalObject, 1, branches.second); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(array); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_values, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + + // ReadableStreamIteratorOptions { boolean preventCancel = false; } + bool preventCancel = false; + JSValue options = callFrame->argument(0); + if (!options.isUndefinedOrNull()) { + if (!options.isObject()) + return throwVMTypeError(lexicalGlobalObject, scope, "values() options must be an object"_s); + JSValue preventCancelValue = asObject(options)->get(lexicalGlobalObject, builtinNames(vm).preventCancelPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + if (!preventCancelValue.isUndefined()) + preventCancel = preventCancelValue.toBoolean(lexicalGlobalObject); + } + + stream->materializeIfNeeded(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + + auto* domGlobalObject = defaultGlobalObject(lexicalGlobalObject); + auto* iterator = JSReadableStreamAsyncIterator::create(vm, getDOMStructure(vm, *domGlobalObject)); + auto* reader = acquireReadableStreamDefaultReader(lexicalGlobalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + iterator->m_reader.set(vm, iterator, reader); + iterator->m_preventCancel = preventCancel; + return JSValue::encode(iterator); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamStaticFunction_from, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = readableStreamFromIterable(lexicalGlobalObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(stream); +} + +// Bun-only prototype methods. Each is a one-line delegation. + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_text, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToText(lexicalGlobalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_json, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToJSON(lexicalGlobalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_bytes, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToBytes(lexicalGlobalObject, stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamPrototypeFunction_blob, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStream"_s); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToBlob(lexicalGlobalObject, stream))); +} + +// Bun private-name accessors ($bunNativePtr / $bunNativeType / $disturbed). + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_nativePtrGetter, (JSGlobalObject*, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto* stream = uncheckedDowncast(JSValue::decode(thisValue)); + JSValue nativePtr = stream->nativePtrForJS(); + return JSValue::encode(nativePtr.isEmpty() ? jsUndefined() : nativePtr); +} + +JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_nativePtrSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto* stream = uncheckedDowncast(JSValue::decode(thisValue)); + stream->m_nativePtr.set(vm, stream, JSValue::decode(encodedValue)); + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_nativeTypeGetter, (JSGlobalObject*, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto* stream = uncheckedDowncast(JSValue::decode(thisValue)); + return JSValue::encode(jsNumber(stream->m_nativeType)); +} + +JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_nativeTypeSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = uncheckedDowncast(JSValue::decode(thisValue)); + int32_t nativeType = JSValue::decode(encodedValue).toInt32(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, false); + stream->m_nativeType = nativeType; + return true; +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamPrototype_disturbedGetter, (JSGlobalObject*, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto* stream = uncheckedDowncast(JSValue::decode(thisValue)); + return JSValue::encode(jsBoolean(stream->m_disturbed)); +} + +JSC_DEFINE_CUSTOM_SETTER(jsReadableStreamPrototype_disturbedSetter, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, PropertyName)) +{ + auto* stream = uncheckedDowncast(JSValue::decode(thisValue)); + stream->m_disturbed = JSValue::decode(encodedValue).toBoolean(lexicalGlobalObject); + return true; +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStream.h b/src/jsc/bindings/webcore/streams/JSReadableStream.h new file mode 100644 index 000000000000..b0e8f145cc52 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStream.h @@ -0,0 +1,118 @@ +// JSReadableStream — the ReadableStream instance cell. ONE GC cell IS the stream: no +// wrapped impl, no RefCounted, no toWrapped. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include +#include +#include + +namespace WebCore { + +// Non-destructible (owns no WTF container). +class JSReadableStream final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + // Internal (non-user) allocation entry point; callers use getDOMStructure(). + static JSReadableStream* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_reader, m_storedError, m_controller, m_nativePtr, + // m_directUnderlyingSource, m_asyncContext. No barrier container ⇒ no cellLock needed. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Spec internal slots + + // [[state]] + ReadableStreamState m_state { ReadableStreamState::Readable }; + // [[disturbed]] + bool m_disturbed { false }; + // [[Detached]] (transferable streams are not implemented; the slot exists) + bool m_detached { false }; + // Bun: locked by a native/direct consumer WITHOUT a real reader object. Part of every + // isReadableStreamLocked() check. + bool m_lockedWithoutReader { false }; + // `$bunNativeType`: write-only today, kept for the FFI ABI. + int32_t m_nativeType { 0 }; + // Set by jsFunctionTransferToNativeReadableStream. + bool m_transferred { false }; + // The Bun lazy-start mode; tells materializeIfNeeded() what to do. + BunStreamMode m_bunMode { BunStreamMode::Default }; + // The tag for the ERASED m_controller below. Every switch over it is TOTAL. + ControllerKind m_controllerKind { ControllerKind::None }; + // `typeof rawHighWaterMark === "number"` at construction time. + bool m_bunHighWaterMarkIsNumber { false }; + + // [[reader]] — a default reader, a BYOB reader, or null (undefined). + JSC::WriteBarrier m_reader; + // [[storedError]] — gate reads on m_state == Errored (an errored stream's stored error + // can legitimately BE `undefined`). + JSC::WriteBarrier m_storedError; + // [[controller]] — the subsystem's ONE mandatory ERASED back-pointer: a spec controller, + // a JSDirectStreamController, or a generated JSReadable*Controller JSSink cell. Raw + // jsCast/jsDynamicCast on this slot is BANNED; dispatch on m_controllerKind through the + // total switch. + JSC::WriteBarrier m_controller; + + // Bun extension state + + // `$bunNativePtr`: empty = not native; a JSCell = the JS{Blob,File,Bytes}Internal- + // ReadableStreamSource handle from Rust; jsNumber(-1) = detached. + JSC::WriteBarrier m_nativePtr; + // `$underlyingSource` on the STREAM. Non-null ⇔ type:"direct" AND not yet consumed. + JSC::WriteBarrier m_directUnderlyingSource; + // `$asyncContext` snapshot at construction. Written once in finishCreation. + JSC::WriteBarrier m_asyncContext; + // `$highWaterMark` on the STREAM (the raw strategy HWM, ToNumber'd once). NaN = unset. + // Written by ALL FOUR constructor arms. + double m_bunHighWaterMark { std::numeric_limits::quiet_NaN() }; + // autoAllocateChunkSize from $createNativeReadableStream. 0 = unset (=> 256 KiB default). + uint64_t m_autoAllocateChunkSize { 0 }; + + // Bun helpers + + // Runs the lazy-start thunk if any. Idempotent. MUST be the first thing every consumer + // does. userJS: YES (direct pull setup / native handle.start()). + void materializeIfNeeded(JSC::JSGlobalObject*); + + // The value the old `$bunNativePtr` DOMAttribute getter returned. + JSC::JSValue nativePtrForJS() const + { + if (m_transferred) + return JSC::jsNumber(-1); + return m_nativePtr.get(); // may be empty + } + bool nativeHandleDetached() const + { + return m_transferred || (m_nativePtr.get().isInt32() && m_nativePtr.get().asInt32() == -1); + } + +private: + JSReadableStream(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +using JSReadableStreamConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.cpp b/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.cpp new file mode 100644 index 000000000000..c4ef7263add1 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.cpp @@ -0,0 +1,323 @@ +#include "config.h" +#include "JSReadableStreamAsyncIterator.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMWrapperCache.h" +#include "JSReadRequest.h" +#include "JSReadableStreamDefaultController.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSStreamsRuntime.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamAsyncIteratorPrototypeFunction_next); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamAsyncIteratorPrototypeFunction_return); + +class JSReadableStreamAsyncIteratorPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSReadableStreamAsyncIteratorPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSReadableStreamAsyncIteratorPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamAsyncIteratorPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamAsyncIteratorPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSReadableStreamAsyncIteratorPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamAsyncIteratorPrototype, JSReadableStreamAsyncIteratorPrototype::Base); + +// %ReadableStreamAsyncIteratorPrototype% owns only `next` and `return`; +// @@asyncIterator comes from its [[Prototype]], %AsyncIteratorPrototype%. +static const HashTableValue JSReadableStreamAsyncIteratorPrototypeTableValues[] = { + { "next"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamAsyncIteratorPrototypeFunction_next, 0 } }, + { "return"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamAsyncIteratorPrototypeFunction_return, 1 } }, +}; + +const ClassInfo JSReadableStreamAsyncIteratorPrototype::s_info = { "ReadableStreamAsyncIterator"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamAsyncIteratorPrototype) }; + +void JSReadableStreamAsyncIteratorPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSReadableStreamAsyncIterator::info(), JSReadableStreamAsyncIteratorPrototypeTableValues, *this); +} + +// JSReadableStreamAsyncIterator + +const ClassInfo JSReadableStreamAsyncIterator::s_info = { "ReadableStreamAsyncIterator"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamAsyncIterator) }; + +JSReadableStreamAsyncIterator::JSReadableStreamAsyncIterator(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSReadableStreamAsyncIterator::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSReadableStreamAsyncIterator* JSReadableStreamAsyncIterator::create(VM& vm, Structure* structure) +{ + auto* iterator = new (NotNull, allocateCell(vm)) JSReadableStreamAsyncIterator(vm, structure); + iterator->finishCreation(vm); + return iterator; +} + +Structure* JSReadableStreamAsyncIterator::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSReadableStreamAsyncIterator::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSReadableStreamAsyncIteratorPrototype::createStructure(vm, &globalObject, globalObject.asyncIteratorPrototype()); + structure->setMayBePrototype(true); + return JSReadableStreamAsyncIteratorPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSReadableStreamAsyncIterator::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +GCClient::IsoSubspace* JSReadableStreamAsyncIterator::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamAsyncIterator.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamAsyncIterator = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamAsyncIterator.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamAsyncIterator = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadableStreamAsyncIterator); + +template +void JSReadableStreamAsyncIterator::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_reader); + visitor.append(thisObject->m_ongoingPromise); +} + +// "Get the next iteration result": the read request's chunk/close/error steps +// (JSReadRequest.cpp, AsyncIterator kind) settle the result promise carried at field 1. +static JSPromise* runAsyncIteratorNextSteps(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStreamAsyncIterator* iterator) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + if (iterator->m_isFinished) { + auto* result = createIteratorResultObject(globalObject, jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, promiseResolvedWith(globalObject, result)); + } + + auto* reader = iterator->m_reader.get(); + ASSERT(reader); + // Queued chunk and nothing waiting: dequeue with no read request. The result promise + // still settles in a microtask, as the spec's read-request chunk steps require. + JSValue chunk = readableStreamDefaultReaderTryReadFromQueue(globalObject, reader); + RETURN_IF_EXCEPTION(scope, nullptr); + if (chunk) { + auto* result = createIteratorResultObject(globalObject, chunk, false); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + queueStreamsMicrotask(globalObject, JSStreamsRuntime::from(globalObject)->onAsyncIteratorResolveMicrotask(), result, promise); + return promise; + } + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + auto* context = InternalFieldTuple::create(vm, domGlobalObject->internalFieldTupleStructure(), iterator, result); + auto* readRequest = JSReadRequest::create(vm, runtime->readRequestStructure(domGlobalObject), ReadRequestKind::AsyncIterator, context); + readableStreamDefaultReaderRead(globalObject, reader, readRequest); + RETURN_IF_EXCEPTION(scope, nullptr); + return result; +} + +// "Asynchronous iterator return", wrapped per Web IDL: the result fulfills with { value, done: true }. +static JSPromise* runAsyncIteratorReturnSteps(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStreamAsyncIterator* iterator, JSValue value) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + if (iterator->m_isFinished) { + auto* result = createIteratorResultObject(globalObject, value, true); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, promiseResolvedWith(globalObject, result)); + } + iterator->m_isFinished = true; + + auto* reader = iterator->m_reader.get(); + ASSERT(reader); + ASSERT(reader->m_readRequests.isEmpty()); + + JSPromise* innerPromise = nullptr; + if (!iterator->m_preventCancel) { + innerPromise = readableStreamReaderGenericCancel(globalObject, reader, value); + RETURN_IF_EXCEPTION(scope, nullptr); + readableStreamDefaultReaderRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, nullptr); + } else { + readableStreamDefaultReaderRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, nullptr); + innerPromise = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, nullptr); + } + + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + // A tuple, not `value` directly: the context channel drops null/undefined contexts. + auto* context = InternalFieldTuple::create(vm, domGlobalObject->internalFieldTupleStructure(), iterator, value); + innerPromise->performPromiseThenWithContext(vm, globalObject, runtime->onAsyncIteratorCancelFulfilled(), jsUndefined(), result, context); + return result; +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamAsyncIteratorPrototypeFunction_next, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* iterator = dynamicDowncast(callFrame->thisValue()); + if (!iterator) [[unlikely]] + RELEASE_AND_RETURN(scope, rejectPromiseWithThisTypeError(*globalObject, "ReadableStreamAsyncIterator"_s, "next"_s)); + + auto* ongoingPromise = iterator->m_ongoingPromise.get(); + if (ongoingPromise && ongoingPromise->status() == JSPromise::Status::Pending) { + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* chained = JSPromise::create(vm, globalObject->promiseStructure()); + auto* onSettled = runtime->onAsyncIteratorNextAfterOngoingSettled(); + ongoingPromise->performPromiseThenWithContext(vm, globalObject, onSettled, onSettled, chained, iterator); + iterator->m_ongoingPromise.set(vm, iterator, chained); + return JSValue::encode(chained); + } + + auto* promise = runAsyncIteratorNextSteps(vm, globalObject, iterator); + RETURN_IF_EXCEPTION(scope, {}); + iterator->m_ongoingPromise.set(vm, iterator, promise); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamAsyncIteratorPrototypeFunction_return, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* iterator = dynamicDowncast(callFrame->thisValue()); + if (!iterator) [[unlikely]] + RELEASE_AND_RETURN(scope, rejectPromiseWithThisTypeError(*globalObject, "ReadableStreamAsyncIterator"_s, "return"_s)); + + JSValue value = callFrame->argument(0); + auto* ongoingPromise = iterator->m_ongoingPromise.get(); + if (ongoingPromise && ongoingPromise->status() == JSPromise::Status::Pending) { + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* chained = JSPromise::create(vm, globalObject->promiseStructure()); + auto* context = InternalFieldTuple::create(vm, domGlobalObject->internalFieldTupleStructure(), iterator, value); + auto* onSettled = runtime->onAsyncIteratorReturnAfterOngoingSettled(); + ongoingPromise->performPromiseThenWithContext(vm, globalObject, onSettled, onSettled, chained, context); + iterator->m_ongoingPromise.set(vm, iterator, chained); + return JSValue::encode(chained); + } + + auto* promise = runAsyncIteratorReturnSteps(vm, globalObject, iterator, value); + RETURN_IF_EXCEPTION(scope, {}); + iterator->m_ongoingPromise.set(vm, iterator, promise); + return JSValue::encode(promise); +} + +// [reaction-convention] handlers (context at argument(1)). Each is a boundary: an exception +// it propagates rejects the chained result promise it was registered with. + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIteratorNextAfterOngoingSettled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* iterator = dynamicDowncast(callFrame->argument(1)); + if (!iterator) + return JSValue::encode(jsUndefined()); + auto* promise = runAsyncIteratorNextSteps(vm, globalObject, iterator); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIteratorReturnAfterOngoingSettled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = dynamicDowncast(callFrame->argument(1)); + if (!context) + return JSValue::encode(jsUndefined()); + auto* iterator = uncheckedDowncast(context->getInternalField(0)); + auto* promise = runAsyncIteratorReturnSteps(vm, globalObject, iterator, context->getInternalField(1)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +// The spec settles next()'s promise from a queued microtask; these two are that job +// ([reaction-convention]: argument(0) = value, argument(1) = the promise). +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIteratorResolveMicrotask, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* promise = uncheckedDowncast(callFrame->argument(1)); + resolvePromise(globalObject, promise, callFrame->argument(0)); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIteratorRejectMicrotask, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* promise = uncheckedDowncast(callFrame->argument(1)); + rejectPromise(globalObject, promise, callFrame->argument(0)); + return JSValue::encode(jsUndefined()); +} + +// Fulfillment steps for the cancel promise: the return() result carries the caller's argument. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onAsyncIteratorCancelFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = dynamicDowncast(callFrame->argument(1)); + if (!context) + return JSValue::encode(jsUndefined()); + auto* result = createIteratorResultObject(globalObject, context->getInternalField(1), true); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(result); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.h b/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.h new file mode 100644 index 000000000000..54304ad13c2c --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamAsyncIterator.h @@ -0,0 +1,57 @@ +// JSReadableStreamAsyncIterator — the spec-native ReadableStream async iterator cell +// (readMany() stays public on the reader). NO globalThis constructor exists; its prototype +// is %ReadableStreamAsyncIteratorPrototype% (own `next` / `return`, +// [[Prototype]] = %AsyncIteratorPrototype% so `for await` finds @@asyncIterator) and +// instances are returned only by values() / [Symbol.asyncIterator](). Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include +#include + +namespace WebCore { + +class JSReadableStreamAsyncIterator final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + // Allocated only by ReadableStream.prototype.values(options) / @@asyncIterator. + static JSReadableStreamAsyncIterator* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_reader, m_ongoingPromise. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // the iterator's exclusive default reader ("iterator's reader"). + JSC::WriteBarrier m_reader; + // "ongoing promise" — chains get-the-next-iteration-result / return calls. + JSC::WriteBarrier m_ongoingPromise; + // "prevent cancel" (values({ preventCancel })) + bool m_preventCancel { false }; + // "is finished" + bool m_isFinished { false }; + +private: + JSReadableStreamAsyncIterator(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBReader.cpp b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBReader.cpp new file mode 100644 index 000000000000..0daeaec21aec --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBReader.cpp @@ -0,0 +1,455 @@ +#include "config.h" +#include "JSReadableStreamBYOBReader.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "ErrorCode.h" +#include "JSDOMBinding.h" +#include "JSDOMConvertNumbers.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadRequest.h" +#include "JSReadableByteStreamController.h" +#include "JSReadableStream.h" +#include "JSStreamsRuntime.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSStreamsRuntime; + +// The only cast of the erased stream->m_controller slot in this file: a BYOB reader can +// only be attached to a byte-controlled stream (SetUpReadableStreamBYOBReader enforces it). +static WebCore::JSReadableByteStreamController* byteControllerOf(JSReadableStream* stream) +{ + ASSERT(stream->m_controllerKind == ControllerKind::Byte); + return uncheckedDowncast(stream->m_controller.get()); +} + +// Detaches [[readIntoRequests]] before dispatch ("set to an empty list, then iterate"): once +// the requests leave the visited deque the MarkedArgumentBuffer is their only root. +static void detachReadIntoRequests(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStreamBYOBReader* reader, MarkedArgumentBuffer& out) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + { + WTF::Locker locker { reader->cellLock() }; + for (auto& request : reader->m_readIntoRequests) + out.append(request.get()); + reader->m_readIntoRequests.clear(); + } + if (out.hasOverflowed()) [[unlikely]] + throwOutOfMemoryError(globalObject, scope); +} + +// ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e) +void readableStreamBYOBReaderErrorReadIntoRequests(JSGlobalObject* globalObject, JSReadableStreamBYOBReader* reader, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + MarkedArgumentBuffer readIntoRequests; + detachReadIntoRequests(vm, globalObject, reader, readIntoRequests); + RETURN_IF_EXCEPTION(scope, void()); + for (size_t i = 0; i < readIntoRequests.size(); ++i) { + uncheckedDowncast(readIntoRequests.at(i))->errorSteps(globalObject, error); + RETURN_IF_EXCEPTION(scope, void()); + } +} + +// ReadableStreamBYOBReaderRead(reader, view, min, readIntoRequest) +void readableStreamBYOBReaderRead(JSGlobalObject* globalObject, JSReadableStreamBYOBReader* reader, JSArrayBufferView* view, uint64_t min, WebCore::JSReadIntoRequest* readIntoRequest) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = reader->m_stream.get(); + ASSERT(stream); + stream->m_disturbed = true; + if (stream->m_state == ReadableStreamState::Errored) { + JSValue storedError = stream->m_storedError.get(); + RELEASE_AND_RETURN(scope, readIntoRequest->errorSteps(globalObject, storedError ? storedError : jsUndefined())); + } + RELEASE_AND_RETURN(scope, readableByteStreamControllerPullInto(globalObject, byteControllerOf(stream), view, min, readIntoRequest)); +} + +// ReadableStreamBYOBReaderRelease(reader) +void readableStreamBYOBReaderRelease(JSGlobalObject* globalObject, JSReadableStreamBYOBReader* reader) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + readableStreamReaderGenericRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, void()); + JSObject* error = Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Releasing reader"_s); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readableStreamBYOBReaderErrorReadIntoRequests(globalObject, reader, error)); +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +// WebIDL argument conversion for read(view, options): `min` is [EnforceRange] unsigned long +// long, defaulting to 1. Throws; the promise-returning caller converts that to a rejection. +struct BYOBReadArguments { + JSC::JSArrayBufferView* view { nullptr }; + uint64_t min { 1 }; +}; +static BYOBReadArguments convertBYOBReadArguments(JSC::VM& vm, JSGlobalObject* globalObject, JSValue viewValue, JSValue options) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + BYOBReadArguments result; + result.view = dynamicDowncast(viewValue); + if (!result.view) { + throwTypeError(globalObject, scope, "ReadableStreamBYOBReader.prototype.read requires an ArrayBufferView"_s); + return result; + } + if (options.isUndefinedOrNull()) + return result; + if (!options.isObject()) { + throwTypeError(globalObject, scope, "ReadableStreamBYOBReader.prototype.read options must be an object"_s); + return result; + } + JSValue minValue = asObject(options)->get(globalObject, builtinNames(vm).minPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (minValue.isUndefined()) + return result; + result.min = convertToIntegerEnforceRange(*globalObject, minValue); + RETURN_IF_EXCEPTION(scope, result); + return result; +} + +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamBYOBReaderPrototypeFunction_cancel); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamBYOBReaderPrototypeFunction_read); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamBYOBReaderPrototypeFunction_releaseLock); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamBYOBReaderPrototypeGetter_closed); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamBYOBReaderPrototypeGetter_constructor); + +class JSReadableStreamBYOBReaderPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSReadableStreamBYOBReaderPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSReadableStreamBYOBReaderPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamBYOBReaderPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBReaderPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSReadableStreamBYOBReaderPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBReaderPrototype, JSReadableStreamBYOBReaderPrototype::Base); + +// JSReadableStreamBYOBReaderConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSReadableStreamBYOBReaderConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSReadableStreamBYOBReaderConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSReadableStreamBYOBReaderConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSReadableStreamBYOBReaderConstructor::subspaceForImpl(JSC::VM&); +template<> void JSReadableStreamBYOBReaderConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSReadableStreamBYOBReaderConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSReadableStreamBYOBReaderConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSReadableStreamBYOBReaderConstructor::s_info = { "ReadableStreamBYOBReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBReaderConstructor) }; + +template<> JSValue JSReadableStreamBYOBReaderConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSReadableStreamBYOBReaderConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSReadableStreamBYOBReaderConstructor); + +template<> GCClient::IsoSubspace* JSReadableStreamBYOBReaderConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamBYOBReaderConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamBYOBReaderConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamBYOBReaderConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamBYOBReaderConstructor = std::forward(space); }); +} + +template<> void JSReadableStreamBYOBReaderConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "ReadableStreamBYOBReader"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSReadableStreamBYOBReader::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSReadableStreamBYOBReaderConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +// new ReadableStreamBYOBReader(stream): SetUpReadableStreamBYOBReader(this, stream), which +// throws a TypeError when the stream is locked or is not a byte stream. +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSReadableStreamBYOBReaderConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + auto* stream = dynamicDowncast(callFrame->argument(0)); + if (!stream) + return throwVMTypeError(lexicalGlobalObject, scope, "ReadableStreamBYOBReader constructor requires a ReadableStream as its first argument"_s); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* reader = JSReadableStreamBYOBReader::create(vm, structure); + setUpReadableStreamBYOBReader(lexicalGlobalObject, reader, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(reader); +} +JSC_ANNOTATE_HOST_FUNCTION(JSReadableStreamBYOBReaderConstructorConstruct, JSReadableStreamBYOBReaderConstructor::construct); + +// JSReadableStreamBYOBReaderPrototype + +static const HashTableValue JSReadableStreamBYOBReaderPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamBYOBReaderPrototypeGetter_constructor, 0 } }, + { "closed"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamBYOBReaderPrototypeGetter_closed, 0 } }, + { "cancel"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamBYOBReaderPrototypeFunction_cancel, 0 } }, + { "read"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamBYOBReaderPrototypeFunction_read, 1 } }, + { "releaseLock"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamBYOBReaderPrototypeFunction_releaseLock, 0 } }, +}; + +const ClassInfo JSReadableStreamBYOBReaderPrototype::s_info = { "ReadableStreamBYOBReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBReaderPrototype) }; + +void JSReadableStreamBYOBReaderPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSReadableStreamBYOBReader::info(), JSReadableStreamBYOBReaderPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSReadableStreamBYOBReader + +const ClassInfo JSReadableStreamBYOBReader::s_info = { "ReadableStreamBYOBReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBReader) }; + +JSReadableStreamBYOBReader::JSReadableStreamBYOBReader(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSReadableStreamBYOBReader::~JSReadableStreamBYOBReader() = default; + +void JSReadableStreamBYOBReader::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSReadableStreamBYOBReader* JSReadableStreamBYOBReader::create(VM& vm, Structure* structure) +{ + auto* reader = new (NotNull, allocateCell(vm)) JSReadableStreamBYOBReader(vm, structure); + reader->finishCreation(vm); + return reader; +} + +void JSReadableStreamBYOBReader::destroy(JSCell* cell) +{ + static_cast(cell)->JSReadableStreamBYOBReader::~JSReadableStreamBYOBReader(); +} + +Structure* JSReadableStreamBYOBReader::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSReadableStreamBYOBReader::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSReadableStreamBYOBReaderPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSReadableStreamBYOBReaderPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSReadableStreamBYOBReader::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSReadableStreamBYOBReader::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSReadableStreamBYOBReader::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamBYOBReader.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamBYOBReader = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamBYOBReader.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamBYOBReader = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadableStreamBYOBReader); + +template +void JSReadableStreamBYOBReader::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_closedPromise); + WTF::Locker locker { thisObject->cellLock() }; + for (auto& request : thisObject->m_readIntoRequests) + visitor.append(request); +} + +// Prototype accessors and host functions + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamBYOBReaderPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSReadableStreamBYOBReader::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamBYOBReaderPrototypeGetter_closed, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto* reader = dynamicDowncast(JSValue::decode(thisValue)); + if (!reader) [[unlikely]] + return JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The 'closed' getter can only be used on a ReadableStreamBYOBReader"_s))); + return JSValue::encode(reader->m_closedPromise.get()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamBYOBReaderPrototypeFunction_cancel, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->thisValue()); + if (!reader) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "ReadableStreamBYOBReader.prototype.cancel can only be called on a ReadableStreamBYOBReader"_s)))); + if (!reader->m_stream) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: The reader is not attached to a stream"_s)))); + auto* promise = readableStreamReaderGenericCancel(lexicalGlobalObject, reader, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamBYOBReaderPrototypeFunction_read, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->thisValue()); + if (!reader) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "ReadableStreamBYOBReader.prototype.read can only be called on a ReadableStreamBYOBReader"_s)))); + + // A promise-returning operation turns argument-conversion failures into rejections. + BYOBReadArguments arguments; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + arguments = convertBYOBReadArguments(vm, lexicalGlobalObject, callFrame->argument(0), callFrame->argument(1)); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(lexicalGlobalObject, catchScope); + if (thrown.isEmpty()) + return {}; + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, thrown))); + } + } + JSArrayBufferView* view = arguments.view; + uint64_t minRequested = arguments.min; + + if (!view->byteLength()) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The view passed to read() must have a non-zero byteLength"_s)))); + RefPtr viewedBuffer = view->possiblySharedBuffer(); + if (!viewedBuffer || !viewedBuffer->byteLength()) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The view passed to read() is backed by a zero-length ArrayBuffer"_s)))); + if (viewedBuffer->isDetached() || view->isDetached()) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The view passed to read() is backed by a detached ArrayBuffer"_s)))); + if (!minRequested) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The 'min' option must be greater than 0"_s)))); + TypedArrayType viewType = typedArrayType(view->type()); + uint64_t minLimit = viewType == TypeDataView ? static_cast(view->byteLength()) : static_cast(view->length()); + if (minRequested > minLimit) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createRangeError(lexicalGlobalObject, "The 'min' option cannot be larger than the view passed to read()"_s)))); + if (!reader->m_stream) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: The reader is not attached to a stream"_s)))); + + auto* domGlobalObject = defaultGlobalObject(lexicalGlobalObject); + auto* runtime = JSStreamsRuntime::from(lexicalGlobalObject); + auto* promise = JSPromise::create(vm, lexicalGlobalObject->promiseStructure()); + auto* readIntoRequest = JSReadIntoRequest::create(vm, runtime->readIntoRequestStructure(domGlobalObject), ReadIntoRequestKind::Promise, promise); + readableStreamBYOBReaderRead(lexicalGlobalObject, reader, view, minRequested, readIntoRequest); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamBYOBReaderPrototypeFunction_releaseLock, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->thisValue()); + if (!reader) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStreamBYOBReader"_s); + if (!reader->m_stream) + return JSValue::encode(jsUndefined()); + readableStreamBYOBReaderRelease(lexicalGlobalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBReader.h b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBReader.h new file mode 100644 index 000000000000..e94acde9c4dc --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBReader.h @@ -0,0 +1,55 @@ +// JSReadableStreamBYOBReader — the ReadableStreamBYOBReader instance cell. +// DESTRUCTIBLE (owns the [[readIntoRequests]] Deque). +#pragma once + +#include "root.h" +#include "StreamsForward.h" +#include "JSReadableStreamReaderBase.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include + +namespace WebCore { + +class JSReadableStreamBYOBReader final : public JSReadableStreamReaderBase { +public: + using Base = JSReadableStreamReaderBase; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + // Internal allocation entry point (acquireReadableStreamBYOBReader). + static JSReadableStreamBYOBReader* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream + m_closedPromise (from the base) and + // m_readIntoRequests (a barrier container: UNDER cellLock()). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // [[readIntoRequests]] — mutated AND visited under cellLock(). + WTF::Deque, 4> m_readIntoRequests; + +private: + JSReadableStreamBYOBReader(JSC::VM&, JSC::Structure*); + ~JSReadableStreamBYOBReader(); + void finishCreation(JSC::VM&); +}; + +using JSReadableStreamBYOBReaderConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBRequest.cpp b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBRequest.cpp new file mode 100644 index 000000000000..2a37df3bc2bb --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBRequest.cpp @@ -0,0 +1,242 @@ +#include "config.h" +#include "JSReadableStreamBYOBRequest.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMConvertNumbers.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableByteStreamController.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamBYOBRequestPrototypeFunction_respond); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamBYOBRequestPrototypeFunction_respondWithNewView); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamBYOBRequestPrototypeGetter_view); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamBYOBRequestPrototypeGetter_constructor); + +class JSReadableStreamBYOBRequestPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSReadableStreamBYOBRequestPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSReadableStreamBYOBRequestPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamBYOBRequestPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBRequestPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSReadableStreamBYOBRequestPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamBYOBRequestPrototype, JSReadableStreamBYOBRequestPrototype::Base); + +// JSReadableStreamBYOBRequestConstructor = JSDOMConstructorNotConstructable<...>: +// construct/call both throw; only the prototype link and the name/length live here. + +template<> JSValue JSReadableStreamBYOBRequestConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject& globalObject); +template<> void JSReadableStreamBYOBRequestConstructor::initializeProperties(JSC::VM&, JSDOMGlobalObject&); + +template<> const ClassInfo JSReadableStreamBYOBRequestConstructor::s_info = { "ReadableStreamBYOBRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBRequestConstructor) }; + +template<> JSValue JSReadableStreamBYOBRequestConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> void JSReadableStreamBYOBRequestConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "ReadableStreamBYOBRequest"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSReadableStreamBYOBRequest::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +// JSReadableStreamBYOBRequestPrototype + +static const HashTableValue JSReadableStreamBYOBRequestPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamBYOBRequestPrototypeGetter_constructor, 0 } }, + { "view"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamBYOBRequestPrototypeGetter_view, 0 } }, + { "respond"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamBYOBRequestPrototypeFunction_respond, 1 } }, + { "respondWithNewView"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamBYOBRequestPrototypeFunction_respondWithNewView, 1 } }, +}; + +const ClassInfo JSReadableStreamBYOBRequestPrototype::s_info = { "ReadableStreamBYOBRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBRequestPrototype) }; + +void JSReadableStreamBYOBRequestPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSReadableStreamBYOBRequest::info(), JSReadableStreamBYOBRequestPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSReadableStreamBYOBRequest + +const ClassInfo JSReadableStreamBYOBRequest::s_info = { "ReadableStreamBYOBRequest"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamBYOBRequest) }; + +JSReadableStreamBYOBRequest::JSReadableStreamBYOBRequest(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSReadableStreamBYOBRequest::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSReadableStreamBYOBRequest* JSReadableStreamBYOBRequest::create(VM& vm, Structure* structure) +{ + auto* request = new (NotNull, allocateCell(vm)) JSReadableStreamBYOBRequest(vm, structure); + request->finishCreation(vm); + return request; +} + +Structure* JSReadableStreamBYOBRequest::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSReadableStreamBYOBRequest::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSReadableStreamBYOBRequestPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSReadableStreamBYOBRequestPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSReadableStreamBYOBRequest::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSReadableStreamBYOBRequest::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSReadableStreamBYOBRequest::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamBYOBRequest.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamBYOBRequest = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamBYOBRequest.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamBYOBRequest = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadableStreamBYOBRequest); + +template +void JSReadableStreamBYOBRequest::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_controller); + visitor.append(thisObject->m_view); +} + +// Prototype host functions + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamBYOBRequestPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSReadableStreamBYOBRequest::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamBYOBRequestPrototypeGetter_view, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* request = dynamicDowncast(JSValue::decode(thisValue)); + if (!request) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStreamBYOBRequest"_s); + JSArrayBufferView* view = request->m_view.get(); + return JSValue::encode(view ? JSValue(view) : jsNull()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamBYOBRequestPrototypeFunction_respond, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* request = dynamicDowncast(callFrame->thisValue()); + if (!request) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStreamBYOBRequest"_s); + + uint64_t bytesWritten = convertToIntegerEnforceRange(*lexicalGlobalObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + + if (!request->m_controller) + return Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: This BYOB request has been invalidated"_s); + ASSERT(request->m_view); + if (request->m_view->isDetached()) + return throwVMTypeError(lexicalGlobalObject, scope, "Cannot respond to a ReadableStreamBYOBRequest whose view has a detached ArrayBuffer"_s); + ASSERT(request->m_view->byteLength() > 0); + + readableByteStreamControllerRespond(lexicalGlobalObject, request->m_controller.get(), bytesWritten); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamBYOBRequestPrototypeFunction_respondWithNewView, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* request = dynamicDowncast(callFrame->thisValue()); + if (!request) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStreamBYOBRequest"_s); + + auto* view = dynamicDowncast(callFrame->argument(0)); + if (!view) + return Bun::ERR::INVALID_ARG_INSTANCE(scope, lexicalGlobalObject, "view"_s, "Buffer, TypedArray, or DataView"_s, callFrame->argument(0)); + + if (!request->m_controller) + return Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: This BYOB request has been invalidated"_s); + if (view->isDetached()) + return throwVMTypeError(lexicalGlobalObject, scope, "Cannot respond with a view whose ArrayBuffer is detached"_s); + + readableByteStreamControllerRespondWithNewView(lexicalGlobalObject, request->m_controller.get(), view); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBRequest.h b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBRequest.h new file mode 100644 index 000000000000..90d73bcde55d --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamBYOBRequest.h @@ -0,0 +1,56 @@ +// JSReadableStreamBYOBRequest — the ReadableStreamBYOBRequest instance cell. +// Not user-constructible. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMConstructorNotConstructable.h" +#include "JSDOMGlobalObject.h" +#include + +namespace WebCore { + +class JSReadableStreamBYOBRequest final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + // Internal allocation entry point (readableByteStreamControllerGetBYOBRequest / PullInto). + static JSReadableStreamBYOBRequest* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_controller, m_view. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // [[controller]] — null after ReadableByteStreamControllerInvalidateBYOBRequest. + JSC::WriteBarrier m_controller; + // [[view]] — a typed array view over the head pull-into descriptor, or null after + // invalidation. + JSC::WriteBarrier m_view; + +private: + JSReadableStreamBYOBRequest(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +// Construct throws `TypeError: Illegal constructor`; the constructor object is still +// installed on globalThis so instanceof / .prototype work. +using JSReadableStreamBYOBRequestConstructor = JSDOMConstructorNotConstructable; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.cpp b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.cpp new file mode 100644 index 000000000000..a5446d58ef70 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.cpp @@ -0,0 +1,632 @@ +#include "config.h" +#include "JSReadableStreamDefaultController.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadRequest.h" +#include "JSReadableStream.h" +#include "JSStreamTeeState.h" +#include "JSStreamsRuntime.h" +#include "JSTransformStream.h" +#include "WebStreamsInternals.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +// WebIDL "invoke a callback function" with a Promise return type: an abrupt completion is +// converted into a rejected promise (a completion-record conversion), never a synchronous throw. +static JSC::JSPromise* invokePromiseReturningMethod(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* method, JSC::JSValue thisValue, const JSC::MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSValue result; + JSC::JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = JSC::getCallData(method); + ASSERT(callData.type != JSC::CallData::Type::None); + result = JSC::call(globalObject, method, callData, thisValue, args); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (result.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseResolvedWith(globalObject, result)); +} + +// The [[pullAlgorithm]] dispatch. ByteTeeBranch is byte-controller-only and CrossRealm sources +// are never created (transferable streams are unimplemented); the switch is total over SourceKind. +static JSC::JSPromise* performDefaultControllerPullAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_algorithms.kind) { + case SourceKind::JavaScript: { + JSC::JSObject* pullMethod = controller->m_algorithms.method1.get(); + if (!pullMethod) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + JSC::MarkedArgumentBuffer args; + args.append(controller); + if (args.hasOverflowed()) [[unlikely]] { + JSC::throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + StreamAsyncContextScope asyncContextScope(globalObject, controller->m_stream.get()); + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, pullMethod, controller->m_algorithms.underlyingObject.get(), args)); + } + case SourceKind::Nothing: + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + case SourceKind::Transform: + RELEASE_AND_RETURN(scope, transformStreamDefaultSourcePullAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()))); + case SourceKind::TeeBranch: + RELEASE_AND_RETURN(scope, defaultTeePullAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()), controller->m_algorithms.teeBranchIndex)); + case SourceKind::FromIterable: + RELEASE_AND_RETURN(scope, fromIterablePullAlgorithm(globalObject, controller)); + case SourceKind::Native: + RELEASE_AND_RETURN(scope, nativeSourcePull(globalObject, controller)); + case SourceKind::ByteTeeBranch: + case SourceKind::CrossRealm: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +// The [[cancelAlgorithm]] dispatch. Same reachable kind set as the pull dispatch. +static JSC::JSPromise* performDefaultControllerCancelAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller, JSC::JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_algorithms.kind) { + case SourceKind::JavaScript: { + JSC::JSObject* cancelMethod = controller->m_algorithms.method2.get(); + if (!cancelMethod) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + JSC::MarkedArgumentBuffer args; + args.append(reason); + if (args.hasOverflowed()) [[unlikely]] { + JSC::throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + StreamAsyncContextScope asyncContextScope(globalObject, controller->m_stream.get()); + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, cancelMethod, controller->m_algorithms.underlyingObject.get(), args)); + } + case SourceKind::Nothing: + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + case SourceKind::Transform: + RELEASE_AND_RETURN(scope, transformStreamDefaultSourceCancelAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()), reason)); + case SourceKind::TeeBranch: + RELEASE_AND_RETURN(scope, defaultTeeCancelAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()), controller->m_algorithms.teeBranchIndex, reason)); + case SourceKind::FromIterable: + RELEASE_AND_RETURN(scope, fromIterableCancelAlgorithm(globalObject, controller, reason)); + case SourceKind::Native: + RELEASE_AND_RETURN(scope, nativeSourceCancel(globalObject, controller, reason)); + case SourceKind::ByteTeeBranch: + case SourceKind::CrossRealm: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamDefaultControllerConstructorGetter); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamDefaultControllerPrototypeGetter_desiredSize); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamDefaultControllerPrototypeFunction_close); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamDefaultControllerPrototypeFunction_enqueue); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamDefaultControllerPrototypeFunction_error); + +class JSReadableStreamDefaultControllerPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSReadableStreamDefaultControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSReadableStreamDefaultControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamDefaultControllerPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultControllerPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSReadableStreamDefaultControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultControllerPrototype, JSReadableStreamDefaultControllerPrototype::Base); + +static const HashTableValue JSReadableStreamDefaultControllerPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamDefaultControllerConstructorGetter, 0 } }, + { "desiredSize"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor, NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamDefaultControllerPrototypeGetter_desiredSize, 0 } }, + { "close"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamDefaultControllerPrototypeFunction_close, 0 } }, + { "enqueue"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamDefaultControllerPrototypeFunction_enqueue, 0 } }, + { "error"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamDefaultControllerPrototypeFunction_error, 0 } }, +}; + +const ClassInfo JSReadableStreamDefaultControllerPrototype::s_info = { "ReadableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultControllerPrototype) }; + +void JSReadableStreamDefaultControllerPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSReadableStreamDefaultController::info(), JSReadableStreamDefaultControllerPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +template<> const ClassInfo JSReadableStreamDefaultControllerConstructor::s_info = { "ReadableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultControllerConstructor) }; + +template<> JSValue JSReadableStreamDefaultControllerConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + UNUSED_PARAM(vm); + return globalObject.functionPrototype(); +} + +template<> void JSReadableStreamDefaultControllerConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "ReadableStreamDefaultController"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSReadableStreamDefaultController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +const ClassInfo JSReadableStreamDefaultController::s_info = { "ReadableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultController) }; + +JSReadableStreamDefaultController::JSReadableStreamDefaultController(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSReadableStreamDefaultController::~JSReadableStreamDefaultController() = default; + +void JSReadableStreamDefaultController::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSReadableStreamDefaultController* JSReadableStreamDefaultController::create(VM& vm, Structure* structure) +{ + JSReadableStreamDefaultController* controller = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamDefaultController(vm, structure); + controller->finishCreation(vm); + return controller; +} + +void JSReadableStreamDefaultController::destroy(JSCell* cell) +{ + static_cast(cell)->JSReadableStreamDefaultController::~JSReadableStreamDefaultController(); +} + +Structure* JSReadableStreamDefaultController::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSReadableStreamDefaultController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSReadableStreamDefaultControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSReadableStreamDefaultControllerPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSReadableStreamDefaultController::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSReadableStreamDefaultController::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSReadableStreamDefaultController::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamDefaultController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamDefaultController = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamDefaultController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamDefaultController = std::forward(space); }); +} + +template +void JSReadableStreamDefaultController::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_algorithms.underlyingObject); + visitor.append(thisObject->m_algorithms.method1); + visitor.append(thisObject->m_algorithms.method2); + visitor.append(thisObject->m_algorithms.algorithmContext); + visitor.append(thisObject->m_strategySizeAlgorithm); + WTF::Locker locker { thisObject->cellLock() }; + thisObject->m_queue.visit(locker, visitor); +} + +DEFINE_VISIT_CHILDREN(JSReadableStreamDefaultController); + +// [[CancelSteps]](reason) +JSPromise* JSReadableStreamDefaultController::cancelSteps(JSGlobalObject* globalObject, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + { + WTF::Locker locker { cellLock() }; + m_queue.resetQueue(locker); + } + JSPromise* result = performDefaultControllerCancelAlgorithm(vm, globalObject, this, reason); + RETURN_IF_EXCEPTION(scope, nullptr); + readableStreamDefaultControllerClearAlgorithms(this); + return result; +} + +JSValue JSReadableStreamDefaultController::dequeueChunkForRead(JSGlobalObject* globalObject) +{ + auto scope = DECLARE_THROW_SCOPE(getVM(globalObject)); + ASSERT(!m_queue.isEmpty()); + JSValue chunk; + { + WTF::Locker locker { cellLock() }; + chunk = m_queue.dequeueValue(locker); + } + if (m_closeRequested && m_queue.isEmpty()) { + readableStreamDefaultControllerClearAlgorithms(this); + readableStreamClose(globalObject, m_stream.get()); + RETURN_IF_EXCEPTION(scope, {}); + } else { + readableStreamDefaultControllerCallPullIfNeeded(globalObject, this); + RETURN_IF_EXCEPTION(scope, {}); + } + return chunk; +} + +// [[PullSteps]](readRequest) +void JSReadableStreamDefaultController::pullSteps(JSGlobalObject* globalObject, JSReadRequest* readRequest) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSReadableStream* stream = m_stream.get(); + if (!m_queue.isEmpty()) { + JSValue chunk = dequeueChunkForRead(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readRequest->chunkSteps(globalObject, chunk)); + } + readableStreamAddReadRequest(vm, stream, readRequest); + RELEASE_AND_RETURN(scope, readableStreamDefaultControllerCallPullIfNeeded(globalObject, this)); +} + +// [[ReleaseSteps]]() +void JSReadableStreamDefaultController::releaseSteps() +{ +} + +// The shared start/pull reaction handlers ([reaction-convention]; context at argument(1)). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSDefaultControllerStartFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + controller->m_started = true; + ASSERT(!controller->m_pulling); + ASSERT(!controller->m_pullAgain); + readableStreamDefaultControllerCallPullIfNeeded(globalObject, controller); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSDefaultControllerStartRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + readableStreamDefaultControllerError(globalObject, controller, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSDefaultControllerPullFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + controller->m_pulling = false; + if (controller->m_pullAgain) { + controller->m_pullAgain = false; + readableStreamDefaultControllerCallPullIfNeeded(globalObject, controller); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onRSDefaultControllerPullRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + readableStreamDefaultControllerError(globalObject, controller, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// Prototype accessors & methods. + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamDefaultControllerConstructorGetter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(globalObject, scope); + return JSValue::encode(JSReadableStreamDefaultController::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamDefaultControllerPrototypeGetter_desiredSize, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableStreamDefaultController"_s); + std::optional desiredSize = readableStreamDefaultControllerGetDesiredSize(thisObject); + if (!desiredSize) + return JSValue::encode(jsNull()); + return JSValue::encode(jsNumber(*desiredSize)); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamDefaultControllerPrototypeFunction_close, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableStreamDefaultController"_s); + if (!readableStreamDefaultControllerCanCloseOrEnqueue(thisObject)) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Controller is already closed"_s); + readableStreamDefaultControllerClose(globalObject, thisObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamDefaultControllerPrototypeFunction_enqueue, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableStreamDefaultController"_s); + if (!readableStreamDefaultControllerCanCloseOrEnqueue(thisObject)) + return Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Controller is already closed"_s); + readableStreamDefaultControllerEnqueue(globalObject, thisObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamDefaultControllerPrototypeFunction_error, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "ReadableStreamDefaultController"_s); + readableStreamDefaultControllerError(globalObject, thisObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using namespace WebCore; + +void readableStreamDefaultControllerCallPullIfNeeded(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!readableStreamDefaultControllerShouldCallPull(controller)) + return; + if (controller->m_pulling) { + controller->m_pullAgain = true; + return; + } + ASSERT(!controller->m_pullAgain); + controller->m_pulling = true; + JSPromise* pullPromise = performDefaultControllerPullAlgorithm(vm, globalObject, controller); + RETURN_IF_EXCEPTION(scope, void()); + auto* runtime = JSStreamsRuntime::from(globalObject); + pullPromise->performPromiseThenWithContext(vm, globalObject, runtime->onRSDefaultControllerPullFulfilled(), runtime->onRSDefaultControllerPullRejected(), jsUndefined(), controller); +} + +bool readableStreamDefaultControllerShouldCallPull(JSReadableStreamDefaultController* controller) +{ + JSReadableStream* stream = controller->m_stream.get(); + if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) + return false; + if (!controller->m_started) + return false; + if (readableStreamHasDefaultReader(stream) && readableStreamGetNumReadRequests(stream) > 0) + return true; + std::optional desiredSize = readableStreamDefaultControllerGetDesiredSize(controller); + ASSERT(desiredSize); + return *desiredSize > 0; +} + +void readableStreamDefaultControllerClearAlgorithms(JSReadableStreamDefaultController* controller) +{ + controller->m_algorithms.kind = SourceKind::Nothing; + controller->m_algorithms.underlyingObject.clear(); + controller->m_algorithms.method1.clear(); + controller->m_algorithms.method2.clear(); + controller->m_algorithms.algorithmContext.clear(); + controller->m_strategySizeAlgorithm.clear(); +} + +void readableStreamDefaultControllerClose(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) + return; + JSReadableStream* stream = controller->m_stream.get(); + controller->m_closeRequested = true; + if (controller->m_queue.isEmpty()) { + readableStreamDefaultControllerClearAlgorithms(controller); + RELEASE_AND_RETURN(scope, readableStreamClose(globalObject, stream)); + } +} + +void readableStreamDefaultControllerEnqueue(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!readableStreamDefaultControllerCanCloseOrEnqueue(controller)) + return; + JSReadableStream* stream = controller->m_stream.get(); + if (readableStreamHasDefaultReader(stream) && readableStreamGetNumReadRequests(stream) > 0) { + readableStreamFulfillReadRequest(globalObject, stream, chunk, false); + RETURN_IF_EXCEPTION(scope, void()); + } else { + double chunkSize = 1; + if (JSObject* sizeAlgorithm = controller->m_strategySizeAlgorithm.get()) { + JSValue chunkSizeValue; + { + // The strategy size() call is interpreted as a completion record: an abrupt + // completion errors the controller and is then rethrown. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = JSC::getCallData(sizeAlgorithm); + ASSERT(callData.type != JSC::CallData::Type::None); + JSC::MarkedArgumentBuffer args; + args.append(chunk); + if (args.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return; + } + chunkSizeValue = JSC::call(globalObject, sizeAlgorithm, callData, jsUndefined(), args); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) [[unlikely]] + return; + readableStreamDefaultControllerError(globalObject, controller, thrown); + RETURN_IF_EXCEPTION(scope, void()); + throwException(globalObject, scope, thrown); + return; + } + } + // Web IDL: the size callback returns an `unrestricted double` — a full ToNumber + // (can run user JS); a throw from it is the same abrupt completion as size() throwing. + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + chunkSize = chunkSizeValue.toNumber(globalObject); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) [[unlikely]] + return; + readableStreamDefaultControllerError(globalObject, controller, thrown); + RETURN_IF_EXCEPTION(scope, void()); + throwException(globalObject, scope, thrown); + return; + } + } + } + // EnqueueValueWithSize is interpreted as a completion record: same recovery. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + controller->m_queue.enqueueValueWithSize(globalObject, controller, chunk, chunkSize); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) [[unlikely]] + return; + readableStreamDefaultControllerError(globalObject, controller, thrown); + RETURN_IF_EXCEPTION(scope, void()); + throwException(globalObject, scope, thrown); + return; + } + } + RELEASE_AND_RETURN(scope, readableStreamDefaultControllerCallPullIfNeeded(globalObject, controller)); +} + +void readableStreamDefaultControllerError(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSReadableStream* stream = controller->m_stream.get(); + if (stream->m_state != ReadableStreamState::Readable) + return; + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.resetQueue(locker); + } + readableStreamDefaultControllerClearAlgorithms(controller); + RELEASE_AND_RETURN(scope, readableStreamError(globalObject, stream, error)); +} + +std::optional readableStreamDefaultControllerGetDesiredSize(JSReadableStreamDefaultController* controller) +{ + switch (controller->m_stream->m_state) { + case ReadableStreamState::Errored: + return std::nullopt; + case ReadableStreamState::Closed: + return 0; + case ReadableStreamState::Readable: + break; + } + return controller->m_strategyHWM - controller->m_queue.totalSize(); +} + +bool readableStreamDefaultControllerHasBackpressure(JSReadableStreamDefaultController* controller) +{ + return !readableStreamDefaultControllerShouldCallPull(controller); +} + +bool readableStreamDefaultControllerCanCloseOrEnqueue(JSReadableStreamDefaultController* controller) +{ + return !controller->m_closeRequested && controller->m_stream->m_state == ReadableStreamState::Readable; +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.h b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.h new file mode 100644 index 000000000000..eafa9b627f4c --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultController.h @@ -0,0 +1,93 @@ +// JSReadableStreamDefaultController — the ReadableStreamDefaultController instance cell. +// Not user-constructible. The algorithm slots are the kind tag + method/context members of +// SourceAlgorithmSlots (no stored closures). DESTRUCTIBLE (owns the [[queue]] StreamQueue). +#pragma once + +#include "root.h" +#include "StreamsForward.h" +#include "StreamQueue.h" + +#include "JSDOMConstructorNotConstructable.h" +#include "JSDOMGlobalObject.h" +#include + +namespace WebCore { + +class JSReadableStreamDefaultController final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + // Internal allocation entry point (setUpReadableStreamDefaultController* / createReadableStream). + static JSReadableStreamDefaultController* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream, every barrier inside m_algorithms, + // m_strategySizeAlgorithm, and m_queue (a barrier container: via + // m_queue.visit(locker, visitor) inside ONE `Locker { cellLock() }` scope taken by THIS + // visitChildrenImpl — cellLock() is non-recursive; see StreamQueue.h). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Spec internal slots + + // [[queue]] + [[queueTotalSize]] + Bun::WebStreams::StreamQueue m_queue; + // [[stream]] + JSC::WriteBarrier m_stream; + // [[strategyHWM]] + double m_strategyHWM { 1 }; + // [[started]] + bool m_started { false }; + // [[pulling]] + bool m_pulling { false }; + // [[pullAgain]] + bool m_pullAgain { false }; + // [[closeRequested]] + bool m_closeRequested { false }; + + // The algorithm machinery — replaces [[pullAlgorithm]] and [[cancelAlgorithm]]; the + // start algorithm is never stored. See SourceAlgorithmSlots (StreamQueue.h). + Bun::WebStreams::SourceAlgorithmSlots m_algorithms; + + // [[strategySizeAlgorithm]] — null ⇒ the default `() => 1`. + JSC::WriteBarrier m_strategySizeAlgorithm; + + // Internal methods + + // [[CancelSteps]](reason) — userJS: YES (performs the user cancel algorithm). + JSC::JSPromise* cancelSteps(JSC::JSGlobalObject*, JSC::JSValue reason); + // [[PullSteps]](readRequest) — userJS: YES (may run the user pull algorithm). + void pullSteps(JSC::JSGlobalObject*, JSReadRequest*); + // The queue-hit half of [[PullSteps]]: dequeue + the close-or-pull bookkeeping. + // Caller checks !m_queue.isEmpty(). Returns the chunk (empty on exception). + JSC::JSValue dequeueChunkForRead(JSC::JSGlobalObject*); + // [[ReleaseSteps]]() — spec: "Return." (no-op). userJS: no. + void releaseSteps(); + +private: + JSReadableStreamDefaultController(JSC::VM&, JSC::Structure*); + ~JSReadableStreamDefaultController(); + void finishCreation(JSC::VM&); +}; + +// Construct throws `TypeError: Illegal constructor`; the constructor object is still +// installed on globalThis so instanceof / .prototype work. +using JSReadableStreamDefaultControllerConstructor = JSDOMConstructorNotConstructable; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultReader.cpp b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultReader.cpp new file mode 100644 index 000000000000..c9d14c84a400 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultReader.cpp @@ -0,0 +1,771 @@ +#include "config.h" +#include "JSReadableStreamDefaultReader.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "ErrorCode.h" +#include "JSDOMBinding.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSDirectStreamController.h" +#include "JSReadRequest.h" +#include "JSReadableByteStreamController.h" +#include "JSReadableStream.h" +#include "JSReadableStreamDefaultController.h" +#include "JSStreamsRuntime.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSStreamsRuntime; + +// The only cast of the erased stream->m_controller slot in this file; every switch is TOTAL. +static WebCore::JSReadableStreamDefaultController* defaultControllerOf(JSReadableStream* stream) +{ + ASSERT(stream->m_controllerKind == ControllerKind::Default); + return uncheckedDowncast(stream->m_controller.get()); +} + +static WebCore::JSReadableByteStreamController* byteControllerOf(JSReadableStream* stream) +{ + ASSERT(stream->m_controllerKind == ControllerKind::Byte); + return uncheckedDowncast(stream->m_controller.get()); +} + +// Detaches [[readRequests]] before dispatch ("set to an empty list, then iterate"): once the +// requests leave the visited deque the MarkedArgumentBuffer is their only root. +static void detachReadRequests(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader, MarkedArgumentBuffer& out) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + { + WTF::Locker locker { reader->cellLock() }; + for (auto& request : reader->m_readRequests) + out.append(request.get()); + reader->m_readRequests.clear(); + } + if (out.hasOverflowed()) [[unlikely]] + throwOutOfMemoryError(globalObject, scope); +} + +// ReadableStreamDefaultReaderErrorReadRequests(reader, e) +void readableStreamDefaultReaderErrorReadRequests(JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + MarkedArgumentBuffer readRequests; + detachReadRequests(vm, globalObject, reader, readRequests); + RETURN_IF_EXCEPTION(scope, void()); + for (size_t i = 0; i < readRequests.size(); ++i) { + uncheckedDowncast(readRequests.at(i))->errorSteps(globalObject, error); + RETURN_IF_EXCEPTION(scope, void()); + } +} + +// ReadableStreamDefaultReaderRead(reader, readRequest) +// A read on a readable, default-controller stream with a queued chunk and no pending read +// requests needs no JSReadRequest: dequeue synchronously. Returns an empty JSValue when the +// fast path does not apply (or on exception; callers RETURN_IF_EXCEPTION). +JSValue readableStreamDefaultReaderTryReadFromQueue(JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader) +{ + auto scope = DECLARE_THROW_SCOPE(getVM(globalObject)); + auto* stream = reader->m_stream.get(); + if (!stream || stream->m_state != ReadableStreamState::Readable || stream->m_controllerKind != ControllerKind::Default || !reader->m_readRequests.isEmpty()) + return {}; + auto* controller = uncheckedDowncast(stream->m_controller.get()); + if (controller->m_queue.isEmpty()) + return {}; + stream->m_disturbed = true; + RELEASE_AND_RETURN(scope, controller->dequeueChunkForRead(globalObject)); +} + +void readableStreamDefaultReaderRead(JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader, WebCore::JSReadRequest* readRequest) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = reader->m_stream.get(); + ASSERT(stream); + stream->m_disturbed = true; + if (stream->m_state == ReadableStreamState::Closed) + RELEASE_AND_RETURN(scope, readRequest->closeSteps(globalObject)); + if (stream->m_state == ReadableStreamState::Errored) { + JSValue storedError = stream->m_storedError.get(); + RELEASE_AND_RETURN(scope, readRequest->errorSteps(globalObject, storedError ? storedError : jsUndefined())); + } + + switch (stream->m_controllerKind) { + case ControllerKind::Default: + RELEASE_AND_RETURN(scope, defaultControllerOf(stream)->pullSteps(globalObject, readRequest)); + case ControllerKind::Byte: + RELEASE_AND_RETURN(scope, byteControllerOf(stream)->pullSteps(globalObject, readRequest)); + case ControllerKind::None: + // No controller yet (an unmaterialized Bun stream): the read stays pending. + readableStreamAddReadRequest(vm, stream, readRequest); + return; + case ControllerKind::Direct: { + auto* controller = uncheckedDowncast(stream->m_controller.get()); + // The direct pump allocates and settles its own head-of-line promise; a + // promise-backed read adopts it instead of waiting in [[readRequests]]. + if (readRequest->kind() == ReadRequestKind::Promise) { + auto* readPromise = uncheckedDowncast(readRequest->m_context.get()); + JSValue pulled = controller->onPull(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + if (!pulled.isObject()) { + // The pump refused (already closed / re-entrant pull): report done. + JSObject* doneResult = createIteratorResultObject(globalObject, jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, readPromise, doneResult)); + } + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, readPromise, pulled)); + } + // Other read-request kinds wait in [[readRequests]]; the pump's unobserved + // head-of-line promise for this read is dropped so delivery reaches the request. + readableStreamAddReadRequest(vm, stream, readRequest); + bool hadPendingRead = !!controller->m_pendingRead; + JSValue pulled = controller->onPull(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + if (!hadPendingRead && controller->m_pendingRead && pulled == JSValue(controller->m_pendingRead.get())) + controller->m_pendingRead.clear(); + return; + } + case ControllerKind::NativeSink: { + // A native-sink-locked stream cannot acquire a default reader. + ASSERT_NOT_REACHED(); + JSObject* error = Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: This ReadableStream is locked to a native sink"_s); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readRequest->errorSteps(globalObject, error)); + } + } +} + +// ReadableStreamDefaultReaderRelease(reader) +void readableStreamDefaultReaderRelease(JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + readableStreamReaderGenericRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, void()); + JSObject* error = Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Releasing reader"_s); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, readableStreamDefaultReaderErrorReadRequests(globalObject, reader, error)); +} + +// The `{value, size, done}` readMany result shape. +static JSObject* createReadManyResult(JSC::VM& vm, JSGlobalObject* globalObject, JSValue value, double size, bool done) +{ + auto* structure = JSStreamsRuntime::from(globalObject)->readManyResultStructure(defaultGlobalObject(globalObject)); + auto* result = constructEmptyObject(vm, structure); + result->putDirectOffset(vm, 0, value); + result->putDirectOffset(vm, 1, jsNumber(size)); + result->putDirectOffset(vm, 2, jsBoolean(done)); + return result; +} + +// Drains the whole queue (after an optional already-read head chunk) into a fresh array, +// runs the close-if-requested / pull-if-needed step, resets the queue, and returns the +// `{value, size, done: false}` result. `size` is the PRE-drain [[queueTotalSize]], and the +// pull decision runs against it (the drain leaves [[queueTotalSize]] untouched until the +// final ResetQueue), matching the readMany contract. +// Appends every queued chunk to `into` at `base`, runs the close-if-requested / +// pull-if-needed step, resets the queue, and returns the PRE-drain [[queueTotalSize]] +// (the pull decision runs against it, matching the readMany contract). +static double drainQueueEntriesInto(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStream* stream, JSArray* into, unsigned base) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + bool isByte = stream->m_controllerKind == ControllerKind::Byte; + ASSERT(isByte || stream->m_controllerKind == ControllerKind::Default); + auto* defaultController = isByte ? nullptr : defaultControllerOf(stream); + auto* byteController = isByte ? byteControllerOf(stream) : nullptr; + + double size = isByte ? byteController->m_queue.totalSize() : defaultController->m_queue.totalSize(); + size_t queueLength = isByte ? byteController->m_queue.size() : defaultController->m_queue.size(); + // [[queueTotalSize]] is deliberately NOT decremented while draining (see above). + for (unsigned i = 0; i < queueLength; ++i) { + JSValue chunk; + if (isByte) { + RefPtr buffer; + size_t byteOffset = 0; + size_t byteLength = 0; + { + WTF::Locker locker { byteController->cellLock() }; + auto& entry = byteController->m_queue.first(); + buffer = WTF::move(entry.buffer); + byteOffset = entry.byteOffset; + byteLength = entry.byteLength; + byteController->m_queue.removeFirst(locker); + } + bool resizable = buffer->isResizableOrGrowableShared(); + chunk = JSUint8Array::create(globalObject, globalObject->typedArrayStructure(TypeUint8, resizable), WTF::move(buffer), byteOffset, byteLength); + RETURN_IF_EXCEPTION(scope, size); + } else { + WTF::Locker locker { defaultController->cellLock() }; + auto& entry = defaultController->m_queue.first(); + chunk = entry.value.get(); + defaultController->m_queue.removeFirst(locker); + } + into->putDirectIndex(globalObject, base + i, chunk); + RETURN_IF_EXCEPTION(scope, size); + } + + if (stream->m_state != ReadableStreamState::Closed) { + bool closeRequested = isByte ? byteController->m_closeRequested : defaultController->m_closeRequested; + if (closeRequested) + readableStreamCloseIfPossible(globalObject, stream); + else if (isByte) + readableByteStreamControllerCallPullIfNeeded(globalObject, byteController); + else + readableStreamDefaultControllerCallPullIfNeeded(globalObject, defaultController); + RETURN_IF_EXCEPTION(scope, size); + } + if (isByte) { + WTF::Locker locker { byteController->cellLock() }; + byteController->m_queue.resetQueue(locker); + } else { + WTF::Locker locker { defaultController->cellLock() }; + defaultController->m_queue.resetQueue(locker); + } + return size; +} + +static JSValue drainQueueForReadMany(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStream* stream, JSValue headChunk) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + bool isByte = stream->m_controllerKind == ControllerKind::Byte; + size_t queueLength = isByte ? byteControllerOf(stream)->m_queue.size() : defaultControllerOf(stream)->m_queue.size(); + unsigned base = headChunk ? 1 : 0; + auto* values = constructEmptyArray(globalObject, nullptr, base + queueLength); + RETURN_IF_EXCEPTION(scope, {}); + if (headChunk) { + values->putDirectIndex(globalObject, 0, headChunk); + RETURN_IF_EXCEPTION(scope, {}); + } + double size = drainQueueEntriesInto(vm, globalObject, stream, values, base); + RETURN_IF_EXCEPTION(scope, {}); + return createReadManyResult(vm, globalObject, values, size, false); +} + +// The buffered-consumer pump step: bulk-appends everything queued to `chunks`; when the +// queue is empty and the stream is still readable, issues ONE spec read and hands its +// promise back via `pendingRead`. Throws the stored error on an errored stream. +ConsumerFillStep readableStreamDefaultReaderFillFromQueue(JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader, JSArray* chunks, JSPromise** pendingRead) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + *pendingRead = nullptr; + auto* stream = reader->m_stream.get(); + if (!stream) [[unlikely]] { + throwException(globalObject, scope, Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: The reader is not attached to a stream"_s)); + return ConsumerFillStep::Done; + } + stream->m_disturbed = true; + while (true) { + if (stream->m_state == ReadableStreamState::Errored) { + JSValue storedError = stream->m_storedError.get(); + throwException(globalObject, scope, storedError ? storedError : jsUndefined()); + return ConsumerFillStep::Done; + } + bool isByte = stream->m_controllerKind == ControllerKind::Byte; + bool queueEmpty = isByte ? byteControllerOf(stream)->m_queue.isEmpty() : defaultControllerOf(stream)->m_queue.isEmpty(); + if (!queueEmpty) { + drainQueueEntriesInto(vm, globalObject, stream, chunks, chunks->length()); + RETURN_IF_EXCEPTION(scope, ConsumerFillStep::Done); + continue; + } + if (stream->m_state == ReadableStreamState::Closed) + return ConsumerFillStep::Done; + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* readRequest = WebCore::JSReadRequest::create(vm, runtime->readRequestStructure(defaultGlobalObject(globalObject)), ReadRequestKind::Promise, promise); + if (isByte) + byteControllerOf(stream)->pullSteps(globalObject, readRequest); + else + defaultControllerOf(stream)->pullSteps(globalObject, readRequest); + RETURN_IF_EXCEPTION(scope, ConsumerFillStep::Done); + *pendingRead = promise; + return ConsumerFillStep::Pending; + } +} + +static JSValue emptyDoneReadManyResult(JSC::VM& vm, JSGlobalObject* globalObject) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* values = constructEmptyArray(globalObject, nullptr, 0); + RETURN_IF_EXCEPTION(scope, {}); + return createReadManyResult(vm, globalObject, values, 0, true); +} + +// The onReadManyPullFulfilled continuation: `result` is the `{value, done}` the spec pull +// resolved, prepended to whatever that pull enqueued. +static JSValue readManyAfterPull(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader, JSValue result) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (!result.isObject()) [[unlikely]] + RELEASE_AND_RETURN(scope, emptyDoneReadManyResult(vm, globalObject)); + JSValue chunk = asObject(result)->get(globalObject, vm.propertyNames->value); + RETURN_IF_EXCEPTION(scope, {}); + JSValue done = asObject(result)->get(globalObject, vm.propertyNames->done); + RETURN_IF_EXCEPTION(scope, {}); + if (done.toBoolean(globalObject)) { + auto* values = constructEmptyArray(globalObject, nullptr, chunk.toBoolean(globalObject) ? 1 : 0); + RETURN_IF_EXCEPTION(scope, {}); + if (values->length()) { + values->putDirectIndex(globalObject, 0, chunk); + RETURN_IF_EXCEPTION(scope, {}); + } + return createReadManyResult(vm, globalObject, values, 0, true); + } + // The reader can have been released by the user pull that produced the chunk. + auto* stream = reader->m_stream.get(); + if (!stream || (stream->m_controllerKind != ControllerKind::Default && stream->m_controllerKind != ControllerKind::Byte)) [[unlikely]] { + auto* values = constructEmptyArray(globalObject, nullptr, 1); + RETURN_IF_EXCEPTION(scope, {}); + values->putDirectIndex(globalObject, 0, chunk); + RETURN_IF_EXCEPTION(scope, {}); + return createReadManyResult(vm, globalObject, values, 1, false); + } + RELEASE_AND_RETURN(scope, drainQueueForReadMany(vm, globalObject, stream, chunk)); +} + +// The onReadManyDirectPullFulfilled continuation: maps the direct pump's `{done, value}` +// into the readMany result shape. +static JSValue readManyAfterDirectPull(JSC::VM& vm, JSGlobalObject* globalObject, JSValue result) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (!result.isObject()) [[unlikely]] + RELEASE_AND_RETURN(scope, emptyDoneReadManyResult(vm, globalObject)); + JSValue chunk = asObject(result)->get(globalObject, vm.propertyNames->value); + RETURN_IF_EXCEPTION(scope, {}); + JSValue done = asObject(result)->get(globalObject, vm.propertyNames->done); + RETURN_IF_EXCEPTION(scope, {}); + bool isDone = done.toBoolean(globalObject); + bool hasChunk = isDone ? chunk.toBoolean(globalObject) : true; + auto* values = constructEmptyArray(globalObject, nullptr, hasChunk ? 1 : 0); + RETURN_IF_EXCEPTION(scope, {}); + if (hasChunk) { + values->putDirectIndex(globalObject, 0, chunk); + RETURN_IF_EXCEPTION(scope, {}); + } + return createReadManyResult(vm, globalObject, values, isDone ? 0 : 1, !!isDone); +} + +// Bun `reader.readMany()`: `{value, size, done}` synchronously, or a promise of one. +JSValue readableStreamDefaultReaderReadMany(JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = reader->m_stream.get(); + if (!stream) { + throwException(globalObject, scope, Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: The reader is not attached to a stream"_s)); + return {}; + } + stream->m_disturbed = true; + if (stream->m_state == ReadableStreamState::Errored) { + JSValue storedError = stream->m_storedError.get(); + throwException(globalObject, scope, storedError ? storedError : jsUndefined()); + return {}; + } + + auto* runtime = JSStreamsRuntime::from(globalObject); + switch (stream->m_controllerKind) { + case ControllerKind::Direct: { + if (stream->m_state == ReadableStreamState::Closed) + break; + auto* controller = uncheckedDowncast(stream->m_controller.get()); + JSValue pulled = controller->onPull(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto* pulledPromise = dynamicDowncast(pulled); + if (!pulledPromise) + break; + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + pulledPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReadManyDirectPullFulfilled(), jsUndefined(), result, reader); + RETURN_IF_EXCEPTION(scope, {}); + return result; + } + case ControllerKind::None: + if (stream->m_state == ReadableStreamState::Closed) + break; + throwException(globalObject, scope, Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: This ReadableStream has no controller"_s)); + return {}; + case ControllerKind::NativeSink: + ASSERT_NOT_REACHED(); + throwException(globalObject, scope, Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: This ReadableStream is locked to a native sink"_s)); + return {}; + case ControllerKind::Default: + case ControllerKind::Byte: { + bool isByte = stream->m_controllerKind == ControllerKind::Byte; + bool queueIsEmpty = isByte ? byteControllerOf(stream)->m_queue.isEmpty() : defaultControllerOf(stream)->m_queue.isEmpty(); + if (!queueIsEmpty) + RELEASE_AND_RETURN(scope, drainQueueForReadMany(vm, globalObject, stream, JSValue())); + if (stream->m_state == ReadableStreamState::Closed) + break; + // Queue empty, readable: one spec pull, continued by onReadManyPullFulfilled. + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* readRequest = WebCore::JSReadRequest::create(vm, runtime->readRequestStructure(defaultGlobalObject(globalObject)), ReadRequestKind::Promise, promise); + if (isByte) + byteControllerOf(stream)->pullSteps(globalObject, readRequest); + else + defaultControllerOf(stream)->pullSteps(globalObject, readRequest); + RETURN_IF_EXCEPTION(scope, {}); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + promise->performPromiseThenWithContext(vm, globalObject, runtime->onReadManyPullFulfilled(), jsUndefined(), result, reader); + RETURN_IF_EXCEPTION(scope, {}); + return result; + } + } + RELEASE_AND_RETURN(scope, emptyDoneReadManyResult(vm, globalObject)); +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_cancel); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_read); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_readMany); +static JSC_DECLARE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_releaseLock); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamDefaultReaderPrototypeGetter_closed); +static JSC_DECLARE_CUSTOM_GETTER(jsReadableStreamDefaultReaderPrototypeGetter_constructor); + +class JSReadableStreamDefaultReaderPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSReadableStreamDefaultReaderPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSReadableStreamDefaultReaderPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSReadableStreamDefaultReaderPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultReaderPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSReadableStreamDefaultReaderPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSReadableStreamDefaultReaderPrototype, JSReadableStreamDefaultReaderPrototype::Base); + +// JSReadableStreamDefaultReaderConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSReadableStreamDefaultReaderConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSReadableStreamDefaultReaderConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSReadableStreamDefaultReaderConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSReadableStreamDefaultReaderConstructor::subspaceForImpl(JSC::VM&); +template<> void JSReadableStreamDefaultReaderConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSReadableStreamDefaultReaderConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSReadableStreamDefaultReaderConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSReadableStreamDefaultReaderConstructor::s_info = { "ReadableStreamDefaultReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultReaderConstructor) }; + +template<> JSValue JSReadableStreamDefaultReaderConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSReadableStreamDefaultReaderConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSReadableStreamDefaultReaderConstructor); + +template<> GCClient::IsoSubspace* JSReadableStreamDefaultReaderConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamDefaultReaderConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamDefaultReaderConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamDefaultReaderConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamDefaultReaderConstructor = std::forward(space); }); +} + +template<> void JSReadableStreamDefaultReaderConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "ReadableStreamDefaultReader"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSReadableStreamDefaultReader::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSReadableStreamDefaultReaderConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +// new ReadableStreamDefaultReader(stream): SetUpReadableStreamDefaultReader(this, stream). +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSReadableStreamDefaultReaderConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + auto* stream = dynamicDowncast(callFrame->argument(0)); + if (!stream) + return throwVMTypeError(lexicalGlobalObject, scope, "ReadableStreamDefaultReader constructor requires a ReadableStream as its first argument"_s); + + // Same as getReader(): a lazy native/direct stream materializes before it is locked. + stream->materializeIfNeeded(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto* structure = structureForNewTarget(constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* reader = JSReadableStreamDefaultReader::create(vm, structure); + setUpReadableStreamDefaultReader(lexicalGlobalObject, reader, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(reader); +} +JSC_ANNOTATE_HOST_FUNCTION(JSReadableStreamDefaultReaderConstructorConstruct, JSReadableStreamDefaultReaderConstructor::construct); + +// JSReadableStreamDefaultReaderPrototype + +static const HashTableValue JSReadableStreamDefaultReaderPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamDefaultReaderPrototypeGetter_constructor, 0 } }, + { "closed"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsReadableStreamDefaultReaderPrototypeGetter_closed, 0 } }, + { "cancel"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamDefaultReaderPrototypeFunction_cancel, 0 } }, + { "read"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamDefaultReaderPrototypeFunction_read, 0 } }, + { "readMany"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamDefaultReaderPrototypeFunction_readMany, 0 } }, + { "releaseLock"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsReadableStreamDefaultReaderPrototypeFunction_releaseLock, 0 } }, +}; + +const ClassInfo JSReadableStreamDefaultReaderPrototype::s_info = { "ReadableStreamDefaultReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultReaderPrototype) }; + +void JSReadableStreamDefaultReaderPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSReadableStreamDefaultReader::info(), JSReadableStreamDefaultReaderPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSReadableStreamDefaultReader + +const ClassInfo JSReadableStreamDefaultReader::s_info = { "ReadableStreamDefaultReader"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSReadableStreamDefaultReader) }; + +JSReadableStreamDefaultReader::JSReadableStreamDefaultReader(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSReadableStreamDefaultReader::~JSReadableStreamDefaultReader() = default; + +void JSReadableStreamDefaultReader::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSReadableStreamDefaultReader* JSReadableStreamDefaultReader::create(VM& vm, Structure* structure) +{ + auto* reader = new (NotNull, allocateCell(vm)) JSReadableStreamDefaultReader(vm, structure); + reader->finishCreation(vm); + return reader; +} + +void JSReadableStreamDefaultReader::destroy(JSCell* cell) +{ + static_cast(cell)->JSReadableStreamDefaultReader::~JSReadableStreamDefaultReader(); +} + +Structure* JSReadableStreamDefaultReader::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSReadableStreamDefaultReader::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSReadableStreamDefaultReaderPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSReadableStreamDefaultReaderPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSReadableStreamDefaultReader::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSReadableStreamDefaultReader::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSReadableStreamDefaultReader::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForReadableStreamDefaultReader.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForReadableStreamDefaultReader = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForReadableStreamDefaultReader.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForReadableStreamDefaultReader = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSReadableStreamDefaultReader); + +template +void JSReadableStreamDefaultReader::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_closedPromise); + visitor.append(thisObject->m_pipeOperation); + WTF::Locker locker { thisObject->cellLock() }; + for (auto& request : thisObject->m_readRequests) + visitor.append(request); +} + +// Prototype accessors and host functions + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamDefaultReaderPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSReadableStreamDefaultReader::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsReadableStreamDefaultReaderPrototypeGetter_closed, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto* reader = dynamicDowncast(JSValue::decode(thisValue)); + if (!reader) [[unlikely]] + return JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The 'closed' getter can only be used on a ReadableStreamDefaultReader"_s))); + return JSValue::encode(reader->m_closedPromise.get()); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_cancel, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->thisValue()); + if (!reader) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "ReadableStreamDefaultReader.prototype.cancel can only be called on a ReadableStreamDefaultReader"_s)))); + if (!reader->m_stream) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: The reader is not attached to a stream"_s)))); + auto* promise = readableStreamReaderGenericCancel(lexicalGlobalObject, reader, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_read, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->thisValue()); + if (!reader) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "ReadableStreamDefaultReader.prototype.read can only be called on a ReadableStreamDefaultReader"_s)))); + if (!reader->m_stream) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: The reader is not attached to a stream"_s)))); + + // Queued chunk and nothing waiting: resolve synchronously with no read request. + JSValue chunk = Bun::WebStreams::readableStreamDefaultReaderTryReadFromQueue(lexicalGlobalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + if (chunk) { + JSObject* result = createIteratorResultObject(lexicalGlobalObject, chunk, false); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(promiseResolvedWith(lexicalGlobalObject, result))); + } + auto* domGlobalObject = defaultGlobalObject(lexicalGlobalObject); + auto* runtime = JSStreamsRuntime::from(lexicalGlobalObject); + auto* promise = JSPromise::create(vm, lexicalGlobalObject->promiseStructure()); + auto* readRequest = JSReadRequest::create(vm, runtime->readRequestStructure(domGlobalObject), ReadRequestKind::Promise, promise); + readableStreamDefaultReaderRead(lexicalGlobalObject, reader, readRequest); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_readMany, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->thisValue()); + if (!reader) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope, "ReadableStreamDefaultReader.readMany() should not be called directly"_s); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamDefaultReaderReadMany(lexicalGlobalObject, reader))); +} + +JSC_DEFINE_HOST_FUNCTION(jsReadableStreamDefaultReaderPrototypeFunction_releaseLock, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->thisValue()); + if (!reader) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "ReadableStreamDefaultReader"_s); + if (!reader->m_stream) + return JSValue::encode(jsUndefined()); + readableStreamDefaultReaderRelease(lexicalGlobalObject, reader); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// [reaction-convention] handlers owned by this file (context at argument(1) = the reader). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadManyPullFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* reader = dynamicDowncast(callFrame->argument(1)); + if (!reader) [[unlikely]] + return JSValue::encode(jsUndefined()); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readManyAfterPull(vm, globalObject, reader, callFrame->argument(0)))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReadManyDirectPullFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + RELEASE_AND_RETURN(scope, JSValue::encode(Bun::WebStreams::readManyAfterDirectPull(vm, globalObject, callFrame->argument(0)))); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultReader.h b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultReader.h new file mode 100644 index 000000000000..abd74f3e4940 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamDefaultReader.h @@ -0,0 +1,61 @@ +// JSReadableStreamDefaultReader — the ReadableStreamDefaultReader instance cell. +// DESTRUCTIBLE (owns the [[readRequests]] Deque). +#pragma once + +#include "root.h" +#include "StreamsForward.h" +#include "JSReadableStreamReaderBase.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include + +namespace WebCore { + +class JSReadableStreamDefaultReader final : public JSReadableStreamReaderBase { +public: + using Base = JSReadableStreamReaderBase; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + // Internal allocation entry point (acquireReadableStreamDefaultReader). + static JSReadableStreamDefaultReader* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream + m_closedPromise (from the base), + // m_pipeOperation, and m_readRequests (a barrier container: UNDER cellLock()). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // [[readRequests]] — mutated AND visited under cellLock(). + WTF::Deque, 4> m_readRequests; + + // The reader→operation liveness back-edge, set when a pipe (JSStreamPipeToOperation) or + // a Bun pump (JSReadStreamIntoSinkOperation / JSResumableSinkPumpOperation) acquires + // this reader, cleared on release/finalize. ERASED on purpose: one operation per reader + // by construction. Visited. + JSC::WriteBarrier m_pipeOperation; + +private: + JSReadableStreamDefaultReader(JSC::VM&, JSC::Structure*); + ~JSReadableStreamDefaultReader(); + void finishCreation(JSC::VM&); +}; + +using JSReadableStreamDefaultReaderConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamIntoArrayOperation.h b/src/jsc/bindings/webcore/streams/JSReadableStreamIntoArrayOperation.h new file mode 100644 index 000000000000..64c222c61710 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamIntoArrayOperation.h @@ -0,0 +1,51 @@ +// JSReadableStreamIntoArrayOperation — the queue-backed array pump's persistent state: +// the reader it holds, the chunk array it accumulates into, and the result promise it +// settles. One dedicated cell (not nested InternalFieldTuples) so the three fields are +// named, visited, and read back without double unwrapping. +// Internal cell: no prototype, no constructor, never exposed to JS. +// Non-destructible: WriteBarrier members only. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSReadableStreamIntoArrayOperation final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSReadableStreamIntoArrayOperation* create(JSC::VM&, JSC::Structure*, JSReadableStreamDefaultReader*, JSC::JSArray* chunks, JSC::JSPromise* result); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_reader, m_chunks, m_result. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The default reader the pump acquired; released when the pump settles. + JSC::WriteBarrier m_reader; + // Every chunk read so far, in order. + JSC::WriteBarrier m_chunks; + // The promise readableStreamIntoArray returned. + JSC::WriteBarrier m_result; + +private: + JSReadableStreamIntoArrayOperation(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&, JSReadableStreamDefaultReader*, JSC::JSArray*, JSC::JSPromise*); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamReaderBase.cpp b/src/jsc/bindings/webcore/streams/JSReadableStreamReaderBase.cpp new file mode 100644 index 000000000000..25ae80f75d6c --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamReaderBase.cpp @@ -0,0 +1,14 @@ +#include "config.h" +#include "JSReadableStreamReaderBase.h" + +#include "JSReadableStreamBYOBReader.h" +#include + +namespace WebCore { + +bool JSReadableStreamReaderBase::isBYOB() const +{ + return classInfo() == JSReadableStreamBYOBReader::info(); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSReadableStreamReaderBase.h b/src/jsc/bindings/webcore/streams/JSReadableStreamReaderBase.h new file mode 100644 index 000000000000..17ab4758be40 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSReadableStreamReaderBase.h @@ -0,0 +1,43 @@ +// JSReadableStreamReaderBase — the shared, NON-polymorphic C++ base of the two reader +// classes, holding the `ReadableStreamGenericReader` mixin slots. It has no ClassInfo of its +// own and NO C++ `virtual` anywhere; the three `ReadableStreamReaderGeneric*` abstract ops +// take a pointer to this type. +// +// Destructible base: both concrete readers own a WTF::Deque, and the iso-subspace machinery +// statically requires destructible classes to derive from JSC::JSDestructibleObject. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSReadableStreamReaderBase : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + + // NOT visited here (no visitChildrenImpl on the base — it has no ClassInfo). EACH + // concrete subclass's visitChildrenImpl MUST append m_stream and m_closedPromise. + + // [[stream]] (ReadableStreamGenericReader mixin) — null = released / not attached. + JSC::WriteBarrier m_stream; + // [[closedPromise]] (mixin) — spec-required at construction; NOT lazy. + JSC::WriteBarrier m_closedPromise; + + // Discriminates the two concrete readers without a vtable and without a jsDynamicCast: + // compares classInfo() against JSReadableStreamBYOBReader::info(). + // Defined in JSReadableStreamBYOBReader.cpp. + bool isBYOB() const; + +protected: + JSReadableStreamReaderBase(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSResumableSinkPumpOperation.h b/src/jsc/bindings/webcore/streams/JSResumableSinkPumpOperation.h new file mode 100644 index 000000000000..b2fc144e9536 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSResumableSinkPumpOperation.h @@ -0,0 +1,55 @@ +// JSResumableSinkPumpOperation — the assignStreamIntoResumableSink pump's state cell. Its +// drain/cancel callables are [bound-convention] JSBoundFunctions over JSStreamsRuntime +// handlers with THIS cell bound (they are stored on the native ResumableSink, so they must +// be GC-visited callables). +// ROOTING: the acquired reader's visited m_pipeOperation back-edge points HERE. +// Internal cell: no prototype, no constructor. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include + +namespace WebCore { + +class JSResumableSinkPumpOperation final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSResumableSinkPumpOperation* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit all four barriers: m_stream, m_sink, m_reader, m_error. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + JSC::WriteBarrier m_stream; + // the native ResumableSink wrapper (start/setHandlers/write/end). + JSC::WriteBarrier m_sink; + // the acquired default reader (carries the m_pipeOperation back-edge to this cell). + JSC::WriteBarrier m_reader; + // the sticky error, if any (gated by m_closed / emptiness). + JSC::WriteBarrier m_error; + // a drain loop is running (re-entrancy guard). + bool m_reading { false }; + // terminal. + bool m_closed { false }; + +private: + JSResumableSinkPumpOperation(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSStreamAlgorithmContexts.cpp b/src/jsc/bindings/webcore/streams/JSStreamAlgorithmContexts.cpp new file mode 100644 index 000000000000..a79f2536a010 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamAlgorithmContexts.cpp @@ -0,0 +1,64 @@ +#include "config.h" +#include "JSStreamAlgorithmContexts.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +const ClassInfo JSStreamFromIterableContext::s_info = { "StreamFromIterableContext"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSStreamFromIterableContext) }; + +JSStreamFromIterableContext::JSStreamFromIterableContext(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSStreamFromIterableContext::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSStreamFromIterableContext* JSStreamFromIterableContext::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSStreamFromIterableContext(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSStreamFromIterableContext::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSStreamFromIterableContext::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForStreamFromIterableContext.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForStreamFromIterableContext = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForStreamFromIterableContext.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForStreamFromIterableContext = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSStreamFromIterableContext); + +template +void JSStreamFromIterableContext::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_iterator); + visitor.append(thisObject->m_nextMethod); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSStreamAlgorithmContexts.h b/src/jsc/bindings/webcore/streams/JSStreamAlgorithmContexts.h new file mode 100644 index 000000000000..bea709895e91 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamAlgorithmContexts.h @@ -0,0 +1,52 @@ +// JSStreamAlgorithmContexts — the small FromIterable iterator-record context cell and +// NOTHING else. 2-value reaction contexts use JSC's existing InternalFieldTuple +// (globalObject->internalFieldTupleStructure()); NO bespoke pair classes. +// Internal cell: no prototype, no constructor. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include + +namespace WebCore { + +// The context (algorithmContext) of a SourceKind::FromIterable default controller: the +// spec's Iterator Record {[[Iterator]], [[NextMethod]], [[Done]]} from +// GetIterator(asyncIterable, async). +class JSStreamFromIterableContext final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSStreamFromIterableContext* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_iterator, m_nextMethod. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Iterator Record.[[Iterator]] — the async iterator object. + JSC::WriteBarrier m_iterator; + // Iterator Record.[[NextMethod]] — captured ONCE by GetIterator; later mutation of + // `iterator.next` is never observed. + JSC::WriteBarrier m_nextMethod; + // Iterator Record.[[Done]] + bool m_done { false }; + +private: + JSStreamFromIterableContext(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSStreamPipeToOperation.cpp b/src/jsc/bindings/webcore/streams/JSStreamPipeToOperation.cpp new file mode 100644 index 000000000000..75cb797e35f2 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamPipeToOperation.cpp @@ -0,0 +1,592 @@ +#include "config.h" +#include "JSStreamPipeToOperation.h" + +#include "AbortSignal.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSAbortAlgorithm.h" +#include "JSAbortSignal.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSReadRequest.h" +#include "JSReadableStream.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSStreamsRuntime.h" +#include "JSWritableStream.h" +#include "JSWritableStreamDefaultWriter.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static void pipeToLoopStep(JSGlobalObject*, JSStreamPipeToOperation*); +static void performPipeShutdownAction(JSGlobalObject*, JSStreamPipeToOperation*); + +const ClassInfo JSStreamPipeToOperation::s_info = { "StreamPipeToOperation"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSStreamPipeToOperation) }; + +JSStreamPipeToOperation::JSStreamPipeToOperation(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSStreamPipeToOperation::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSStreamPipeToOperation* JSStreamPipeToOperation::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSStreamPipeToOperation(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSStreamPipeToOperation::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSStreamPipeToOperation::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForStreamPipeToOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForStreamPipeToOperation = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForStreamPipeToOperation.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForStreamPipeToOperation = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSStreamPipeToOperation); + +template +void JSStreamPipeToOperation::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_source); + visitor.append(thisObject->m_destination); + visitor.append(thisObject->m_reader); + visitor.append(thisObject->m_writer); + visitor.append(thisObject->m_signal); + visitor.append(thisObject->m_promise); + visitor.append(thisObject->m_currentWrite); + visitor.append(thisObject->m_shutdownActionPromise); + visitor.append(thisObject->m_shutdownError); +} + +static JSValue pipeShutdownError(JSStreamPipeToOperation* op) +{ + if (!op->m_hasShutdownError) + return jsUndefined(); + JSValue error = op->m_shutdownError.get(); + return error ? error : jsUndefined(); +} + +static void registerPipeReaction(JSGlobalObject* globalObject, JSPromise* promise, JSFunction* onFulfilled, JSFunction* onRejected, JSObject* context) +{ + auto& vm = getVM(globalObject); + // With no result capability, JSC requires BOTH handlers to be callable: a non-callable + // handler routes the settlement through PromiseResolveWithoutHandlerJob, which does an + // unconditional [[Get]] on the (here undefined) capability. Substitute the shared no-op. + auto* runtime = JSStreamsRuntime::from(globalObject); + promise->performPromiseThenWithContext(vm, globalObject, onFulfilled ? onFulfilled : runtime->onReturnUndefined(), onRejected ? onRejected : runtime->onReturnUndefined(), jsUndefined(), context); +} + +// [reaction-convention] deferral: runs handler(value, context) as its own microtask, +// carrying the current async context, without allocating a promise. +static void queuePipeReactionJob(JSC::VM& vm, JSGlobalObject* globalObject, JSFunction* handler, JSValue value, JSValue context) +{ + JSValue asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0); + if (asyncContext.isEmpty()) + asyncContext = jsUndefined(); + QueuedTask task { nullptr, InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, handler, asyncContext, value, context }; + vm.queueMicrotask(WTF::move(task)); +} + +// One tick of the read/write loop: backpressure first, then at most one read. +static void pipeToLoopStep(JSGlobalObject* globalObject, JSStreamPipeToOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (op->m_shuttingDown || op->m_finalized || op->m_readInFlight) + return; + auto* writer = op->m_writer.get(); + auto desiredSize = writableStreamDefaultWriterGetDesiredSize(writer); + // null: the destination is erroring/errored; the backward error observer shuts the pipe down. + if (!desiredSize) + return; + auto* runtime = JSStreamsRuntime::from(globalObject); + if (*desiredSize <= 0) { + registerPipeReaction(globalObject, writer->m_readyPromise.get(), runtime->onPipeWriterReadyFulfilled(), nullptr, op); + return; + } + auto* readRequest = JSReadRequest::create(vm, runtime->readRequestStructure(defaultGlobalObject(globalObject)), ReadRequestKind::PipeTo, op); + op->m_readInFlight = true; + readableStreamDefaultReaderRead(globalObject, op->m_reader.get(), readRequest); + RETURN_IF_EXCEPTION(scope, ); +} + +// The pipe's signal abort algorithm: START both actions back-to-back, then wait for ALL of +// them. The wait-for-all latch is `op->m_pendingShutdownActions`; the last settlement +// proceeds, and the FIRST rejection finalizes with its reason (finalize is idempotent). +static void startPipeAbortBothActions(JSC::VM& vm, JSGlobalObject* globalObject, JSStreamPipeToOperation* op, JSValue error) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSPromise* actions[2] = { nullptr, nullptr }; + unsigned actionCount = 0; + if (!op->m_preventAbort) { + auto* destination = op->m_destination.get(); + if (destination->m_state == WritableStreamState::Writable) + actions[actionCount] = writableStreamAbort(globalObject, destination, error); + else + actions[actionCount] = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + actionCount++; + } + if (!op->m_preventCancel) { + // The per-action state guard is evaluated when the action is invoked (after the abort). + auto* source = op->m_source.get(); + if (source->m_state == ReadableStreamState::Readable) + actions[actionCount] = readableStreamCancel(globalObject, source, error); + else + actions[actionCount] = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + actionCount++; + } + if (!actionCount) + RELEASE_AND_RETURN(scope, op->finalize(globalObject)); + op->m_shutdownActionPromise.set(vm, op, actions[0]); + op->m_pendingShutdownActions = static_cast(actionCount); + auto* runtime = JSStreamsRuntime::from(globalObject); + for (unsigned i = 0; i < actionCount; i++) + registerPipeReaction(globalObject, actions[i], runtime->onPipeShutdownActionFulfilled(), runtime->onPipeShutdownActionRejected(), op); +} + +// spec "shutdown with an action" step 4: perform the pending action exactly once. +static void performPipeShutdownAction(JSGlobalObject* globalObject, JSStreamPipeToOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (op->m_finalized || op->m_shutdownActionPromise) + return; + JSValue error = pipeShutdownError(op); + JSPromise* actionPromise = nullptr; + switch (op->m_pendingShutdownAction) { + case JSStreamPipeToOperation::ShutdownAction::None: + RELEASE_AND_RETURN(scope, op->finalize(globalObject)); + case JSStreamPipeToOperation::ShutdownAction::AbortDestination: + actionPromise = writableStreamAbort(globalObject, op->m_destination.get(), error); + break; + case JSStreamPipeToOperation::ShutdownAction::CancelSource: + actionPromise = readableStreamCancel(globalObject, op->m_source.get(), error); + break; + case JSStreamPipeToOperation::ShutdownAction::CloseDestinationWithErrorPropagation: + actionPromise = writableStreamDefaultWriterCloseWithErrorPropagation(globalObject, op->m_writer.get()); + break; + case JSStreamPipeToOperation::ShutdownAction::AbortBoth: + RELEASE_AND_RETURN(scope, startPipeAbortBothActions(vm, globalObject, op, error)); + } + RETURN_IF_EXCEPTION(scope, ); + op->m_shutdownActionPromise.set(vm, op, actionPromise); + auto* runtime = JSStreamsRuntime::from(globalObject); + registerPipeReaction(globalObject, actionPromise, runtime->onPipeShutdownActionFulfilled(), runtime->onPipeShutdownActionRejected(), op); +} + +void JSStreamPipeToOperation::checkErrorsMustBePropagatedForward(JSGlobalObject* globalObject) +{ + auto* source = m_source.get(); + if (source->m_state != ReadableStreamState::Errored) + return; + JSValue storedError = source->m_storedError.get(); + if (!storedError) + storedError = jsUndefined(); + if (!m_preventAbort) + shutdownWithAction(globalObject, ShutdownAction::AbortDestination, storedError, true); + else + shutdown(globalObject, storedError, true); +} + +void JSStreamPipeToOperation::checkErrorsMustBePropagatedBackward(JSGlobalObject* globalObject) +{ + auto* destination = m_destination.get(); + if (destination->m_state != WritableStreamState::Errored) + return; + JSValue storedError = destination->m_storedError.get(); + if (!storedError) + storedError = jsUndefined(); + if (!m_preventCancel) + shutdownWithAction(globalObject, ShutdownAction::CancelSource, storedError, true); + else + shutdown(globalObject, storedError, true); +} + +void JSStreamPipeToOperation::checkClosingMustBePropagatedForward(JSGlobalObject* globalObject) +{ + if (m_source->m_state != ReadableStreamState::Closed) + return; + if (!m_preventClose) + shutdownWithAction(globalObject, ShutdownAction::CloseDestinationWithErrorPropagation, jsUndefined(), false); + else + shutdown(globalObject, jsUndefined(), false); +} + +void JSStreamPipeToOperation::checkClosingMustBePropagatedBackward(JSGlobalObject* globalObject) +{ + auto* destination = m_destination.get(); + if (!writableStreamCloseQueuedOrInFlight(destination) && destination->m_state != WritableStreamState::Closed) + return; + JSValue destClosed = createTypeError(globalObject, "The destination WritableStream closed before all of the data could be piped to it"_s); + if (!m_preventCancel) + shutdownWithAction(globalObject, ShutdownAction::CancelSource, destClosed, true); + else + shutdown(globalObject, destClosed, true); +} + +void JSStreamPipeToOperation::shutdownWithAction(JSGlobalObject* globalObject, ShutdownAction action, JSValue error, bool hasError) +{ + if (m_shuttingDown) + return; + auto& vm = getVM(globalObject); + m_shuttingDown = true; + m_pendingShutdownAction = action; + if (hasError) { + m_hasShutdownError = true; + m_shutdownError.set(vm, this, error); + } + auto* destination = m_destination.get(); + if (destination->m_state == WritableStreamState::Writable && !writableStreamCloseQueuedOrInFlight(destination)) { + if (auto* currentWrite = m_currentWrite.get(); currentWrite && currentWrite->status() == JSPromise::Status::Pending) { + onWritesFinishedForShutdown(globalObject); + return; + } + // Step 3.2's write-drain wait is ALWAYS a reaction ("In parallel"): with no pending + // write, defer so no shutdown effect is observable inside the pipeTo() call. + queuePipeReactionJob(vm, globalObject, JSStreamsRuntime::from(globalObject)->onPipeWritesFinishedForShutdown(), jsUndefined(), this); + return; + } + performPipeShutdownAction(globalObject, this); +} + +void JSStreamPipeToOperation::shutdown(JSGlobalObject* globalObject, JSValue error, bool hasError) +{ + shutdownWithAction(globalObject, ShutdownAction::None, error, hasError); +} + +void JSStreamPipeToOperation::finalize(JSGlobalObject* globalObject) +{ + if (m_finalized) + return; + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + m_finalized = true; + auto* writer = m_writer.get(); + auto* reader = m_reader.get(); + // Unconditional obligations first (back-edges, abort-algorithm removal, the promise and + // error to settle with) so a throwing release cannot skip them. + writer->m_pipeOperation.clear(); + reader->m_pipeOperation.clear(); + if (m_abortAlgorithmId) { + auto& signal = downcast(m_signal.get())->wrapped(); + AbortSignal::removeAbortAlgorithmFromSignal(signal, m_abortAlgorithmId); + m_abortAlgorithmId = 0; + } + auto* promise = m_promise.get(); + bool hasShutdownError = m_hasShutdownError; + JSValue shutdownError = pipeShutdownError(this); + writableStreamDefaultWriterRelease(globalObject, writer); + RETURN_IF_EXCEPTION(scope, ); + readableStreamDefaultReaderRelease(globalObject, reader); + RETURN_IF_EXCEPTION(scope, ); + if (hasShutdownError) + RELEASE_AND_RETURN(scope, rejectPromise(globalObject, promise, shutdownError)); + RELEASE_AND_RETURN(scope, resolvePromise(globalObject, promise, jsUndefined())); +} + +void JSStreamPipeToOperation::onSourceClosedFulfilled(JSGlobalObject* globalObject) +{ + if (m_finalized) + return; + checkClosingMustBePropagatedForward(globalObject); +} + +void JSStreamPipeToOperation::onSourceClosedRejected(JSGlobalObject* globalObject, JSValue) +{ + if (m_finalized) + return; + checkErrorsMustBePropagatedForward(globalObject); +} + +void JSStreamPipeToOperation::onDestClosedFulfilled(JSGlobalObject* globalObject) +{ + if (m_finalized) + return; + checkClosingMustBePropagatedBackward(globalObject); +} + +void JSStreamPipeToOperation::onDestClosedRejected(JSGlobalObject* globalObject, JSValue) +{ + if (m_finalized) + return; + checkErrorsMustBePropagatedBackward(globalObject); +} + +void JSStreamPipeToOperation::onWriterReadyFulfilled(JSGlobalObject* globalObject) +{ + pipeToLoopStep(globalObject, this); +} + +// Reacting to every write promise is the point (no spurious unhandledRejection); the +// loop and shutdown are driven by the read requests and onWritesFinishedForShutdown. +void JSStreamPipeToOperation::onWriteSettled(JSGlobalObject*) +{ +} + +// "Wait until every chunk that has been read has been written": re-checks the CURRENT +// write each time (a chunk read before shutdown may start one more write meanwhile). +void JSStreamPipeToOperation::onWritesFinishedForShutdown(JSGlobalObject* globalObject) +{ + if (m_finalized) + return; + if (auto* currentWrite = m_currentWrite.get(); currentWrite && currentWrite->status() == JSPromise::Status::Pending) { + auto* handler = JSStreamsRuntime::from(globalObject)->onPipeWritesFinishedForShutdown(); + registerPipeReaction(globalObject, currentWrite, handler, handler, this); + return; + } + performPipeShutdownAction(globalObject, this); +} + +void JSStreamPipeToOperation::onShutdownActionFulfilled(JSGlobalObject* globalObject) +{ + if (m_finalized) + return; + finalize(globalObject); +} + +void JSStreamPipeToOperation::onShutdownActionRejected(JSGlobalObject* globalObject, JSValue error) +{ + if (m_finalized) + return; + auto& vm = getVM(globalObject); + m_hasShutdownError = true; + m_shutdownError.set(vm, this, error); + finalize(globalObject); +} + +void JSStreamPipeToOperation::onSignalAbort(JSGlobalObject* globalObject, JSValue reason) +{ + if (m_finalized) + return; + shutdownWithAction(globalObject, ShutdownAction::AbortBoth, reason, true); +} + +#define WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE(name, method) \ + JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_##name, (JSGlobalObject * globalObject, CallFrame * callFrame)) \ + { \ + auto& vm = getVM(globalObject); \ + auto scope = DECLARE_THROW_SCOPE(vm); \ + JSValue contextValue = callFrame->argument(1); \ + auto* op = dynamicDowncast(contextValue); \ + if (!op) [[unlikely]] \ + return JSValue::encode(jsUndefined()); \ + op->method(globalObject); \ + RETURN_IF_EXCEPTION(scope, {}); \ + return JSValue::encode(jsUndefined()); \ + } +#define WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE_WITH_VALUE(name, method) \ + JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_##name, (JSGlobalObject * globalObject, CallFrame * callFrame)) \ + { \ + auto& vm = getVM(globalObject); \ + auto scope = DECLARE_THROW_SCOPE(vm); \ + JSValue contextValue = callFrame->argument(1); \ + auto* op = dynamicDowncast(contextValue); \ + if (!op) [[unlikely]] \ + return JSValue::encode(jsUndefined()); \ + op->method(globalObject, callFrame->argument(0)); \ + RETURN_IF_EXCEPTION(scope, {}); \ + return JSValue::encode(jsUndefined()); \ + } + +WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE(onPipeSourceClosedFulfilled, onSourceClosedFulfilled) +WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE_WITH_VALUE(onPipeSourceClosedRejected, onSourceClosedRejected) +WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE(onPipeDestClosedFulfilled, onDestClosedFulfilled) +WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE_WITH_VALUE(onPipeDestClosedRejected, onDestClosedRejected) +WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE(onPipeWriterReadyFulfilled, onWriterReadyFulfilled) +WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE(onPipeWriteSettled, onWriteSettled) +WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE(onPipeWritesFinishedForShutdown, onWritesFinishedForShutdown) + +#undef WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE +#undef WEB_STREAMS_DEFINE_PIPE_REACTION_TRAMPOLINE_WITH_VALUE + +// [reaction-convention] the deferred sink write. context = InternalFieldTuple{op, chunk}; +// the result promise it was registered with (op->m_currentWrite) adopts the write promise. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onPipeChunkDeferredWrite, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = dynamicDowncast(callFrame->argument(1)); + if (!context) [[unlikely]] + return JSValue::encode(jsUndefined()); + auto* op = dynamicDowncast(context->getInternalField(0)); + if (!op) [[unlikely]] + return JSValue::encode(jsUndefined()); + if (op->m_finalized) + return JSValue::encode(jsUndefined()); + auto* writePromise = writableStreamDefaultWriterWrite(globalObject, op->m_writer.get(), context->getInternalField(1)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(writePromise); +} + +// [reaction-convention] shutdown-action settlement. The context is the op cell; the +// AbortBoth wait-for-all is the op's m_pendingShutdownActions counter. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onPipeShutdownActionFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = dynamicDowncast(callFrame->argument(1)); + if (!op) [[unlikely]] + return JSValue::encode(jsUndefined()); + if (op->m_pendingShutdownActions > 1) { + op->m_pendingShutdownActions--; + return JSValue::encode(jsUndefined()); + } + op->m_pendingShutdownActions = 0; + op->onShutdownActionFulfilled(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// The wait-for-all rejects immediately with the FIRST rejection's reason; finalize is +// idempotent, so the other action's later settlement is a no-op. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onPipeShutdownActionRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* op = dynamicDowncast(callFrame->argument(1)); + if (!op) [[unlikely]] + return JSValue::encode(jsUndefined()); + op->m_pendingShutdownActions = 0; + op->onShutdownActionRejected(globalObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// [bound-convention]: (pipeOpCell, reason). +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_boundPipeAbortAlgorithm, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue contextValue = callFrame->argument(0); + auto* op = dynamicDowncast(contextValue); + if (!op) [[unlikely]] + return JSValue::encode(jsUndefined()); + op->onSignalAbort(globalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +void startPipeToOperation(JSGlobalObject* globalObject, JSStreamPipeToOperation* op) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + + if (JSObject* signalObject = op->m_signal.get()) { + auto& signal = downcast(signalObject)->wrapped(); + if (signal.aborted()) { + JSValue reason = signal.jsReason(*globalObject); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, op->onSignalAbort(globalObject, reason)); + } + MarkedArgumentBuffer boundArguments; + boundArguments.append(op); + ASSERT(!boundArguments.hasOverflowed()); + auto sourceCode = makeSource("pipeToAbortAlgorithm"_s, SourceOrigin(), SourceTaintedOrigin::Untainted); + auto* boundAlgorithm = JSBoundFunction::create(vm, globalObject, runtime->boundPipeAbortAlgorithm(), jsUndefined(), ArgList(boundArguments), 1, nullptr, sourceCode); + RETURN_IF_EXCEPTION(scope, ); + op->m_abortAlgorithmId = WebCore::AbortSignal::addAbortAlgorithmToSignal(signal, WebCore::JSAbortAlgorithm::create(vm, boundAlgorithm)); + } + + auto* reader = op->m_reader.get(); + auto* writer = op->m_writer.get(); + WebCore::registerPipeReaction(globalObject, reader->m_closedPromise.get(), runtime->onPipeSourceClosedFulfilled(), runtime->onPipeSourceClosedRejected(), op); + WebCore::registerPipeReaction(globalObject, writer->m_closedPromise.get(), runtime->onPipeDestClosedFulfilled(), runtime->onPipeDestClosedRejected(), op); + + op->checkErrorsMustBePropagatedForward(globalObject); + RETURN_IF_EXCEPTION(scope, ); + op->checkErrorsMustBePropagatedBackward(globalObject); + RETURN_IF_EXCEPTION(scope, ); + op->checkClosingMustBePropagatedForward(globalObject); + RETURN_IF_EXCEPTION(scope, ); + op->checkClosingMustBePropagatedBackward(globalObject); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, WebCore::pipeToLoopStep(globalObject, op)); +} + +// The PipeTo read request's steps (JSReadRequest.cpp dispatches its PipeTo arm here). +// No {value,done} object and no read promise: the chunk goes straight into the writer. +void pipeToReadRequestChunkSteps(JSGlobalObject* globalObject, JSStreamPipeToOperation* op, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + op->m_readInFlight = false; + if (op->m_finalized) + return; + auto* writer = op->m_writer.get(); + auto* runtime = JSStreamsRuntime::from(globalObject); + // The sink write is deferred by one reaction so an enqueue() inside the source never + // synchronously reenters the destination's write algorithm. m_currentWrite is the deferred + // write's promise, so a shutdown that must drain the pending writes still waits for it. + auto* deferred = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + auto* writePromise = JSPromise::create(vm, globalObject->promiseStructure()); + auto* context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), op, chunk); + deferred->performPromiseThenWithContext(vm, globalObject, runtime->onPipeChunkDeferredWrite(), jsUndefined(), writePromise, context); + op->m_currentWrite.set(vm, op, writePromise); + auto* settledHandler = runtime->onPipeWriteSettled(); + WebCore::registerPipeReaction(globalObject, writePromise, settledHandler, settledHandler, op); + // A shutdown that is waiting on m_currentWrite re-checks it when its reaction fires. + if (op->m_shuttingDown) + return; + WebCore::registerPipeReaction(globalObject, writer->m_readyPromise.get(), runtime->onPipeWriterReadyFulfilled(), nullptr, op); +} + +void pipeToReadRequestCloseSteps(JSGlobalObject* globalObject, JSStreamPipeToOperation* op) +{ + op->m_readInFlight = false; + if (op->m_finalized) + return; + op->checkClosingMustBePropagatedForward(globalObject); +} + +void pipeToReadRequestErrorSteps(JSGlobalObject* globalObject, JSStreamPipeToOperation* op, JSValue) +{ + op->m_readInFlight = false; + if (op->m_finalized) + return; + op->checkErrorsMustBePropagatedForward(globalObject); +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSStreamPipeToOperation.h b/src/jsc/bindings/webcore/streams/JSStreamPipeToOperation.h new file mode 100644 index 000000000000..8f31b1b45041 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamPipeToOperation.h @@ -0,0 +1,152 @@ +// JSStreamPipeToOperation — one cell per pipeTo/pipeThrough holding the operation's entire +// state. No closures; one visitChildren. +// +// LIVENESS: the acquired reader and writer each hold a visited m_pipeOperation back-edge to +// THIS cell, set at acquire and cleared in "finalize". Either stream end reachable ⇒ its +// reader/writer ⇒ this op ⇒ the other end. Zero Strong handles. +// The AbortSignal registration MUST go through the GC-visited +// addAbortAlgorithmToSignal/removeAbortAlgorithmFromSignal API (never +// AbortSignal::addAlgorithm) and MUST be removed on every terminal path. The registered +// callable is a JSBoundFunction over the [bound-convention] `boundPipeAbortAlgorithm` +// target (JSStreamsRuntime.h) with THIS cell bound at argument(0) — JSAbortAlgorithm invokes +// it as `(reason)` with no context slot, so a reaction-convention handler cannot be used. +// +// OWNERSHIP: `readableStreamPipeTo` (ReadableStreamOperations.cpp) only validates, allocates +// + populates this cell, and calls `startPipeToOperation(global, op)` (WebStreamsInternals.h). +// EVERYTHING ELSE — the loop, the four propagation checks, shutdown / shutdown-with-an-action +// / finalize, and every onPipe* reaction body — is a method here, owned by +// JSStreamPipeToOperation.cpp. +// Internal cell: no prototype, no constructor. Non-destructible (no WTF-container member). +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSStreamPipeToOperation final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSStreamPipeToOperation* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_source, m_destination, m_reader, m_writer, m_signal, + // m_promise, m_currentWrite, m_shutdownActionPromise, m_shutdownError. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The pipe state machine. ALL methods: userJS: yes. + + // The CLOSED set of spec "shutdown with an action" actions (no stored closures anywhere + // in the subsystem, so the pending action is an enum + m_shutdownError, performed by + // shutdownWithAction / after onPipeWritesFinishedForShutdown). + enum class ShutdownAction : uint8_t { + None, // plain "shutdown" (no action) + AbortDestination, // ! WritableStreamAbort(dest, error) — errors forward + CancelSource, // ! ReadableStreamCancel(source, error) — errors backward + CloseDestinationWithErrorPropagation, // writer close-with-error — closing forward + AbortBoth, // the signal's abort algorithm: abort dest THEN cancel source + }; + + // The four spec propagation checks. Each re-tests its condition from live state (never + // from a cached snapshot) and triggers shutdown/shutdownWithAction if it holds. + // spec: "Errors must be propagated forward: if source.[[state]] is/becomes 'errored'". + void checkErrorsMustBePropagatedForward(JSC::JSGlobalObject*); + // spec: "Errors must be propagated backward: if dest.[[state]] is/becomes 'errored'". + void checkErrorsMustBePropagatedBackward(JSC::JSGlobalObject*); + // spec: "Closing must be propagated forward: if source.[[state]] is/becomes 'closed'". + void checkClosingMustBePropagatedForward(JSC::JSGlobalObject*); + // spec: "Closing must be propagated backward: if ! WritableStreamCloseQueuedOrInFlight + // or dest.[[state]] is 'closed'". + void checkClosingMustBePropagatedBackward(JSC::JSGlobalObject*); + + // The spec shutdown protocol. `hasError` gates `error` (undefined is a legal error). + // spec "shutdown with an action": waits for pending writes, performs `action`, finalizes. + void shutdownWithAction(JSC::JSGlobalObject*, ShutdownAction, JSC::JSValue error, bool hasError); + // spec "shutdown": waits for pending writes, then finalizes (no action). + void shutdown(JSC::JSGlobalObject*, JSC::JSValue error, bool hasError); + // spec "finalize": releases the reader/writer, CLEARS both m_pipeOperation back-edges, + // removes the abort algorithm, and settles m_promise. Idempotent (m_finalized). + void finalize(JSC::JSGlobalObject*); + + // The per-reaction entry points. Each jsWebStreamsHandler_onPipe* trampoline + // (JSStreamsRuntime.h, [reaction-convention]) jsCasts its context cell to THIS class and + // calls the matching method; the bodies live in JSStreamPipeToOperation.cpp. + void onSourceClosedFulfilled(JSC::JSGlobalObject*); + void onSourceClosedRejected(JSC::JSGlobalObject*, JSC::JSValue error); + void onDestClosedFulfilled(JSC::JSGlobalObject*); + void onDestClosedRejected(JSC::JSGlobalObject*, JSC::JSValue error); + void onWriterReadyFulfilled(JSC::JSGlobalObject*); + // Registered as BOTH the fulfillment and the rejection handler of every write promise. + void onWriteSettled(JSC::JSGlobalObject*); + void onWritesFinishedForShutdown(JSC::JSGlobalObject*); + void onShutdownActionFulfilled(JSC::JSGlobalObject*); + void onShutdownActionRejected(JSC::JSGlobalObject*, JSC::JSValue error); + // The signal's abort-algorithm body ([bound-convention] boundPipeAbortAlgorithm): + // performs the spec's "abort both" shutdown-with-an-action. + void onSignalAbort(JSC::JSGlobalObject*, JSC::JSValue reason); + + // The piped streams & their acquired lock holders. + JSC::WriteBarrier m_source; // `source` + JSC::WriteBarrier m_destination; // `dest` + // The acquired reader (the reference pipe always uses a default reader, even for a + // byte source). Its m_pipeOperation points back here. + JSC::WriteBarrier m_reader; + // The acquired writer. Its m_pipeOperation points back here. + JSC::WriteBarrier m_writer; + + // The JSAbortSignal wrapper cell (null = no signal). Roots the impl the abort algorithm + // is registered on so removeAbortAlgorithmFromSignal(m_abortAlgorithmId) can always run. + JSC::WriteBarrier m_signal; + // Handle returned by WebCore::addAbortAlgorithmToSignal; 0 = none registered. + uint32_t m_abortAlgorithmId { 0 }; + + // Operation state. + // The promise pipeTo() returned. Roots nothing by itself; kept so finalize can settle it. + JSC::WriteBarrier m_promise; + // The promise of the write we are currently reacting to (the pipe reacts to EVERY + // write-request promise). + JSC::WriteBarrier m_currentWrite; + // "shutdown with an action": the action's promise while it is pending. + JSC::WriteBarrier m_shutdownActionPromise; + // The `originalError` / `error` handed to finalize; gated by m_hasShutdownError + // (an error value of `undefined` is legal). + JSC::WriteBarrier m_shutdownError; + bool m_hasShutdownError { false }; + // "shutdown with an action" wait-for-all latch: the number of action promises still + // pending (AbortBoth registers two). The last settlement proceeds. + uint8_t m_pendingShutdownActions { 0 }; + // The pending-abort action: which spec action shutdownWithAction is to perform once the + // pending writes drain (onWritesFinishedForShutdown). No closures. + ShutdownAction m_pendingShutdownAction { ShutdownAction::None }; + // `shuttingDown` + bool m_shuttingDown { false }; + // set once "finalize" ran (back-edges cleared, abort algorithm removed). + bool m_finalized { false }; + // a read has been issued and its read request has not settled yet. + bool m_readInFlight { false }; + bool m_preventClose { false }; + bool m_preventAbort { false }; + bool m_preventCancel { false }; + +private: + JSStreamPipeToOperation(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSStreamTeeState.cpp b/src/jsc/bindings/webcore/streams/JSStreamTeeState.cpp new file mode 100644 index 000000000000..b364cfb2796d --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamTeeState.cpp @@ -0,0 +1,70 @@ +#include "config.h" +#include "JSStreamTeeState.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSReadableStream.h" +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +const ClassInfo JSStreamTeeState::s_info = { "StreamTeeState"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSStreamTeeState) }; + +JSStreamTeeState::JSStreamTeeState(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSStreamTeeState::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSStreamTeeState* JSStreamTeeState::create(VM& vm, Structure* structure) +{ + auto* cell = new (NotNull, allocateCell(vm)) JSStreamTeeState(vm, structure); + cell->finishCreation(vm); + return cell; +} + +Structure* JSStreamTeeState::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +GCClient::IsoSubspace* JSStreamTeeState::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForStreamTeeState.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForStreamTeeState = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForStreamTeeState.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForStreamTeeState = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSStreamTeeState); + +template +void JSStreamTeeState::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_reader); + visitor.append(thisObject->m_branch1); + visitor.append(thisObject->m_branch2); + visitor.append(thisObject->m_cancelPromise); + visitor.append(thisObject->m_reason1); + visitor.append(thisObject->m_reason2); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSStreamTeeState.h b/src/jsc/bindings/webcore/streams/JSStreamTeeState.h new file mode 100644 index 000000000000..4465394b7234 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamTeeState.h @@ -0,0 +1,70 @@ +// JSStreamTeeState — the shared per-tee() state cell for BOTH the default tee and the byte +// tee. It is the algorithmContext of both branch controllers (SourceKind::TeeBranch / +// ByteTeeBranch; the branch index lives on the controller). ReadableByteStreamTee is a +// DIFFERENT algorithm from the default tee — the two only share this state cell. +// Internal cell: no prototype, no constructor. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include + +namespace WebCore { + +class JSStreamTeeState final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSStreamTeeState* create(JSC::VM&, JSC::Structure*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream, m_reader, m_branch1, m_branch2, + // m_cancelPromise, m_reason1, m_reason2. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The ORIGINAL stream — every cancel needs it. + JSC::WriteBarrier m_stream; + // MUTABLE: the byte tee releases and re-acquires readers of EITHER kind repeatedly. + // Erased to JSCell on purpose. + JSC::WriteBarrier m_reader; + // `branch1` / `branch2` + JSC::WriteBarrier m_branch1; + JSC::WriteBarrier m_branch2; + // `cancelPromise` + JSC::WriteBarrier m_cancelPromise; + // `reason1` / `reason2` — only meaningful once canceled1/canceled2 is set. + JSC::WriteBarrier m_reason1; + JSC::WriteBarrier m_reason2; + // `reading` + bool m_reading { false }; + // default tee: `readAgain`; byte tee: `readAgainForBranch1`. (One flag, two spec names.) + bool m_readAgain1 { false }; + // byte tee only: `readAgainForBranch2`. + bool m_readAgain2 { false }; + // `canceled1` / `canceled2` + bool m_canceled1 { false }; + bool m_canceled2 { false }; + // Bun: structured-clone branch2's chunks (Response.clone() passes true; + // ReadableStream.prototype.tee() passes false). Default-tee chunkSteps only. + bool m_shouldClone { false }; + +private: + JSStreamTeeState(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSStreamsRuntime.cpp b/src/jsc/bindings/webcore/streams/JSStreamsRuntime.cpp new file mode 100644 index 000000000000..e7db77d4b8b2 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamsRuntime.cpp @@ -0,0 +1,227 @@ +#include "config.h" +#include "JSStreamsRuntime.h" + +#include "WebStreamsInternals.h" + +#include "BunStandaloneTextSink.h" +#include "BunStreamSource.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSCrossRealmTransformState.h" +#include "JSDirectSinkCloseState.h" +#include "JSAsyncIteratorSourceOperation.h" +#include "JSDirectStreamController.h" +#include "JSOneShotDirectSink.h" +#include "JSReadableStreamIntoArrayOperation.h" +#include "JSPullIntoDescriptor.h" +#include "JSReadRequest.h" +#include "JSReadStreamIntoSinkOperation.h" +#include "JSResumableSinkPumpOperation.h" +#include "JSStreamAlgorithmContexts.h" +#include "JSStreamPipeToOperation.h" +#include "JSStreamTeeState.h" +#include "WebCoreJSClientData.h" +#include "ZigGlobalObject.h" + +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; + +const ClassInfo JSStreamsRuntime::s_info = { "StreamsRuntime"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSStreamsRuntime) }; + +JSStreamsRuntime::JSStreamsRuntime(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +Structure* JSStreamsRuntime::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSStreamsRuntime* JSStreamsRuntime::create(VM& vm, Zig::GlobalObject* globalObject) +{ + auto* structure = createStructure(vm, globalObject, jsNull()); + auto* cell = new (NotNull, allocateCell(vm)) JSStreamsRuntime(vm, structure); + cell->finishCreation(vm, globalObject); + return cell; +} + +JSStreamsRuntime* JSStreamsRuntime::from(JSGlobalObject* globalObject) +{ + return defaultGlobalObject(globalObject)->streamsRuntime(); +} + +GCClient::IsoSubspace* JSStreamsRuntime::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForStreamsRuntime.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForStreamsRuntime = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForStreamsRuntime.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForStreamsRuntime = std::forward(space); }); +} + +void JSStreamsRuntime::finishCreation(VM& vm, Zig::GlobalObject*) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + + using HandlerProperty = JSC::LazyProperty; + +#define WEB_STREAMS_INIT_HANDLER(name) \ + m_##name.initLater([](const HandlerProperty::Initializer& init) { \ + init.set(JSFunction::create(init.vm, init.owner->globalObject(), 2, #name ""_s, \ + jsWebStreamsHandler_##name, ImplementationVisibility::Private)); \ + }); + FOR_EACH_WEB_STREAMS_REACTION_HANDLER(WEB_STREAMS_INIT_HANDLER) + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET(WEB_STREAMS_INIT_HANDLER) +#undef WEB_STREAMS_INIT_HANDLER + + // Spec: `%FooQueuingStrategy%.prototype.size` is ONE user-visible function object per realm. + m_byteLengthQueuingStrategySizeFunction.initLater([](const HandlerProperty::Initializer& init) { + init.set(JSFunction::create(init.vm, init.owner->globalObject(), 1, "size"_s, + jsWebStreamsByteLengthQueuingStrategySize, ImplementationVisibility::Public)); + }); + m_countQueuingStrategySizeFunction.initLater([](const HandlerProperty::Initializer& init) { + init.set(JSFunction::create(init.vm, init.owner->globalObject(), 0, "size"_s, + jsWebStreamsCountQueuingStrategySize, ImplementationVisibility::Public)); + }); + +#define WEB_STREAMS_INIT_STRUCTURE(memberName, ClassName) \ + m_##memberName.initLater([](const JSC::LazyProperty::Initializer& init) { \ + init.set(ClassName::createStructure(init.vm, init.owner->globalObject(), jsNull())); \ + }); + FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE(WEB_STREAMS_INIT_STRUCTURE) +#undef WEB_STREAMS_INIT_STRUCTURE + + m_readManyResultStructure.initLater([](const JSC::LazyProperty::Initializer& init) { + auto* globalObject = init.owner->globalObject(); + auto& vm = init.vm; + auto* structure = globalObject->structureCache().emptyObjectStructureForPrototype(globalObject, globalObject->objectPrototype(), 3); + JSC::PropertyOffset offset; + structure = Structure::addPropertyTransition(vm, structure, vm.propertyNames->value, 0, offset); + RELEASE_ASSERT(offset == 0); + structure = Structure::addPropertyTransition(vm, structure, WebCore::builtinNames(vm).sizePublicName(), 0, offset); + RELEASE_ASSERT(offset == 1); + structure = Structure::addPropertyTransition(vm, structure, vm.propertyNames->done, 0, offset); + RELEASE_ASSERT(offset == 2); + init.set(structure); + }); +} + +DEFINE_VISIT_CHILDREN(JSStreamsRuntime); + +template +void JSStreamsRuntime::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + +#define WEB_STREAMS_VISIT_HANDLER(name) thisObject->m_##name.visit(visitor); + FOR_EACH_WEB_STREAMS_REACTION_HANDLER(WEB_STREAMS_VISIT_HANDLER) + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET(WEB_STREAMS_VISIT_HANDLER) +#undef WEB_STREAMS_VISIT_HANDLER + + thisObject->m_byteLengthQueuingStrategySizeFunction.visit(visitor); + thisObject->m_countQueuingStrategySizeFunction.visit(visitor); + +#define WEB_STREAMS_VISIT_STRUCTURE(memberName, ClassName) thisObject->m_##memberName.visit(visitor); + FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE(WEB_STREAMS_VISIT_STRUCTURE) + thisObject->m_readManyResultStructure.visit(visitor); +#undef WEB_STREAMS_VISIT_STRUCTURE + + { + WTF::Locker locker { thisObject->cellLock() }; + for (auto& controller : thisObject->m_endOfTickFlushes) + visitor.append(controller); + } +} + +// See src/jsc/event_loop.rs. The deferred queue runs right after every microtask drain; the +// runtime cell (global lifetime, non-destructible) is the only pointer it ever holds for streams. +extern "C" bool Bun__EventLoop__postDeferredTask(void* bunVM, void* ctx, bool (*task)(void*)); + +extern "C" bool Bun__StreamsRuntime__endOfTickFlush(void* ctx) +{ + auto* runtime = static_cast(ctx); + auto* globalObject = runtime->globalObject(); + auto& vm = JSC::getVM(globalObject); + WTF::Vector> pending; + { + WTF::Locker locker { runtime->cellLock() }; + pending = std::exchange(runtime->m_endOfTickFlushes, {}); + } + // Keep the queue entry registered while draining: controllers armed by user JS running + // inside onFlush land in the fresh list and are picked up by the "stay registered" return. + JSC::MarkedArgumentBuffer live; + for (auto& barrier : pending) + live.append(barrier.get()); + for (unsigned i = 0; i < live.size(); i++) { + auto* controller = uncheckedDowncast(live.at(i)); + controller->m_endOfTickFlushArmed = false; + if (controller->m_closed || !controller->m_stream) + continue; + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + controller->onFlush(globalObject); + if (scope.exception()) [[unlikely]] { + // There is no JS caller: error the stream like a throwing flush() would. + JSC::JSValue error = Bun::WebStreams::takeAbruptCompletion(globalObject, scope); + if (error) + controller->handleError(globalObject, error); + scope.clearExceptionExceptTermination(); + } + } + bool keepRegistered; + { + WTF::Locker locker { runtime->cellLock() }; + keepRegistered = !runtime->m_endOfTickFlushes.isEmpty(); + runtime->m_endOfTickFlushTaskRegistered = keepRegistered; + } + return keepRegistered; +} + +void JSStreamsRuntime::armEndOfTickFlush(JSGlobalObject* globalObject, JSDirectStreamController* controller) +{ + auto& vm = JSC::getVM(globalObject); + { + WTF::Locker locker { cellLock() }; + m_endOfTickFlushes.append(JSC::WriteBarrier(vm, this, controller)); + } + vm.writeBarrier(this, controller); + if (m_endOfTickFlushTaskRegistered) + return; + m_endOfTickFlushTaskRegistered = true; + Bun__EventLoop__postDeferredTask(bunVM(globalObject), this, &Bun__StreamsRuntime__endOfTickFlush); +} + +JSFunction* JSStreamsRuntime::byteLengthQueuingStrategySizeFunction(const Zig::GlobalObject*) +{ + return m_byteLengthQueuingStrategySizeFunction.get(this); +} + +JSFunction* JSStreamsRuntime::countQueuingStrategySizeFunction(const Zig::GlobalObject*) +{ + return m_countQueuingStrategySizeFunction.get(this); +} + +#define WEB_STREAMS_DEFINE_STRUCTURE_ACCESSOR(memberName, ClassName) \ + Structure* JSStreamsRuntime::memberName(const Zig::GlobalObject*) \ + { \ + return m_##memberName.get(this); \ + } +FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE(WEB_STREAMS_DEFINE_STRUCTURE_ACCESSOR) +#undef WEB_STREAMS_DEFINE_STRUCTURE_ACCESSOR + +Structure* JSStreamsRuntime::readManyResultStructure(const Zig::GlobalObject*) +{ + return m_readManyResultStructure.get(this); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSStreamsRuntime.h b/src/jsc/bindings/webcore/streams/JSStreamsRuntime.h new file mode 100644 index 000000000000..d088db49fd45 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSStreamsRuntime.h @@ -0,0 +1,412 @@ +// JSStreamsRuntime — the ONE per-global cell holding every piece of per-global Web Streams +// state: the two CLOSED handler-function lists, the per-realm queuing-strategy `size` +// functions, and the cached Structures of every internal (prototype-less) cell class. It is +// reached through ONE LazyProperty on Zig::GlobalObject (`globalObject->streamsRuntime()`); +// do NOT add per-function fields to ZigGlobalObject. Every handler / size function / +// Structure is a LazyProperty materialized on first use via `m_NAME.get(this)`. +// +// THE TWO CALLABLE MECHANISMS — the ONLY two. Anything else (a per-stream JSFunction, ANY +// capturing JSNativeStdFunction) is FORBIDDEN in this subsystem. +// +// [reaction-convention] — FOR_EACH_WEB_STREAMS_REACTION_HANDLER. Registered ONLY through +// `promise->performPromiseThenWithContext(vm, global, onFulfilled, onRejected, +// resultPromiseOrJSUndefined, contextCell)`. The handler is invoked as +// handler(resolutionValue, contextCell) // context at argument(1) +// with `this` = undefined. The SAME convention is used for the native +// `queueMicrotask(handler, value, contextCell)` deferrals, so a reaction handler is +// reusable as a microtask job. Every handler is a BOUNDARY: it must convert any internal +// failure into the spec action and never return with a pending exception. +// +// [bound-convention] — FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET. The shared function is +// NEVER called directly; it is wrapped per use-site in +// `JSC::JSBoundFunction::create(vm, global, target, jsUndefined(), {contextCell}, ...)` +// and STORED ON an object we do not control (the native source handle, the JSSink +// controller, the ResumableSink). `boundFunctionCall` PREPENDS the bound args, so the +// target receives +// handler(contextCell, ...callArgs) // context at argument(0) +// — the OPPOSITE position. A function may belong to EXACTLY ONE of the two lists. +// +// Both handler lists are CLOSED: adding a handler requires a new macro entry here plus a +// JSC_DEFINE_HOST_FUNCTION in the owner .cpp; it changes no signature. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include +#include +#include + +namespace WebCore { + +class JSDirectStreamController; + +// [reaction-convention] handlers, grouped by the .cpp that OWNS the body. +// Signature of every entry: name(JSC::JSValue resolutionValue, contextCell at argument(1)). + +// owner: WebStreamsMisc.cpp — the shared "fulfillment step that returns undefined" / no-op +// reaction (readableStreamCancel; readDirectStream's `.then(noop)`). context: unused. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_MISC(V) \ + V(onReturnUndefined) + +// owner: JSReadableStreamDefaultController.cpp. context = JSReadableStreamDefaultController. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_RS_DEFAULT_CONTROLLER(V) \ + V(onRSDefaultControllerStartFulfilled) \ + V(onRSDefaultControllerStartRejected) \ + V(onRSDefaultControllerPullFulfilled) \ + V(onRSDefaultControllerPullRejected) + +// owner: JSReadableByteStreamController.cpp. context = JSReadableByteStreamController. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_RS_BYTE_CONTROLLER(V) \ + V(onRSByteControllerStartFulfilled) \ + V(onRSByteControllerStartRejected) \ + V(onRSByteControllerPullFulfilled) \ + V(onRSByteControllerPullRejected) + +// owner: ReadableStreamOperations.cpp. +// FromIterable: context = the JSReadableStreamDefaultController (its algorithmContext is +// the JSStreamFromIterableContext). +// Tee: context = the JSStreamTeeState, except onByteTeeReaderClosedRejected whose context +// is an InternalFieldTuple{teeState, thisReader}. +// The two *Microtask entries are the tee chunk-steps "queue a microtask" jobs. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_RS_OPERATIONS(V) \ + V(onFromIterablePullFulfilled) \ + V(onFromIterableCancelFulfilled) \ + V(onDefaultTeeReadChunkMicrotask) \ + V(onDefaultTeeReaderClosedRejected) \ + V(onByteTeeReadChunkMicrotask) \ + V(onByteTeeReadIntoChunkMicrotask) \ + V(onByteTeeReaderClosedRejected) + +// owner: BunAsyncIterableSource.cpp. context = the JSAsyncIteratorSourceOperation, EXCEPT +// onAsyncIterableSourceErrorRethrow / onAsyncIterableSourceErrorSwallowed, whose context is +// an InternalFieldTuple{op, originalError} (registered on iter.throw()'s settlement). +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_ASYNC_ITERABLE_SOURCE(V) \ + V(onAsyncIterableSourceNextFulfilled) \ + V(onAsyncIterableSourceFlushFulfilled) \ + V(onAsyncIterableSourceErrored) \ + V(onAsyncIterableSourceEndFulfilled) \ + V(onAsyncIterableSourceCleanupSettled) \ + V(onAsyncIterableSourceErrorRethrow) \ + V(onAsyncIterableSourceErrorSwallowed) + +// owner: JSReadableStreamAsyncIterator.cpp. context = the JSReadableStreamAsyncIterator, +// EXCEPT onAsyncIteratorReturnAfterOngoingSettled and onAsyncIteratorCancelFulfilled, whose +// context is an InternalFieldTuple{iterator, value} (the return()/cancel value may be null/undefined). +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_ASYNC_ITERATOR(V) \ + V(onAsyncIteratorNextAfterOngoingSettled) \ + V(onAsyncIteratorReturnAfterOngoingSettled) \ + V(onAsyncIteratorCancelFulfilled) \ + V(onAsyncIteratorResolveMicrotask) \ + V(onAsyncIteratorRejectMicrotask) + +// owner: JSStreamPipeToOperation.cpp. context = the JSStreamPipeToOperation, EXCEPT +// onPipeChunkDeferredWrite, whose context is an InternalFieldTuple{op, chunk} (the pipe's +// read-request chunk steps defer the sink write by one reaction). +// onPipeWriteSettled is registered as BOTH the fulfillment and the rejection handler of +// every write-request promise (the pipe must react to every one). +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_PIPE(V) \ + V(onPipeChunkDeferredWrite) \ + V(onPipeSourceClosedFulfilled) \ + V(onPipeSourceClosedRejected) \ + V(onPipeDestClosedFulfilled) \ + V(onPipeDestClosedRejected) \ + V(onPipeWriterReadyFulfilled) \ + V(onPipeWriteSettled) \ + V(onPipeWritesFinishedForShutdown) \ + V(onPipeShutdownActionFulfilled) \ + V(onPipeShutdownActionRejected) + +// owner: WritableStreamOperations.cpp. context = the JSWritableStream. +// (WritableStreamFinishErroring's reaction to the [[AbortSteps]] promise.) +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_WS_OPERATIONS(V) \ + V(onWSAbortStepsFulfilled) \ + V(onWSAbortStepsRejected) + +// owner: JSWritableStreamDefaultController.cpp. context = JSWritableStreamDefaultController. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_WS_CONTROLLER(V) \ + V(onWSControllerStartFulfilled) \ + V(onWSControllerStartRejected) \ + V(onWSSinkCloseFulfilled) \ + V(onWSSinkCloseRejected) \ + V(onWSSinkWriteFulfilled) \ + V(onWSSinkWriteRejected) + +// owner: TransformStreamOperations.cpp. context = the JSTransformStream, EXCEPT +// onTSSinkWriteBackpressureChangeFulfilled, whose context is an +// InternalFieldTuple{transformStream, chunk}. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_TS_OPERATIONS(V) \ + V(onTSSinkWriteBackpressureChangeFulfilled) \ + V(onTSSinkAbortCancelFulfilled) \ + V(onTSSinkAbortCancelRejected) \ + V(onTSSinkCloseFlushFulfilled) \ + V(onTSSinkCloseFlushRejected) \ + V(onTSSourceCancelFulfilled) \ + V(onTSSourceCancelRejected) + +// owner: JSTransformStreamDefaultController.cpp. context = JSTransformStreamDefaultController. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_TS_CONTROLLER(V) \ + V(onTSPerformTransformRejected) + +// owner: CrossRealmTransform.cpp (transferable streams are not implemented; the handler may +// assert-not-reached). context = the JSCrossRealmTransformState. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_CROSS_REALM(V) \ + V(onCrossRealmWritableBackpressureFulfilled) + +// owner: BunStreamSource.cpp. +// onNativePull*: context = the JSNativeStreamSourceAdapter. +// onNativeSourceCallCloseMicrotask: the native source's `queueMicrotask(callClose)` job; +// context = the adapter. +// onReadStreamIntoSink*: context = the JSReadStreamIntoSinkOperation. +// onResumableSink*: context = the JSResumableSinkPumpOperation. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_BUN_SOURCE(V) \ + V(onNativePullFulfilled) \ + V(onNativePullRejected) \ + V(onNativeSourceCallCloseMicrotask) \ + V(onReadStreamIntoSinkReadManyFulfilled) \ + V(onReadStreamIntoSinkReadFulfilled) \ + V(onReadStreamIntoSinkFlushFulfilled) \ + V(onReadStreamIntoSinkRejected) \ + V(onResumableSinkReadFulfilled) \ + V(onResumableSinkReadRejected) \ + V(onResumableSinkEndMicrotask) + +// owner: JSDirectStreamController.cpp. context = the JSDirectStreamController. +// onDirectPullRejected is THE one reaction registered WITH a real (fresh, unhandled) result +// promise — the unhandledRejection is load-bearing. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_DIRECT_CONTROLLER(V) \ + V(onDirectPullRejected) + +// owner: JSReadableStreamDefaultReader.cpp (readMany). context = the reader. +// onReadManyPullFulfilled: controller.$pull()'s fulfillment. +// onReadManyDirectPullFulfilled: the Direct (not-yet-started) controller branch: maps +// directController->onPull()'s {done,value} into the readMany {value,size,done} result +// shape (a DIFFERENT mapping from onReadManyPullFulfilled's). +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_READER(V) \ + V(onReadManyPullFulfilled) \ + V(onReadManyDirectPullFulfilled) + +// owner: BunStreamConsumers.cpp. +// onBufferedFastPath*: context = the JSReadableStream (the fast path's catch/finally pair). +// onReadableStreamTo*Fulfilled: the generic-path promise chains +// (toArrayBuffer/toBytes/toBlob: value = the chunk array; toJSON: value = the text; +// toFormData: value = the Blob, context = the contentType JSString). +// onIntoArrayReadMany*: readableStreamIntoArray's readMany() continuation (readMany may +// return a Promise); context = an InternalFieldTuple{reader, resultArray}. +// onDirectConsumeLoopRead*: the readableStreamTo{Text,Array}Direct read loop; +// context = an InternalFieldTuple{stream, reader}. +// onConsumeDirectToArrayBufferPull*: the one-shot pull's settlement; context = the +// JSOneShotDirectSink cell (it roots the stream, the ArrayBufferSink, the capability +// promise, and the closed flag — see JSOneShotDirectSink.h). +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER_BUN_CONSUMERS(V) \ + V(onBufferedFastPathRejected) \ + V(onBufferedFastPathSettled) \ + V(onReadableStreamToArrayBufferFulfilled) \ + V(onReadableStreamToBytesFulfilled) \ + V(onReadableStreamToTextChunksFulfilled) \ + V(onReadableStreamToJSONFulfilled) \ + V(onReadableStreamToBlobFulfilled) \ + V(onReadableStreamToFormDataFulfilled) \ + V(onIntoArrayReadManyFulfilled) /* append value; !done => readMany() again; done => release + resolve */ \ + V(onIntoArrayReadManyRejected) /* release the reader, reject the result promise */ \ + V(onIntoArrayReadFulfilled) /* persistent-op pump: append the read chunk, keep filling */ \ + V(onIntoArrayReadRejected) /* persistent-op pump: release the reader, reject the result */ \ + V(onDirectConsumeLoopReadFulfilled) \ + V(onDirectConsumeLoopReadRejected) \ + V(onConsumeDirectToArrayBufferPullFulfilled) \ + V(onConsumeDirectToArrayBufferPullRejected) + +// THE closed [reaction-convention] list. +#define FOR_EACH_WEB_STREAMS_REACTION_HANDLER(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_MISC(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_RS_DEFAULT_CONTROLLER(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_RS_BYTE_CONTROLLER(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_RS_OPERATIONS(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_ASYNC_ITERATOR(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_ASYNC_ITERABLE_SOURCE(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_PIPE(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_WS_OPERATIONS(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_WS_CONTROLLER(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_TS_OPERATIONS(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_TS_CONTROLLER(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_CROSS_REALM(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_BUN_SOURCE(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_DIRECT_CONTROLLER(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_READER(V) \ + FOR_EACH_WEB_STREAMS_REACTION_HANDLER_BUN_CONSUMERS(V) + +// [bound-convention] targets, grouped by the .cpp that OWNS the body. +// Signature of every entry: name(contextCell at argument(0), ...callArgs). + +// owner: BunStreamSource.cpp. +// boundOnNativeSourceClose(adapter) / boundOnNativeSourceDrain(adapter, chunk): stored as +// handle.onClose / handle.onDrain. +// boundReadDirectStreamOnClose(state, streamOrUndefined, reason): readDirectStream's +// JSSink onClose. +// boundReadStreamIntoSinkOnClose(op, stream, reason): readStreamIntoSink's JSSink onClose. +// boundResumableSinkDrain(op) / boundResumableSinkCancel(op, unused, reason): stored on +// the native ResumableSink via setHandlers. +#define FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_BUN_SOURCE(V) \ + V(boundOnNativeSourceClose) \ + V(boundOnNativeSourceDrain) \ + V(boundReadDirectStreamOnClose) \ + V(boundReadStreamIntoSinkOnClose) \ + V(boundResumableSinkDrain) \ + V(boundResumableSinkCancel) + +// owner: JSDirectStreamController.cpp — the FIVE detachable own methods of the direct +// controller: `end` and `close` are two bound cells over the ONE boundDirectClose target. +#define FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_DIRECT_CONTROLLER(V) \ + V(boundDirectWrite) \ + V(boundDirectClose) \ + V(boundDirectFlush) \ + V(boundDirectError) + +// owner: BunStreamConsumers.cpp — the one-shot direct consumer's throwaway controller +// (consumeDirectStreamToArrayBuffer). Its {start, write, end, close, flush} are OWN +// JSBoundFunctions over these; context (argument 0) = the JSOneShotDirectSink cell. This +// path deliberately does NOT reuse boundDirect* / JSDirectStreamController. +#define FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_ONE_SHOT(V) \ + V(boundOneShotStart) /* `start` is bound to this no-op target that returns undefined */ \ + V(boundOneShotDirectWrite) \ + V(boundOneShotDirectClose) /* `end` and `close` are two bound cells over this one target */ \ + V(boundOneShotDirectFlush) + +// owner: JSStreamPipeToOperation.cpp — the pipe's AbortSignal abort algorithm. +// `readableStreamPipeTo({signal})` registers it through the GC-visited +// addAbortAlgorithmToSignal API, whose JSAbortAlgorithm wraps ONE JSObject* callback invoked +// as `(reason)` with no context slot — so the callable MUST be a JSBoundFunction over this +// target with the op cell bound at argument 0: boundPipeAbortAlgorithm(pipeOpCell, reason). +#define FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_PIPE(V) \ + V(boundPipeAbortAlgorithm) + +// owner: BunAsyncIterableSource.cpp — the async-iterable direct source's three methods. +// Bound context (argument 0) = the JSAsyncIteratorSourceOperation. +#define FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_ASYNC_ITERABLE_SOURCE(V) \ + V(boundAsyncIterableSourcePull) \ + V(boundAsyncIterableSourceCancel) \ + V(boundAsyncIterableSourceClose) + +// THE closed [bound-convention] list. +#define FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET(V) \ + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_BUN_SOURCE(V) \ + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_DIRECT_CONTROLLER(V) \ + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_ONE_SHOT(V) \ + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_PIPE(V) \ + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET_ASYNC_ITERABLE_SOURCE(V) + +// The native trampolines behind every handler. Each is DEFINED (JSC_DEFINE_HOST_FUNCTION) +// in its owner .cpp above; JSStreamsRuntime.cpp only wraps them in shared JSFunctions. +#define WEB_STREAMS_DECLARE_HANDLER_HOST_FUNCTION(name) \ + JSC_DECLARE_HOST_FUNCTION(jsWebStreamsHandler_##name); +FOR_EACH_WEB_STREAMS_REACTION_HANDLER(WEB_STREAMS_DECLARE_HANDLER_HOST_FUNCTION) +FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET(WEB_STREAMS_DECLARE_HANDLER_HOST_FUNCTION) +#undef WEB_STREAMS_DECLARE_HANDLER_HOST_FUNCTION + +// The per-realm queuing-strategy size functions (owner: WebStreamsMisc.cpp). +JSC_DECLARE_HOST_FUNCTION(jsWebStreamsByteLengthQueuingStrategySize); +JSC_DECLARE_HOST_FUNCTION(jsWebStreamsCountQueuingStrategySize); + +// The internal (prototype-less) cell classes whose per-global Structure is cached here. +// V(memberName, ClassName) +#define FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE(V) \ + V(readRequestStructure, JSReadRequest) \ + V(readIntoRequestStructure, JSReadIntoRequest) \ + V(pullIntoDescriptorStructure, JSPullIntoDescriptor) \ + V(pipeToOperationStructure, JSStreamPipeToOperation) \ + V(teeStateStructure, JSStreamTeeState) \ + V(crossRealmTransformStateStructure, JSCrossRealmTransformState) \ + V(fromIterableContextStructure, JSStreamFromIterableContext) \ + V(directStreamControllerStructure, JSDirectStreamController) \ + V(nativeStreamSourceAdapterStructure, JSNativeStreamSourceAdapter) \ + V(directSinkCloseStateStructure, JSDirectSinkCloseState) \ + V(asyncIteratorSourceOperationStructure, JSAsyncIteratorSourceOperation) \ + V(readStreamIntoSinkOperationStructure, JSReadStreamIntoSinkOperation) \ + V(resumableSinkPumpOperationStructure, JSResumableSinkPumpOperation) \ + V(standaloneTextSinkStructure, JSBunStandaloneTextSink) \ + V(oneShotDirectSinkStructure, JSOneShotDirectSink) \ + V(intoArrayOperationStructure, JSReadableStreamIntoArrayOperation) + +// Non-destructible: LazyProperty members only (plus the end-of-tick flush list, a +// WriteBarrier container mutated and visited under this cell's lock). +class JSStreamsRuntime final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + // Zig::GlobalObject holds ONE LazyProperty whose initializer calls this. + static JSStreamsRuntime* create(JSC::VM&, Zig::GlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + // The one accessor everything uses: `defaultGlobalObject(global)->streamsRuntime()` + // behind a free function so streams .cpp files do not include ZigGlobalObject.h. + static JSStreamsRuntime* from(JSC::JSGlobalObject*); + + // End-of-tick flush service for JS-facing direct controllers: the runtime (a + // global-lifetime, non-destructible cell) is the only pointer registered with the + // event loop's deferred task queue; armed controllers are rooted by m_endOfTickFlushes. + void armEndOfTickFlush(JSC::JSGlobalObject*, JSDirectStreamController*); + WTF::Vector> m_endOfTickFlushes; + bool m_endOfTickFlushTaskRegistered { false }; + + DECLARE_INFO; + // visitChildrenImpl MUST visit: EVERY m_ LazyProperty (both macro lists), the + // two size-function LazyProperties, and every LazyProperty in + // FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // The shared handler functions. Each LazyProperty gets its initializer in finishCreation + // and materializes the JSFunction on the FIRST get(this) — never eagerly. +#define WEB_STREAMS_DECLARE_HANDLER_ACCESSOR(name) \ + JSC::JSFunction* name() const { return m_##name.get(this); } + FOR_EACH_WEB_STREAMS_REACTION_HANDLER(WEB_STREAMS_DECLARE_HANDLER_ACCESSOR) + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET(WEB_STREAMS_DECLARE_HANDLER_ACCESSOR) +#undef WEB_STREAMS_DECLARE_HANDLER_ACCESSOR + + // The per-realm queuing-strategy size functions (spec: same function object per realm; + // %ByteLengthQueuingStrategy%.prototype.size / %CountQueuingStrategy%.prototype.size). + JSC::JSFunction* byteLengthQueuingStrategySizeFunction(const Zig::GlobalObject*); + JSC::JSFunction* countQueuingStrategySizeFunction(const Zig::GlobalObject*); + + // The cached Structures of the internal cells. +#define WEB_STREAMS_DECLARE_STRUCTURE_ACCESSOR(memberName, ClassName) \ + JSC::Structure* memberName(const Zig::GlobalObject*); + FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE(WEB_STREAMS_DECLARE_STRUCTURE_ACCESSOR) +#undef WEB_STREAMS_DECLARE_STRUCTURE_ACCESSOR + + // The readMany `{value, size, done}` result shape, so results are built with + // putDirectOffset instead of three transitioning putDirects. + JSC::Structure* readManyResultStructure(const Zig::GlobalObject*); + +private: + JSStreamsRuntime(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&, Zig::GlobalObject*); + +#define WEB_STREAMS_DECLARE_HANDLER_MEMBER(name) \ + JSC::LazyProperty m_##name; + FOR_EACH_WEB_STREAMS_REACTION_HANDLER(WEB_STREAMS_DECLARE_HANDLER_MEMBER) + FOR_EACH_WEB_STREAMS_BOUND_HANDLER_TARGET(WEB_STREAMS_DECLARE_HANDLER_MEMBER) +#undef WEB_STREAMS_DECLARE_HANDLER_MEMBER + + JSC::LazyProperty m_byteLengthQueuingStrategySizeFunction; + JSC::LazyProperty m_countQueuingStrategySizeFunction; + +#define WEB_STREAMS_DECLARE_STRUCTURE_MEMBER(memberName, ClassName) \ + JSC::LazyProperty m_##memberName; + FOR_EACH_WEB_STREAMS_INTERNAL_STRUCTURE(WEB_STREAMS_DECLARE_STRUCTURE_MEMBER) +#undef WEB_STREAMS_DECLARE_STRUCTURE_MEMBER + JSC::LazyProperty m_readManyResultStructure; +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSTextDecoderStream.cpp b/src/jsc/bindings/webcore/streams/JSTextDecoderStream.cpp new file mode 100644 index 000000000000..8ab3f076b7c5 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTextDecoderStream.cpp @@ -0,0 +1,392 @@ +#include "config.h" +#include "JSTextDecoderStream.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStream.h" +#include "JSStreamsRuntime.h" +#include "JSTransformStream.h" +#include "JSTransformStreamDefaultController.h" +#include "JSWritableStream.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_constructor); +static JSC_DECLARE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_encoding); +static JSC_DECLARE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_fatal); +static JSC_DECLARE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_ignoreBOM); +static JSC_DECLARE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_readable); +static JSC_DECLARE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_writable); + +class JSTextDecoderStreamPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSTextDecoderStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSTextDecoderStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTextDecoderStreamPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextDecoderStreamPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSTextDecoderStreamPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextDecoderStreamPrototype, JSTextDecoderStreamPrototype::Base); + +// JSTextDecoderStreamConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSTextDecoderStreamConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSTextDecoderStreamConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSTextDecoderStreamConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSTextDecoderStreamConstructor::subspaceForImpl(JSC::VM&); +template<> void JSTextDecoderStreamConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSTextDecoderStreamConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSTextDecoderStreamConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSTextDecoderStreamConstructor::s_info = { "TextDecoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextDecoderStreamConstructor) }; + +template<> JSValue JSTextDecoderStreamConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSTextDecoderStreamConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSTextDecoderStreamConstructor); + +template<> GCClient::IsoSubspace* JSTextDecoderStreamConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForTextDecoderStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTextDecoderStreamConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForTextDecoderStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForTextDecoderStreamConstructor = std::forward(space); }); +} + +template<> void JSTextDecoderStreamConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "TextDecoderStream"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSTextDecoderStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSTextDecoderStreamConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSTextDecoderStreamConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + auto& names = builtinNames(vm); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* stream = JSTextDecoderStream::create(vm, structure); + + auto* transform = createTransformStream(lexicalGlobalObject, TransformerKind::TextDecoder, stream, 1, nullptr, 0, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + stream->m_transform.set(vm, stream, transform); + + JSValue label = callFrame->argumentCount() >= 1 ? callFrame->uncheckedArgument(0) : jsNontrivialString(vm, "utf-8"_s); + bool fatal = false; + bool ignoreBOM = false; + JSValue options = callFrame->argument(1); + // Web IDL: `optional TextDecoderOptions options = {}` — undefined/null mean defaults. + if (!options.isUndefinedOrNull()) { + JSValue fatalValue = options.get(lexicalGlobalObject, names.fatalPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + fatal = fatalValue.toBoolean(lexicalGlobalObject); + JSValue ignoreBOMValue = options.get(lexicalGlobalObject, names.ignoreBOMPublicName()); + RETURN_IF_EXCEPTION(scope, {}); + ignoreBOM = ignoreBOMValue.toBoolean(lexicalGlobalObject); + } + + // `new TextDecoder(label, { fatal, ignoreBOM })` owns the label validation. + auto* decoderOptions = constructEmptyObject(lexicalGlobalObject); + decoderOptions->putDirect(vm, names.fatalPublicName(), jsBoolean(fatal)); + decoderOptions->putDirect(vm, names.ignoreBOMPublicName(), jsBoolean(ignoreBOM)); + MarkedArgumentBuffer decoderArguments; + decoderArguments.append(label); + decoderArguments.append(decoderOptions); + ASSERT(!decoderArguments.hasOverflowed()); + auto* decoder = JSC::construct(lexicalGlobalObject, defaultGlobalObject(lexicalGlobalObject)->JSTextDecoderConstructor(), decoderArguments, "TextDecoder is not constructible"_s); + RETURN_IF_EXCEPTION(scope, {}); + stream->m_decoder.set(vm, stream, decoder); + + return JSValue::encode(stream); +} +JSC_ANNOTATE_HOST_FUNCTION(JSTextDecoderStreamConstructorConstruct, JSTextDecoderStreamConstructor::construct); + +// JSTextDecoderStreamPrototype + +static const HashTableValue JSTextDecoderStreamPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextDecoderStreamPrototypeGetter_constructor, 0 } }, + { "encoding"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextDecoderStreamPrototypeGetter_encoding, 0 } }, + { "fatal"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextDecoderStreamPrototypeGetter_fatal, 0 } }, + { "ignoreBOM"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextDecoderStreamPrototypeGetter_ignoreBOM, 0 } }, + { "readable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextDecoderStreamPrototypeGetter_readable, 0 } }, + { "writable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextDecoderStreamPrototypeGetter_writable, 0 } }, +}; + +const ClassInfo JSTextDecoderStreamPrototype::s_info = { "TextDecoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextDecoderStreamPrototype) }; + +void JSTextDecoderStreamPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSTextDecoderStream::info(), JSTextDecoderStreamPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSTextDecoderStream + +const ClassInfo JSTextDecoderStream::s_info = { "TextDecoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextDecoderStream) }; + +JSTextDecoderStream::JSTextDecoderStream(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSTextDecoderStream::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSTextDecoderStream* JSTextDecoderStream::create(VM& vm, Structure* structure) +{ + auto* stream = new (NotNull, allocateCell(vm)) JSTextDecoderStream(vm, structure); + stream->finishCreation(vm); + return stream; +} + +Structure* JSTextDecoderStream::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSTextDecoderStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSTextDecoderStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSTextDecoderStreamPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSTextDecoderStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSTextDecoderStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSTextDecoderStream::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForTextDecoderStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTextDecoderStream = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForTextDecoderStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForTextDecoderStream = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSTextDecoderStream); + +template +void JSTextDecoderStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_transform); + visitor.append(thisObject->m_decoder); +} + +// Prototype accessors + +JSC_DEFINE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSTextDecoderStream::getConstructor(vm, prototype->globalObject())); +} + +// The `encoding` / `fatal` / `ignoreBOM` getters delegate to the wrapped TextDecoder. +static EncodedJSValue textDecoderStreamDelegatedGetter(JSGlobalObject* lexicalGlobalObject, EncodedJSValue thisValue, const Identifier& property, ASCIILiteral attributeName) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return throwThisTypeError(*lexicalGlobalObject, scope, "TextDecoderStream"_s, attributeName); + RELEASE_AND_RETURN(scope, JSValue::encode(stream->m_decoder->get(lexicalGlobalObject, property))); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_encoding, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + return textDecoderStreamDelegatedGetter(lexicalGlobalObject, thisValue, builtinNames(JSC::getVM(lexicalGlobalObject)).encodingPublicName(), "encoding"_s); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_fatal, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + return textDecoderStreamDelegatedGetter(lexicalGlobalObject, thisValue, builtinNames(JSC::getVM(lexicalGlobalObject)).fatalPublicName(), "fatal"_s); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_ignoreBOM, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + return textDecoderStreamDelegatedGetter(lexicalGlobalObject, thisValue, builtinNames(JSC::getVM(lexicalGlobalObject)).ignoreBOMPublicName(), "ignoreBOM"_s); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_readable, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "TextDecoderStream"_s); + return JSValue::encode(stream->m_transform->m_readable.get()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextDecoderStreamPrototypeGetter_writable, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "TextDecoderStream"_s); + return JSValue::encode(stream->m_transform->m_writable.get()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSTextDecoderStream; + +// `decoder.decode(input, { stream })` on the wrapped TextDecoder. Runs no user JS: the +// method lives on the TextDecoder's internal prototype. Empty return = it threw. +static JSValue invokeDecode(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* decoder, JSValue input, bool streaming) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto& names = WebCore::builtinNames(vm); + + auto* decodeOptions = constructEmptyObject(globalObject); + decodeOptions->putDirect(vm, names.streamPublicName(), jsBoolean(streaming)); + + JSValue method = decoder->get(globalObject, names.decodePublicName()); + RETURN_IF_EXCEPTION(scope, {}); + auto callData = getCallData(method); + if (callData.type == CallData::Type::None) [[unlikely]] { + throwTypeError(globalObject, scope, "TextDecoder.prototype.decode is not callable"_s); + return {}; + } + MarkedArgumentBuffer args; + args.append(input); + args.append(decodeOptions); + ASSERT(!args.hasOverflowed()); + RELEASE_AND_RETURN(scope, call(globalObject, method, callData, decoder, args)); +} + +// Decodes, then enqueues the decoded string if non-empty; an abrupt decode completion +// becomes a rejected promise. Shared by the transform and flush arms. +static JSPromise* decodeAndEnqueue(JSGlobalObject* globalObject, JSTextDecoderStream* stream, JSTransformStreamDefaultController* controller, JSValue input, bool streaming) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue decoded; + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + decoded = invokeDecode(vm, globalObject, stream->m_decoder.get(), input, streaming); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (decoded.isEmpty()) + return nullptr; + + if (decoded.isString() && asString(decoded)->length()) { + transformStreamDefaultControllerEnqueue(globalObject, controller, decoded); + RETURN_IF_EXCEPTION(scope, nullptr); + } + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +JSPromise* textDecoderStreamTransform(JSGlobalObject* globalObject, JSTextDecoderStream* stream, JSTransformStreamDefaultController* controller, JSValue chunk) +{ + return decodeAndEnqueue(globalObject, stream, controller, chunk, /* streaming */ true); +} + +JSPromise* textDecoderStreamFlush(JSGlobalObject* globalObject, JSTextDecoderStream* stream, JSTransformStreamDefaultController* controller) +{ + return decodeAndEnqueue(globalObject, stream, controller, jsUndefined(), /* streaming */ false); +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSTextDecoderStream.h b/src/jsc/bindings/webcore/streams/JSTextDecoderStream.h new file mode 100644 index 000000000000..ba3a26c0e661 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTextDecoderStream.h @@ -0,0 +1,58 @@ +// JSTextDecoderStream — the TextDecoderStream instance cell: it is +// TransformerKind::TextDecoder's algorithmContext, and the transform/flush algorithms are +// native code over m_decoder ({stream:true} decodes, then a final {stream:false} flush). +// Non-destructible: the decoder state is held as the TextDecoder WRAPPER CELL (a +// WriteBarrier), not a RefPtr. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include + +namespace WebCore { + +class JSTextDecoderStream final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSTextDecoderStream* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_transform, m_decoder. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // the inner TransformStream (created by createTransformStream with + // TransformerKind::TextDecoder and `this` as the algorithm context). + JSC::WriteBarrier m_transform; + // the native TextDecoder wrapper cell, constructed as + // `new TextDecoder(label, {fatal, ignoreBOM})` at TextDecoderStream construction; the + // `encoding` / `fatal` / `ignoreBOM` getters delegate to it. + JSC::WriteBarrier m_decoder; + +private: + JSTextDecoderStream(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +using JSTextDecoderStreamConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSTextEncoderStream.cpp b/src/jsc/bindings/webcore/streams/JSTextEncoderStream.cpp new file mode 100644 index 000000000000..56d1591acfe7 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTextEncoderStream.cpp @@ -0,0 +1,352 @@ +#include "config.h" +#include "JSTextEncoderStream.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStream.h" +#include "JSStreamsRuntime.h" +#include "JSTransformStream.h" +#include "JSTransformStreamDefaultController.h" +#include "JSWritableStream.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_constructor); +static JSC_DECLARE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_encoding); +static JSC_DECLARE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_readable); +static JSC_DECLARE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_writable); + +class JSTextEncoderStreamPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSTextEncoderStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSTextEncoderStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTextEncoderStreamPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextEncoderStreamPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSTextEncoderStreamPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTextEncoderStreamPrototype, JSTextEncoderStreamPrototype::Base); + +// JSTextEncoderStreamConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSTextEncoderStreamConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSTextEncoderStreamConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSTextEncoderStreamConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSTextEncoderStreamConstructor::subspaceForImpl(JSC::VM&); +template<> void JSTextEncoderStreamConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSTextEncoderStreamConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSTextEncoderStreamConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSTextEncoderStreamConstructor::s_info = { "TextEncoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextEncoderStreamConstructor) }; + +template<> JSValue JSTextEncoderStreamConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSTextEncoderStreamConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSTextEncoderStreamConstructor); + +template<> GCClient::IsoSubspace* JSTextEncoderStreamConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForTextEncoderStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTextEncoderStreamConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForTextEncoderStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForTextEncoderStreamConstructor = std::forward(space); }); +} + +template<> void JSTextEncoderStreamConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "TextEncoderStream"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSTextEncoderStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSTextEncoderStreamConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSTextEncoderStreamConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* stream = JSTextEncoderStream::create(vm, structure); + + // The existing native TextEncoderStreamEncoder owns the lone-surrogate buffering. + MarkedArgumentBuffer noArguments; + auto* encoder = JSC::construct(lexicalGlobalObject, defaultGlobalObject(lexicalGlobalObject)->JSTextEncoderStreamEncoderConstructor(), noArguments, "TextEncoderStreamEncoder is not constructible"_s); + RETURN_IF_EXCEPTION(scope, {}); + stream->m_encoder.set(vm, stream, encoder); + + auto* transform = createTransformStream(lexicalGlobalObject, TransformerKind::TextEncoder, stream, 1, nullptr, 0, nullptr); + RETURN_IF_EXCEPTION(scope, {}); + stream->m_transform.set(vm, stream, transform); + + return JSValue::encode(stream); +} +JSC_ANNOTATE_HOST_FUNCTION(JSTextEncoderStreamConstructorConstruct, JSTextEncoderStreamConstructor::construct); + +// JSTextEncoderStreamPrototype + +static const HashTableValue JSTextEncoderStreamPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextEncoderStreamPrototypeGetter_constructor, 0 } }, + { "encoding"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextEncoderStreamPrototypeGetter_encoding, 0 } }, + { "readable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextEncoderStreamPrototypeGetter_readable, 0 } }, + { "writable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTextEncoderStreamPrototypeGetter_writable, 0 } }, +}; + +const ClassInfo JSTextEncoderStreamPrototype::s_info = { "TextEncoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextEncoderStreamPrototype) }; + +void JSTextEncoderStreamPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSTextEncoderStream::info(), JSTextEncoderStreamPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSTextEncoderStream + +const ClassInfo JSTextEncoderStream::s_info = { "TextEncoderStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTextEncoderStream) }; + +JSTextEncoderStream::JSTextEncoderStream(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSTextEncoderStream::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSTextEncoderStream* JSTextEncoderStream::create(VM& vm, Structure* structure) +{ + auto* stream = new (NotNull, allocateCell(vm)) JSTextEncoderStream(vm, structure); + stream->finishCreation(vm); + return stream; +} + +Structure* JSTextEncoderStream::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSTextEncoderStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSTextEncoderStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSTextEncoderStreamPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSTextEncoderStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSTextEncoderStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSTextEncoderStream::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForTextEncoderStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTextEncoderStream = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForTextEncoderStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForTextEncoderStream = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSTextEncoderStream); + +template +void JSTextEncoderStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_transform); + visitor.append(thisObject->m_encoder); +} + +// Prototype accessors + +JSC_DEFINE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSTextEncoderStream::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_encoding, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "TextEncoderStream"_s); + return JSValue::encode(jsNontrivialString(vm, "utf-8"_s)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_readable, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "TextEncoderStream"_s); + return JSValue::encode(stream->m_transform->m_readable.get()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTextEncoderStreamPrototypeGetter_writable, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "TextEncoderStream"_s); + return JSValue::encode(stream->m_transform->m_writable.get()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSTextEncoderStream; + +// `encoder.encode(chunk)` / `encoder.flush()` on the TextEncoderStreamEncoder cell. Runs no +// user JS: the method lives on the encoder's internal prototype. Empty return = it threw. +static JSValue invokeEncoderMethod(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* encoder, const Identifier& methodName, const MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue method = encoder->get(globalObject, methodName); + RETURN_IF_EXCEPTION(scope, {}); + auto callData = getCallData(method); + if (callData.type == CallData::Type::None) [[unlikely]] { + throwTypeError(globalObject, scope, "TextEncoderStreamEncoder method is not callable"_s); + return {}; + } + RELEASE_AND_RETURN(scope, call(globalObject, method, callData, encoder, args)); +} + +static void enqueueIfNonEmptyView(JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller, JSValue buffer) +{ + auto* view = dynamicDowncast(buffer); + if (!view || !view->length()) + return; + transformStreamDefaultControllerEnqueue(globalObject, controller, buffer); +} + +JSPromise* textEncoderStreamTransform(JSGlobalObject* globalObject, JSTextEncoderStream* stream, JSTransformStreamDefaultController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue buffer; + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + MarkedArgumentBuffer args; + args.append(chunk); + ASSERT(!args.hasOverflowed()); + buffer = invokeEncoderMethod(vm, globalObject, stream->m_encoder.get(), builtinNames(vm).encodePublicName(), args); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (buffer.isEmpty()) + return nullptr; + + enqueueIfNonEmptyView(globalObject, controller, buffer); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +JSPromise* textEncoderStreamFlush(JSGlobalObject* globalObject, JSTextEncoderStream* stream, JSTransformStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + MarkedArgumentBuffer noArguments; + JSValue buffer = invokeEncoderMethod(vm, globalObject, stream->m_encoder.get(), builtinNames(vm).flushPublicName(), noArguments); + RETURN_IF_EXCEPTION(scope, nullptr); + + enqueueIfNonEmptyView(globalObject, controller, buffer); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSTextEncoderStream.h b/src/jsc/bindings/webcore/streams/JSTextEncoderStream.h new file mode 100644 index 000000000000..6b6ec166495d --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTextEncoderStream.h @@ -0,0 +1,55 @@ +// JSTextEncoderStream — the TextEncoderStream instance cell: it is +// TransformerKind::TextEncoder's algorithmContext, and the transform/flush algorithms are +// native code over m_encoder. Non-destructible (the lone-surrogate buffering lives in the +// held TextEncoderStreamEncoder cell, not here). +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include + +namespace WebCore { + +class JSTextEncoderStream final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + static JSTextEncoderStream* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_transform, m_encoder. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // the inner TransformStream (created by createTransformStream with + // TransformerKind::TextEncoder and `this` as the algorithm context). + JSC::WriteBarrier m_transform; + // the existing native TextEncoderStreamEncoder cell (owns the lone-surrogate buffering). + JSC::WriteBarrier m_encoder; + +private: + JSTextEncoderStream(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +using JSTextEncoderStreamConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSTransformStream.cpp b/src/jsc/bindings/webcore/streams/JSTransformStream.cpp new file mode 100644 index 000000000000..5a1c3f57909d --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTransformStream.cpp @@ -0,0 +1,310 @@ +#include "config.h" +#include "JSTransformStream.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStream.h" +#include "JSTransformStreamDefaultController.h" +#include "JSWritableStream.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsTransformStreamPrototypeGetter_readable); +static JSC_DECLARE_CUSTOM_GETTER(jsTransformStreamPrototypeGetter_writable); +static JSC_DECLARE_CUSTOM_GETTER(jsTransformStreamPrototypeGetter_constructor); + +class JSTransformStreamPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSTransformStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSTransformStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTransformStreamPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSTransformStreamPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamPrototype, JSTransformStreamPrototype::Base); + +// JSTransformStreamConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSTransformStreamConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSTransformStreamConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSTransformStreamConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSTransformStreamConstructor::subspaceForImpl(JSC::VM&); +template<> void JSTransformStreamConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSTransformStreamConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSTransformStreamConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSTransformStreamConstructor::s_info = { "TransformStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamConstructor) }; + +template<> JSValue JSTransformStreamConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSTransformStreamConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSTransformStreamConstructor); + +template<> GCClient::IsoSubspace* JSTransformStreamConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForTransformStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTransformStreamConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForTransformStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForTransformStreamConstructor = std::forward(space); }); +} + +template<> void JSTransformStreamConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "TransformStream"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSTransformStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSTransformStreamConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSTransformStreamConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + // `optional object transformer`: missing => null; a present non-object is a TypeError. + JSValue transformer = callFrame->argument(0); + if (transformer.isUndefined()) + transformer = jsNull(); + else if (!transformer.isObject()) + return throwVMTypeError(lexicalGlobalObject, scope, "TransformStream constructor takes an object as first argument"_s); + + // The two QueuingStrategy ARGUMENTS convert (left to right) before the constructor steps. + auto writableStrategy = convertQueuingStrategyDict(lexicalGlobalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + auto readableStrategy = convertQueuingStrategyDict(lexicalGlobalObject, callFrame->argument(2)); + RETURN_IF_EXCEPTION(scope, {}); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* stream = JSTransformStream::create(vm, structure); + + auto transformerDict = convertTransformerDict(lexicalGlobalObject, transformer); + RETURN_IF_EXCEPTION(scope, {}); + if (transformerDict.hasReadableType) + return throwVMRangeError(lexicalGlobalObject, scope, "The transformer's 'readableType' property is reserved and must not be present"_s); + if (transformerDict.hasWritableType) + return throwVMRangeError(lexicalGlobalObject, scope, "The transformer's 'writableType' property is reserved and must not be present"_s); + + double readableHighWaterMark = extractHighWaterMark(lexicalGlobalObject, readableStrategy, 0); + RETURN_IF_EXCEPTION(scope, {}); + auto* readableSizeAlgorithm = extractSizeAlgorithm(readableStrategy); + double writableHighWaterMark = extractHighWaterMark(lexicalGlobalObject, writableStrategy, 1); + RETURN_IF_EXCEPTION(scope, {}); + auto* writableSizeAlgorithm = extractSizeAlgorithm(writableStrategy); + + auto* startPromise = JSPromise::create(vm, lexicalGlobalObject->promiseStructure()); + initializeTransformStream(lexicalGlobalObject, stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm); + RETURN_IF_EXCEPTION(scope, {}); + setUpTransformStreamDefaultControllerFromTransformer(lexicalGlobalObject, stream, transformer, transformerDict); + RETURN_IF_EXCEPTION(scope, {}); + + // A sync throw from the user `start` propagates out of the constructor (startPromise is + // never resolved); otherwise startPromise is resolved with start's return value. + JSValue startResult = jsUndefined(); + if (transformerDict.start) { + auto callData = JSC::getCallData(transformerDict.start); + ASSERT(callData.type != CallData::Type::None); + MarkedArgumentBuffer args; + args.append(stream->m_controller.get()); + ASSERT(!args.hasOverflowed()); + startResult = JSC::call(lexicalGlobalObject, transformerDict.start, callData, transformer, args); + RETURN_IF_EXCEPTION(scope, {}); + } + resolvePromise(lexicalGlobalObject, startPromise, startResult); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(stream); +} +JSC_ANNOTATE_HOST_FUNCTION(JSTransformStreamConstructorConstruct, JSTransformStreamConstructor::construct); + +// JSTransformStreamPrototype + +static const HashTableValue JSTransformStreamPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTransformStreamPrototypeGetter_constructor, 0 } }, + { "readable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTransformStreamPrototypeGetter_readable, 0 } }, + { "writable"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsTransformStreamPrototypeGetter_writable, 0 } }, +}; + +const ClassInfo JSTransformStreamPrototype::s_info = { "TransformStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamPrototype) }; + +void JSTransformStreamPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSTransformStream::info(), JSTransformStreamPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSTransformStream + +const ClassInfo JSTransformStream::s_info = { "TransformStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStream) }; + +JSTransformStream::JSTransformStream(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSTransformStream::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSTransformStream* JSTransformStream::create(VM& vm, Structure* structure) +{ + auto* stream = new (NotNull, allocateCell(vm)) JSTransformStream(vm, structure); + stream->finishCreation(vm); + return stream; +} + +Structure* JSTransformStream::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSTransformStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSTransformStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSTransformStreamPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSTransformStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSTransformStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSTransformStream::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForTransformStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTransformStream = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForTransformStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForTransformStream = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSTransformStream); + +template +void JSTransformStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_readable); + visitor.append(thisObject->m_writable); + visitor.append(thisObject->m_controller); + visitor.append(thisObject->m_backpressureChangePromise); +} + +// Prototype host functions + +JSC_DEFINE_CUSTOM_GETTER(jsTransformStreamPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSTransformStream::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTransformStreamPrototypeGetter_readable, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "TransformStream"_s); + return JSValue::encode(stream->m_readable.get()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTransformStreamPrototypeGetter_writable, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "TransformStream"_s); + return JSValue::encode(stream->m_writable.get()); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSTransformStream.h b/src/jsc/bindings/webcore/streams/JSTransformStream.h new file mode 100644 index 000000000000..b1118faa29fd --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTransformStream.h @@ -0,0 +1,63 @@ +// JSTransformStream — the TransformStream instance cell. Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include +#include + +namespace WebCore { + +class JSTransformStream final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + // Internal (non-user) allocation entry point (createTransformStream). + static JSTransformStream* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_readable, m_writable, m_controller, + // m_backpressureChangePromise. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // [[readable]] + JSC::WriteBarrier m_readable; + // [[writable]] + JSC::WriteBarrier m_writable; + // [[controller]] — exact-typed. + JSC::WriteBarrier m_controller; + // [[backpressureChangePromise]] — fulfilled + replaced every time [[backpressure]] flips. + JSC::WriteBarrier m_backpressureChangePromise; + // [[backpressure]] — InitializeTransformStream sets it (to true) before anything reads it, + // so the spec's initial "undefined" state needs no separate representation. + bool m_backpressure { false }; + // [[Detached]] (transferable streams are not implemented; the slot exists) + bool m_detached { false }; + +private: + JSTransformStream(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +using JSTransformStreamConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSTransformStreamDefaultController.cpp b/src/jsc/bindings/webcore/streams/JSTransformStreamDefaultController.cpp new file mode 100644 index 000000000000..258516c91594 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTransformStreamDefaultController.cpp @@ -0,0 +1,417 @@ +#include "config.h" +#include "JSTransformStreamDefaultController.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStream.h" +#include "JSReadableStreamDefaultController.h" +#include "JSStreamsRuntime.h" +#include "JSTextDecoderStream.h" +#include "JSTextEncoderStream.h" +#include "JSTransformStream.h" +#include "WebStreamsInternals.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +// The transform's readable half always carries a default controller. +static JSReadableStreamDefaultController* transformReadableController(JSTransformStream* stream) +{ + auto* readable = stream->m_readable.get(); + ASSERT(readable && readable->m_controllerKind == ControllerKind::Default); + return uncheckedDowncast(readable->m_controller.get()); +} + +// WebIDL callback invoke returning Promise: an abrupt completion becomes a +// rejected promise (a sanctioned completion-record catch). Returns nullptr on VM termination. +static JSPromise* invokePromiseReturningMethod(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* method, JSValue thisValue, const MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue result; + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = getCallData(method); + ASSERT(callData.type != CallData::Type::None); + result = call(globalObject, method, callData, thisValue, args); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (result.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseResolvedWith(globalObject, result)); +} + +// The default [[transformAlgorithm]]: enqueue the chunk unchanged; the enqueue's abrupt +// completion becomes a rejected promise (a sanctioned completion-record catch). +static JSPromise* defaultTransformAlgorithm(JSC::VM& vm, JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller, JSValue chunk) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + transformStreamDefaultControllerEnqueue(globalObject, controller, chunk); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + // takeAbruptCompletion leaves a VM termination pending and returns the empty value. + RETURN_IF_EXCEPTION(scope, nullptr); + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +// The [[transformAlgorithm]] dispatch; the switch is total over TransformerKind. +static JSPromise* performTransformAlgorithm(JSC::VM& vm, JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller, JSValue chunk) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_transformerKind) { + case TransformerKind::JavaScript: + if (JSObject* transformMethod = controller->m_transformMethod.get()) { + MarkedArgumentBuffer args; + args.append(chunk); + args.append(controller); + if (args.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, transformMethod, controller->m_transformer.get(), args)); + } + break; + case TransformerKind::Identity: + break; + case TransformerKind::TextEncoder: + RELEASE_AND_RETURN(scope, textEncoderStreamTransform(globalObject, uncheckedDowncast(controller->m_algorithmContext.get()), controller, chunk)); + case TransformerKind::TextDecoder: + RELEASE_AND_RETURN(scope, textDecoderStreamTransform(globalObject, uncheckedDowncast(controller->m_algorithmContext.get()), controller, chunk)); + } + RELEASE_AND_RETURN(scope, defaultTransformAlgorithm(vm, globalObject, controller, chunk)); +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsTransformStreamDefaultControllerConstructorGetter); +static JSC_DECLARE_CUSTOM_GETTER(jsTransformStreamDefaultControllerPrototypeGetter_desiredSize); +static JSC_DECLARE_HOST_FUNCTION(jsTransformStreamDefaultControllerPrototypeFunction_enqueue); +static JSC_DECLARE_HOST_FUNCTION(jsTransformStreamDefaultControllerPrototypeFunction_error); +static JSC_DECLARE_HOST_FUNCTION(jsTransformStreamDefaultControllerPrototypeFunction_terminate); + +class JSTransformStreamDefaultControllerPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSTransformStreamDefaultControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSTransformStreamDefaultControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSTransformStreamDefaultControllerPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamDefaultControllerPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSTransformStreamDefaultControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSTransformStreamDefaultControllerPrototype, JSTransformStreamDefaultControllerPrototype::Base); + +static const HashTableValue JSTransformStreamDefaultControllerPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsTransformStreamDefaultControllerConstructorGetter, 0 } }, + { "desiredSize"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor, NoIntrinsic, { HashTableValue::GetterSetterType, jsTransformStreamDefaultControllerPrototypeGetter_desiredSize, 0 } }, + { "enqueue"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsTransformStreamDefaultControllerPrototypeFunction_enqueue, 0 } }, + { "error"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsTransformStreamDefaultControllerPrototypeFunction_error, 0 } }, + { "terminate"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsTransformStreamDefaultControllerPrototypeFunction_terminate, 0 } }, +}; + +const ClassInfo JSTransformStreamDefaultControllerPrototype::s_info = { "TransformStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamDefaultControllerPrototype) }; + +void JSTransformStreamDefaultControllerPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSTransformStreamDefaultController::info(), JSTransformStreamDefaultControllerPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +template<> const ClassInfo JSTransformStreamDefaultControllerConstructor::s_info = { "TransformStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamDefaultControllerConstructor) }; + +template<> JSValue JSTransformStreamDefaultControllerConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + UNUSED_PARAM(vm); + return globalObject.functionPrototype(); +} + +template<> void JSTransformStreamDefaultControllerConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "TransformStreamDefaultController"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSTransformStreamDefaultController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +const ClassInfo JSTransformStreamDefaultController::s_info = { "TransformStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSTransformStreamDefaultController) }; + +JSTransformStreamDefaultController::JSTransformStreamDefaultController(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSTransformStreamDefaultController::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSTransformStreamDefaultController* JSTransformStreamDefaultController::create(VM& vm, Structure* structure) +{ + auto* controller = new (NotNull, JSC::allocateCell(vm)) JSTransformStreamDefaultController(vm, structure); + controller->finishCreation(vm); + return controller; +} + +Structure* JSTransformStreamDefaultController::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSTransformStreamDefaultController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSTransformStreamDefaultControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSTransformStreamDefaultControllerPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSTransformStreamDefaultController::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSTransformStreamDefaultController::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSTransformStreamDefaultController::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForTransformStreamDefaultController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForTransformStreamDefaultController = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForTransformStreamDefaultController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForTransformStreamDefaultController = std::forward(space); }); +} + +template +void JSTransformStreamDefaultController::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_finishPromise); + visitor.append(thisObject->m_transformer); + visitor.append(thisObject->m_transformMethod); + visitor.append(thisObject->m_flushMethod); + visitor.append(thisObject->m_cancelMethod); + visitor.append(thisObject->m_algorithmContext); +} + +DEFINE_VISIT_CHILDREN(JSTransformStreamDefaultController); + +// [reaction-convention]: handler(resolutionValue, contextCell). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSPerformTransformRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue rejection = callFrame->argument(0); + auto* controller = uncheckedDowncast(callFrame->argument(1)); + transformStreamError(globalObject, controller->m_stream.get(), rejection); + RETURN_IF_EXCEPTION(scope, {}); + throwException(globalObject, scope, rejection); + return {}; +} + +// Prototype accessors & methods. + +JSC_DEFINE_CUSTOM_GETTER(jsTransformStreamDefaultControllerConstructorGetter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(globalObject, scope); + return JSValue::encode(JSTransformStreamDefaultController::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsTransformStreamDefaultControllerPrototypeGetter_desiredSize, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "TransformStreamDefaultController"_s); + std::optional desiredSize = readableStreamDefaultControllerGetDesiredSize(transformReadableController(thisObject->m_stream.get())); + if (!desiredSize) + return JSValue::encode(jsNull()); + return JSValue::encode(jsNumber(*desiredSize)); +} + +JSC_DEFINE_HOST_FUNCTION(jsTransformStreamDefaultControllerPrototypeFunction_enqueue, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "TransformStreamDefaultController"_s); + transformStreamDefaultControllerEnqueue(globalObject, thisObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsTransformStreamDefaultControllerPrototypeFunction_error, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "TransformStreamDefaultController"_s); + transformStreamDefaultControllerError(globalObject, thisObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsTransformStreamDefaultControllerPrototypeFunction_terminate, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "TransformStreamDefaultController"_s); + transformStreamDefaultControllerTerminate(globalObject, thisObject); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using namespace WebCore; + +void transformStreamDefaultControllerClearAlgorithms(JSTransformStreamDefaultController* controller) +{ + controller->m_transformerKind = TransformerKind::Identity; + controller->m_transformer.clear(); + controller->m_transformMethod.clear(); + controller->m_flushMethod.clear(); + controller->m_cancelMethod.clear(); + controller->m_algorithmContext.clear(); +} + +void transformStreamDefaultControllerEnqueue(JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = controller->m_stream.get(); + auto* readableController = transformReadableController(stream); + if (!readableStreamDefaultControllerCanCloseOrEnqueue(readableController)) { + throwTypeError(globalObject, scope, "Cannot enqueue a chunk into a TransformStream whose readable side is closed or has already requested close"_s); + return; + } + JSValue thrown; + { + // The readable-side enqueue interpreted as a completion record (a sanctioned + // completion-record catch): an abrupt completion errors the WRITABLE side and + // rethrows the readable's stored error. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + readableStreamDefaultControllerEnqueue(globalObject, readableController, chunk); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + // takeAbruptCompletion leaves a VM termination pending and returns the empty value. + RETURN_IF_EXCEPTION(scope, void()); + if (!thrown.isEmpty()) [[unlikely]] { + transformStreamErrorWritableAndUnblockWrite(globalObject, stream, thrown); + RETURN_IF_EXCEPTION(scope, void()); + // The readable is not necessarily Errored here: the user size() callback may have + // closed it before throwing, leaving [[storedError]] unset — then we throw undefined. + JSValue storedError = stream->m_readable.get()->m_storedError.get(); + throwException(globalObject, scope, storedError ? storedError : jsUndefined()); + return; + } + bool backpressure = readableStreamDefaultControllerHasBackpressure(readableController); + if (backpressure != stream->m_backpressure) { + ASSERT(backpressure); + RELEASE_AND_RETURN(scope, transformStreamSetBackpressure(globalObject, stream, true)); + } +} + +void transformStreamDefaultControllerError(JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + RELEASE_AND_RETURN(scope, transformStreamError(globalObject, controller->m_stream.get(), error)); +} + +JSPromise* transformStreamDefaultControllerPerformTransform(JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSPromise* transformPromise = performTransformAlgorithm(vm, globalObject, controller, chunk); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + auto* runtime = JSStreamsRuntime::from(globalObject); + transformPromise->performPromiseThenWithContext(vm, globalObject, jsUndefined(), runtime->onTSPerformTransformRejected(), result, controller); + return result; +} + +void transformStreamDefaultControllerTerminate(JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = controller->m_stream.get(); + readableStreamDefaultControllerClose(globalObject, transformReadableController(stream)); + RETURN_IF_EXCEPTION(scope, void()); + JSObject* error = createTypeError(globalObject, "The TransformStream has been terminated"_s); + RELEASE_AND_RETURN(scope, transformStreamErrorWritableAndUnblockWrite(globalObject, stream, error)); +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSTransformStreamDefaultController.h b/src/jsc/bindings/webcore/streams/JSTransformStreamDefaultController.h new file mode 100644 index 000000000000..b8ed14cd8ce8 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSTransformStreamDefaultController.h @@ -0,0 +1,77 @@ +// JSTransformStreamDefaultController — the TransformStreamDefaultController instance cell. +// Not user-constructible. The algorithm slots are the TransformerKind tag + method/context +// members (no stored closures). Non-destructible (no WTF container). +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMConstructorNotConstructable.h" +#include "JSDOMGlobalObject.h" +#include +#include + +namespace WebCore { + +class JSTransformStreamDefaultController final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + // Internal allocation entry point (setUpTransformStreamDefaultController*). + static JSTransformStreamDefaultController* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream, m_finishPromise, m_transformer, + // m_transformMethod, m_flushMethod, m_cancelMethod, m_algorithmContext. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Spec internal slots + + // [[stream]] + JSC::WriteBarrier m_stream; + // [[finishPromise]] — unpopulated (null) ⇔ neither cancel nor flush has been invoked yet. + JSC::WriteBarrier m_finishPromise; + + // The algorithm machinery — replaces [[transformAlgorithm]], [[flushAlgorithm]], + // [[cancelAlgorithm]]. + + // Which arm runs transform/flush/cancel. + TransformerKind m_transformerKind { TransformerKind::Identity }; + // JavaScript kind only: the user transformer object (the call `this`). + JSC::WriteBarrier m_transformer; + // JavaScript kind only: converted `transform` method; null ⇒ the identity algorithm. + JSC::WriteBarrier m_transformMethod; + // JavaScript kind only: converted `flush` method; null ⇒ the trivial algorithm. + JSC::WriteBarrier m_flushMethod; + // JavaScript kind only: converted `cancel` method; null ⇒ the trivial algorithm. + JSC::WriteBarrier m_cancelMethod; + // NON-JavaScript kinds only: TextEncoder → the JSTextEncoderStream cell; + // TextDecoder → the JSTextDecoderStream cell. + JSC::WriteBarrier m_algorithmContext; + +private: + JSTransformStreamDefaultController(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +// Construct throws `TypeError: Illegal constructor`; the constructor object is still +// installed on globalThis so instanceof / .prototype work. +using JSTransformStreamDefaultControllerConstructor = JSDOMConstructorNotConstructable; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSWritableStream.cpp b/src/jsc/bindings/webcore/streams/JSWritableStream.cpp new file mode 100644 index 000000000000..b18c1dd99aba --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSWritableStream.cpp @@ -0,0 +1,336 @@ +#include "config.h" +#include "JSWritableStream.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSDOMBinding.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSWritableStreamDefaultController.h" +#include "JSWritableStreamDefaultWriter.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_abort); +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_close); +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_getWriter); +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamPrototypeGetter_locked); +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamPrototypeGetter_constructor); + +class JSWritableStreamPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSWritableStreamPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSWritableStreamPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSWritableStreamPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamPrototype, JSWritableStreamPrototype::Base); + +// JSWritableStreamConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWritableStreamConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSWritableStreamConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSWritableStreamConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSWritableStreamConstructor::subspaceForImpl(JSC::VM&); +template<> void JSWritableStreamConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSWritableStreamConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSWritableStreamConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSWritableStreamConstructor::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamConstructor) }; + +template<> JSValue JSWritableStreamConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSWritableStreamConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSWritableStreamConstructor); + +template<> GCClient::IsoSubspace* JSWritableStreamConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForWritableStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStreamConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForWritableStreamConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStreamConstructor = std::forward(space); }); +} + +template<> void JSWritableStreamConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "WritableStream"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSWritableStream::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSWritableStreamConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWritableStreamConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + // `optional object underlyingSink`: missing => null; a present non-object is a TypeError. + JSValue underlyingSink = callFrame->argument(0); + if (underlyingSink.isUndefined()) + underlyingSink = jsNull(); + else if (!underlyingSink.isObject()) + return throwVMTypeError(lexicalGlobalObject, scope, "WritableStream constructor takes an object as first argument"_s); + + // WebIDL converts the strategy ARGUMENT before the constructor steps convert the sink. + auto strategy = convertQueuingStrategyDict(lexicalGlobalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* stream = JSWritableStream::create(vm, structure); + + auto sink = convertUnderlyingSinkDict(lexicalGlobalObject, underlyingSink); + RETURN_IF_EXCEPTION(scope, {}); + if (sink.hasType) + return throwVMRangeError(lexicalGlobalObject, scope, "The underlying sink's 'type' property is reserved and must not be present"_s); + + initializeWritableStream(stream); + auto* sizeAlgorithm = extractSizeAlgorithm(strategy); + double highWaterMark = extractHighWaterMark(lexicalGlobalObject, strategy, 1); + RETURN_IF_EXCEPTION(scope, {}); + setUpWritableStreamDefaultControllerFromUnderlyingSink(lexicalGlobalObject, stream, underlyingSink, sink, highWaterMark, sizeAlgorithm); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(stream); +} +JSC_ANNOTATE_HOST_FUNCTION(JSWritableStreamConstructorConstruct, JSWritableStreamConstructor::construct); + +// JSWritableStreamPrototype + +static const HashTableValue JSWritableStreamPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamPrototypeGetter_constructor, 0 } }, + { "locked"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamPrototypeGetter_locked, 0 } }, + { "abort"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_abort, 0 } }, + { "close"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_close, 0 } }, + { "getWriter"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamPrototypeFunction_getWriter, 0 } }, +}; + +const ClassInfo JSWritableStreamPrototype::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamPrototype) }; + +void JSWritableStreamPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSWritableStream::info(), JSWritableStreamPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSWritableStream + +const ClassInfo JSWritableStream::s_info = { "WritableStream"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStream) }; + +JSWritableStream::JSWritableStream(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSWritableStream::~JSWritableStream() = default; + +void JSWritableStream::destroy(JSCell* cell) +{ + static_cast(cell)->JSWritableStream::~JSWritableStream(); +} + +void JSWritableStream::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSWritableStream* JSWritableStream::create(VM& vm, Structure* structure) +{ + auto* stream = new (NotNull, allocateCell(vm)) JSWritableStream(vm, structure); + stream->finishCreation(vm); + return stream; +} + +Structure* JSWritableStream::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSWritableStream::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSWritableStreamPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSWritableStreamPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSWritableStream::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSWritableStream::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSWritableStream::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForWritableStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStream = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForWritableStream.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStream = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSWritableStream); + +template +void JSWritableStream::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_controller); + visitor.append(thisObject->m_writer); + visitor.append(thisObject->m_storedError); + visitor.append(thisObject->m_closeRequest); + visitor.append(thisObject->m_inFlightWriteRequest); + visitor.append(thisObject->m_inFlightCloseRequest); + visitor.append(thisObject->m_pendingAbortRequest.promise); + visitor.append(thisObject->m_pendingAbortRequest.reason); + { + WTF::Locker locker { thisObject->cellLock() }; + for (auto& writeRequest : thisObject->m_writeRequests) + visitor.append(writeRequest); + } +} + +// Prototype host functions + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSWritableStream::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamPrototypeGetter_locked, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(thisValue)); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "WritableStream"_s); + return JSValue::encode(jsBoolean(isWritableStreamLocked(stream))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_abort, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "WritableStream.prototype.abort can only be called on a WritableStream"_s)))); + if (isWritableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "Cannot abort a locked WritableStream"_s)))); + auto* promise = writableStreamAbort(lexicalGlobalObject, stream, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_close, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "WritableStream.prototype.close can only be called on a WritableStream"_s)))); + if (isWritableStreamLocked(stream)) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "Cannot close a locked WritableStream"_s)))); + if (writableStreamCloseQueuedOrInFlight(stream)) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "Cannot close a WritableStream that is already closing"_s)))); + auto* promise = writableStreamClose(lexicalGlobalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamPrototypeFunction_getWriter, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = dynamicDowncast(callFrame->thisValue()); + if (!stream) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "WritableStream"_s); + auto* writer = acquireWritableStreamDefaultWriter(lexicalGlobalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(writer); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSWritableStream.h b/src/jsc/bindings/webcore/streams/JSWritableStream.h new file mode 100644 index 000000000000..845b1b081136 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSWritableStream.h @@ -0,0 +1,96 @@ +// JSWritableStream — the WritableStream instance cell. ONE GC cell IS the stream (there is +// no InternalWritableStream / WritableStream impl split). +// DESTRUCTIBLE (owns the [[writeRequests]] Deque). +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +// WritableStream [[pendingAbortRequest]]. "the slot is undefined" ⇔ `!promise` +// (gate on the barrier, never on a separate bool). Declared here (not WebStreamsInternals.h) +// because it is a member of JSWritableStream. +struct PendingAbortRequest { + JSC::WriteBarrier promise; // "promise" + JSC::WriteBarrier reason; // "reason" + bool wasAlreadyErroring { false }; // "was already erroring" +}; + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +class JSWritableStream final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + // Internal (non-user) allocation entry point (createWritableStream / transform / transfer). + static JSWritableStream* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_controller, m_writer, m_storedError, m_closeRequest, + // m_inFlightWriteRequest, m_inFlightCloseRequest, m_pendingAbortRequest.{promise,reason}, + // and m_writeRequests (a barrier container: UNDER cellLock()). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Spec internal slots + + // [[writeRequests]] — a deque of PROMISES, not of request cells. + // Mutated AND visited under cellLock(). + WTF::Deque, 4> m_writeRequests; + // [[controller]] — exact-typed (only the readable side is erased). + JSC::WriteBarrier m_controller; + // [[writer]] + JSC::WriteBarrier m_writer; + // [[storedError]] — gate reads on m_state. + JSC::WriteBarrier m_storedError; + // [[closeRequest]] + JSC::WriteBarrier m_closeRequest; + // [[inFlightWriteRequest]] + JSC::WriteBarrier m_inFlightWriteRequest; + // [[inFlightCloseRequest]] + JSC::WriteBarrier m_inFlightCloseRequest; + // [[pendingAbortRequest]] — "undefined" ⇔ !m_pendingAbortRequest.promise. + Bun::WebStreams::PendingAbortRequest m_pendingAbortRequest; + // [[state]] + WritableStreamState m_state { WritableStreamState::Writable }; + // [[backpressure]] + bool m_backpressure { false }; + // [[Detached]] (transferable streams are not implemented; the slot exists) + bool m_detached { false }; + +private: + JSWritableStream(JSC::VM&, JSC::Structure*); + ~JSWritableStream(); + void finishCreation(JSC::VM&); +}; + +using JSWritableStreamConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultController.cpp b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultController.cpp new file mode 100644 index 000000000000..cf4d9727f4f9 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultController.cpp @@ -0,0 +1,632 @@ +#include "config.h" +#include "JSWritableStreamDefaultController.h" + +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "JSAbortController.h" +#include "JSAbortSignal.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSStreamsRuntime.h" +#include "JSTransformStream.h" +#include "JSWritableStream.h" +#include "WebStreamsInternals.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +// WebIDL "invoke a callback function" with a Promise return type: an abrupt completion is +// converted into a rejected promise (a completion-record conversion), never a synchronous throw. +static JSC::JSPromise* invokePromiseReturningMethod(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSObject* method, JSC::JSValue thisValue, const JSC::MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSC::JSValue result; + JSC::JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = JSC::getCallData(method); + ASSERT(callData.type != JSC::CallData::Type::None); + result = JSC::call(globalObject, method, callData, thisValue, args); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (result.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseResolvedWith(globalObject, result)); +} + +// The [[writeAlgorithm]] dispatch. The reachable SinkKind set on a writable default +// controller is {JavaScript, Nothing, Transform} (CrossRealm: transferable streams are not +// implemented, so setUpCrossRealmTransformWritable never creates one). +static JSC::JSPromise* performWriteAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSC::JSValue chunk) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_algorithms.kind) { + case SinkKind::JavaScript: { + JSC::JSObject* writeMethod = controller->m_algorithms.method1.get(); + if (!writeMethod) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + JSC::MarkedArgumentBuffer args; + args.append(chunk); + args.append(controller); + if (args.hasOverflowed()) [[unlikely]] { + JSC::throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, writeMethod, controller->m_algorithms.underlyingObject.get(), args)); + } + case SinkKind::Nothing: + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + case SinkKind::Transform: + RELEASE_AND_RETURN(scope, transformStreamDefaultSinkWriteAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()), chunk)); + case SinkKind::CrossRealm: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +// The [[closeAlgorithm]] dispatch. Same reachable kind set as the write dispatch. +static JSC::JSPromise* performCloseAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_algorithms.kind) { + case SinkKind::JavaScript: { + JSC::JSObject* closeMethod = controller->m_algorithms.method2.get(); + if (!closeMethod) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + JSC::MarkedArgumentBuffer args; + if (args.hasOverflowed()) [[unlikely]] { + JSC::throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, closeMethod, controller->m_algorithms.underlyingObject.get(), args)); + } + case SinkKind::Nothing: + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + case SinkKind::Transform: + RELEASE_AND_RETURN(scope, transformStreamDefaultSinkCloseAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()))); + case SinkKind::CrossRealm: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +// The [[abortAlgorithm]] dispatch. Same reachable kind set as the write dispatch. +static JSC::JSPromise* performAbortAlgorithm(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSC::JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_algorithms.kind) { + case SinkKind::JavaScript: { + JSC::JSObject* abortMethod = controller->m_algorithms.method3.get(); + if (!abortMethod) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + JSC::MarkedArgumentBuffer args; + args.append(reason); + if (args.hasOverflowed()) [[unlikely]] { + JSC::throwOutOfMemoryError(globalObject, scope); + return nullptr; + } + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, abortMethod, controller->m_algorithms.underlyingObject.get(), args)); + } + case SinkKind::Nothing: + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + case SinkKind::Transform: + RELEASE_AND_RETURN(scope, transformStreamDefaultSinkAbortAlgorithm(globalObject, uncheckedDowncast(controller->m_algorithms.algorithmContext.get()), reason)); + case SinkKind::CrossRealm: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultControllerConstructorGetter); +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultControllerPrototypeGetter_signal); +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamDefaultControllerPrototypeFunction_error); + +class JSWritableStreamDefaultControllerPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSWritableStreamDefaultControllerPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSWritableStreamDefaultControllerPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamDefaultControllerPrototype(vm, globalObject, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultControllerPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSWritableStreamDefaultControllerPrototype(JSC::VM& vm, JSC::JSGlobalObject*, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultControllerPrototype, JSWritableStreamDefaultControllerPrototype::Base); + +static const HashTableValue JSWritableStreamDefaultControllerPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultControllerConstructorGetter, 0 } }, + { "signal"_s, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor, NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultControllerPrototypeGetter_signal, 0 } }, + { "error"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamDefaultControllerPrototypeFunction_error, 0 } }, +}; + +const ClassInfo JSWritableStreamDefaultControllerPrototype::s_info = { "WritableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultControllerPrototype) }; + +void JSWritableStreamDefaultControllerPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSWritableStreamDefaultController::info(), JSWritableStreamDefaultControllerPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +template<> const ClassInfo JSWritableStreamDefaultControllerConstructor::s_info = { "WritableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultControllerConstructor) }; + +template<> JSValue JSWritableStreamDefaultControllerConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + UNUSED_PARAM(vm); + return globalObject.functionPrototype(); +} + +template<> void JSWritableStreamDefaultControllerConstructor::initializeProperties(VM& vm, JSDOMGlobalObject& globalObject) +{ + putDirect(vm, vm.propertyNames->length, jsNumber(0), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "WritableStreamDefaultController"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSWritableStreamDefaultController::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); +} + +const ClassInfo JSWritableStreamDefaultController::s_info = { "WritableStreamDefaultController"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultController) }; + +JSWritableStreamDefaultController::JSWritableStreamDefaultController(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +JSWritableStreamDefaultController::~JSWritableStreamDefaultController() = default; + +void JSWritableStreamDefaultController::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSWritableStreamDefaultController* JSWritableStreamDefaultController::create(VM& vm, Structure* structure) +{ + JSWritableStreamDefaultController* controller = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamDefaultController(vm, structure); + controller->finishCreation(vm); + return controller; +} + +void JSWritableStreamDefaultController::destroy(JSCell* cell) +{ + static_cast(cell)->JSWritableStreamDefaultController::~JSWritableStreamDefaultController(); +} + +Structure* JSWritableStreamDefaultController::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSWritableStreamDefaultController::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSWritableStreamDefaultControllerPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSWritableStreamDefaultControllerPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSWritableStreamDefaultController::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSWritableStreamDefaultController::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSWritableStreamDefaultController::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForWritableStreamDefaultController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStreamDefaultController = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForWritableStreamDefaultController.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStreamDefaultController = std::forward(space); }); +} + +template +void JSWritableStreamDefaultController::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_abortController); + visitor.append(thisObject->m_algorithms.underlyingObject); + visitor.append(thisObject->m_algorithms.method1); + visitor.append(thisObject->m_algorithms.method2); + visitor.append(thisObject->m_algorithms.method3); + visitor.append(thisObject->m_algorithms.algorithmContext); + visitor.append(thisObject->m_strategySizeAlgorithm); + // ONE non-recursive cellLock scope covers the barrier container (StreamQueue.h). + WTF::Locker locker { thisObject->cellLock() }; + thisObject->m_queue.visit(locker, visitor); +} + +DEFINE_VISIT_CHILDREN(JSWritableStreamDefaultController); + +// [[AbortSteps]](reason) +JSPromise* JSWritableStreamDefaultController::abortSteps(JSGlobalObject* globalObject, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSPromise* result = performAbortAlgorithm(vm, globalObject, this, reason); + RETURN_IF_EXCEPTION(scope, nullptr); + writableStreamDefaultControllerClearAlgorithms(this); + return result; +} + +// [[ErrorSteps]]() +void JSWritableStreamDefaultController::errorSteps() +{ + WTF::Locker locker { cellLock() }; + m_queue.resetQueue(locker); +} + +// The shared start / sink write / sink close reaction handlers +// ([reaction-convention]; context at argument(1)). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSControllerStartFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + auto* stream = controller->m_stream.get(); + ASSERT(stream->m_state == WritableStreamState::Writable || stream->m_state == WritableStreamState::Erroring); + UNUSED_PARAM(stream); + controller->m_started = true; + writableStreamDefaultControllerAdvanceQueueIfNeeded(globalObject, controller); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSControllerStartRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + auto* stream = controller->m_stream.get(); + ASSERT(stream->m_state == WritableStreamState::Writable || stream->m_state == WritableStreamState::Erroring); + controller->m_started = true; + writableStreamDealWithRejection(globalObject, stream, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSSinkCloseFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + writableStreamFinishInFlightClose(globalObject, controller->m_stream.get()); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSSinkCloseRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + writableStreamFinishInFlightCloseWithError(globalObject, controller->m_stream.get(), callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSSinkWriteFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + auto* stream = controller->m_stream.get(); + writableStreamFinishInFlightWrite(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + WritableStreamState state = stream->m_state; + ASSERT(state == WritableStreamState::Writable || state == WritableStreamState::Erroring); + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.dequeueValue(locker); + } + if (!writableStreamCloseQueuedOrInFlight(stream) && state == WritableStreamState::Writable) { + bool backpressure = writableStreamDefaultControllerGetBackpressure(controller); + writableStreamUpdateBackpressure(globalObject, stream, backpressure); + RETURN_IF_EXCEPTION(scope, {}); + } + writableStreamDefaultControllerAdvanceQueueIfNeeded(globalObject, controller); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSSinkWriteRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = dynamicDowncast(callFrame->argument(1)); + if (!controller) [[unlikely]] + return JSValue::encode(jsUndefined()); + auto* stream = controller->m_stream.get(); + if (stream->m_state == WritableStreamState::Writable) + writableStreamDefaultControllerClearAlgorithms(controller); + writableStreamFinishInFlightWriteWithError(globalObject, stream, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// Prototype accessors & methods. + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultControllerConstructorGetter, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(globalObject, scope); + return JSValue::encode(JSWritableStreamDefaultController::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultControllerPrototypeGetter_signal, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(JSValue::decode(thisValue)); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "WritableStreamDefaultController"_s); + auto* jsAbortController = uncheckedDowncast(thisObject->m_abortController.get()); + RELEASE_AND_RETURN(scope, JSValue::encode(toJS(globalObject, jsAbortController->globalObject(), jsAbortController->wrapped().signal()))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamDefaultControllerPrototypeFunction_error, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* thisObject = dynamicDowncast(callFrame->thisValue()); + if (!thisObject) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, globalObject, "WritableStreamDefaultController"_s); + if (thisObject->m_stream->m_state != WritableStreamState::Writable) + return JSValue::encode(jsUndefined()); + writableStreamDefaultControllerError(globalObject, thisObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using namespace WebCore; + +void writableStreamDefaultControllerAdvanceQueueIfNeeded(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = controller->m_stream.get(); + if (!controller->m_started) + return; + if (stream->m_inFlightWriteRequest) + return; + WritableStreamState state = stream->m_state; + ASSERT(state != WritableStreamState::Closed && state != WritableStreamState::Errored); + if (state == WritableStreamState::Erroring) + RELEASE_AND_RETURN(scope, writableStreamFinishErroring(globalObject, stream)); + if (controller->m_queue.isEmpty()) + return; + // An EMPTY value barrier is the close sentinel (StreamQueue.h). + JSValue value = controller->m_queue.peekQueueValue(); + if (!value) + RELEASE_AND_RETURN(scope, writableStreamDefaultControllerProcessClose(globalObject, controller)); + RELEASE_AND_RETURN(scope, writableStreamDefaultControllerProcessWrite(globalObject, controller, value)); +} + +void writableStreamDefaultControllerClearAlgorithms(JSWritableStreamDefaultController* controller) +{ + controller->m_algorithms.kind = SinkKind::Nothing; + controller->m_algorithms.underlyingObject.clear(); + controller->m_algorithms.method1.clear(); + controller->m_algorithms.method2.clear(); + controller->m_algorithms.method3.clear(); + controller->m_algorithms.algorithmContext.clear(); + controller->m_strategySizeAlgorithm.clear(); +} + +void writableStreamDefaultControllerClose(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + // The close sentinel: an EMPTY value with size 0 (never throws). + controller->m_queue.enqueueValueWithSize(globalObject, controller, JSValue(), 0); + scope.assertNoException(); + RELEASE_AND_RETURN(scope, writableStreamDefaultControllerAdvanceQueueIfNeeded(globalObject, controller)); +} + +void writableStreamDefaultControllerError(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = controller->m_stream.get(); + ASSERT(stream->m_state == WritableStreamState::Writable); + writableStreamDefaultControllerClearAlgorithms(controller); + RELEASE_AND_RETURN(scope, writableStreamStartErroring(globalObject, stream, error)); +} + +void writableStreamDefaultControllerErrorIfNeeded(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (controller->m_stream->m_state != WritableStreamState::Writable) + return; + RELEASE_AND_RETURN(scope, writableStreamDefaultControllerError(globalObject, controller, error)); +} + +bool writableStreamDefaultControllerGetBackpressure(JSWritableStreamDefaultController* controller) +{ + return writableStreamDefaultControllerGetDesiredSize(controller) <= 0; +} + +double writableStreamDefaultControllerGetChunkSize(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + // null covers BOTH the default `() => 1` algorithm and the cleared (undefined) slot; + // both return 1 without running user JS. + auto* sizeAlgorithm = controller->m_strategySizeAlgorithm.get(); + if (!sizeAlgorithm) + return 1; + + // "interpreting the result as a completion record": the size() call AND the WebIDL + // `unrestricted double` conversion of its return value (the sanctioned size() catch family). + double size = 1; + JSValue thrown; + bool abrupt = false; + MarkedArgumentBuffer args; + args.append(chunk); + ASSERT(!args.hasOverflowed()); + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = getCallData(sizeAlgorithm); + ASSERT(callData.type != CallData::Type::None); + JSValue returnValue = call(globalObject, sizeAlgorithm, callData, jsUndefined(), args); + if (!catchScope.exception()) + size = returnValue.toNumber(globalObject); + if (catchScope.exception()) [[unlikely]] { + abrupt = true; + thrown = takeAbruptCompletion(globalObject, catchScope); + } + } + if (abrupt) [[unlikely]] { + // A VM termination is never consumed: it is still pending on the scope. + if (thrown.isEmpty()) + return 1; + writableStreamDefaultControllerErrorIfNeeded(globalObject, controller, thrown); + RETURN_IF_EXCEPTION(scope, 1); + return 1; + } + return size; +} + +double writableStreamDefaultControllerGetDesiredSize(JSWritableStreamDefaultController* controller) +{ + return controller->m_strategyHWM - controller->m_queue.totalSize(); +} + +void writableStreamDefaultControllerProcessClose(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = controller->m_stream.get(); + writableStreamMarkCloseRequestInFlight(vm, stream); + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.dequeueValue(locker); + } + ASSERT(controller->m_queue.isEmpty()); + JSPromise* sinkClosePromise = performCloseAlgorithm(vm, globalObject, controller); + RETURN_IF_EXCEPTION(scope, ); + writableStreamDefaultControllerClearAlgorithms(controller); + auto* runtime = JSStreamsRuntime::from(globalObject); + sinkClosePromise->performPromiseThenWithContext(vm, globalObject, runtime->onWSSinkCloseFulfilled(), runtime->onWSSinkCloseRejected(), jsUndefined(), controller); +} + +void writableStreamDefaultControllerProcessWrite(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + writableStreamMarkFirstWriteRequestInFlight(vm, controller->m_stream.get()); + JSPromise* sinkWritePromise = performWriteAlgorithm(vm, globalObject, controller, chunk); + RETURN_IF_EXCEPTION(scope, ); + auto* runtime = JSStreamsRuntime::from(globalObject); + sinkWritePromise->performPromiseThenWithContext(vm, globalObject, runtime->onWSSinkWriteFulfilled(), runtime->onWSSinkWriteRejected(), jsUndefined(), controller); +} + +void writableStreamDefaultControllerWrite(JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSValue chunk, double chunkSize) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + // "If enqueueResult is an abrupt completion" — EnqueueValueWithSize's RangeError on an + // invalid size is interpreted as a completion record (no user JS runs). + JSValue enqueueError; + bool abrupt = false; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + controller->m_queue.enqueueValueWithSize(globalObject, controller, chunk, chunkSize); + if (catchScope.exception()) [[unlikely]] { + abrupt = true; + enqueueError = takeAbruptCompletion(globalObject, catchScope); + } + } + if (abrupt) [[unlikely]] { + // A VM termination is never consumed: it is still pending on the scope. + if (enqueueError.isEmpty()) + return; + RELEASE_AND_RETURN(scope, writableStreamDefaultControllerErrorIfNeeded(globalObject, controller, enqueueError)); + } + + auto* stream = controller->m_stream.get(); + if (!writableStreamCloseQueuedOrInFlight(stream) && stream->m_state == WritableStreamState::Writable) { + bool backpressure = writableStreamDefaultControllerGetBackpressure(controller); + writableStreamUpdateBackpressure(globalObject, stream, backpressure); + RETURN_IF_EXCEPTION(scope, ); + } + RELEASE_AND_RETURN(scope, writableStreamDefaultControllerAdvanceQueueIfNeeded(globalObject, controller)); +} + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultController.h b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultController.h new file mode 100644 index 000000000000..c99bc5707db1 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultController.h @@ -0,0 +1,85 @@ +// JSWritableStreamDefaultController — the WritableStreamDefaultController instance cell. +// Not user-constructible. DESTRUCTIBLE (owns the [[queue]]). +#pragma once + +#include "root.h" +#include "StreamsForward.h" +#include "StreamQueue.h" + +#include "JSDOMConstructorNotConstructable.h" +#include "JSDOMGlobalObject.h" +#include + +namespace WebCore { + +class JSWritableStreamDefaultController final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::NeedsDestruction; + + // Internal allocation entry point (setUpWritableStreamDefaultController*). + static JSWritableStreamDefaultController* create(JSC::VM&, JSC::Structure*); + static void destroy(JSC::JSCell*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream, m_abortController, every barrier inside + // m_algorithms, m_strategySizeAlgorithm, and m_queue (a barrier container: via + // m_queue.visit(locker, visitor) inside ONE `Locker { cellLock() }` scope taken by THIS + // visitChildrenImpl — cellLock() is non-recursive; see StreamQueue.h). + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Spec internal slots + + // [[queue]] + [[queueTotalSize]] — the close sentinel is an EMPTY value barrier + // (StreamQueue.h). [[queueTotalSize]] is a double. + Bun::WebStreams::StreamQueue m_queue; + // [[stream]] + JSC::WriteBarrier m_stream; + // [[abortController]] — the JSAbortController wrapper cell (its `signal` is the + // controller's exposed [[signal]]). + JSC::WriteBarrier m_abortController; + // [[strategyHWM]] + double m_strategyHWM { 1 }; + // [[started]] + bool m_started { false }; + + // The algorithm machinery — replaces [[writeAlgorithm]], [[closeAlgorithm]], and + // [[abortAlgorithm]]. See SinkAlgorithmSlots (StreamQueue.h). + Bun::WebStreams::SinkAlgorithmSlots m_algorithms; + + // [[strategySizeAlgorithm]] — null ⇒ the default `() => 1`. + JSC::WriteBarrier m_strategySizeAlgorithm; + + // Internal methods + + // [[AbortSteps]](reason) — userJS: YES (performs the user abort algorithm). + JSC::JSPromise* abortSteps(JSC::JSGlobalObject*, JSC::JSValue reason); + // [[ErrorSteps]]() — ResetQueue only. userJS: no. + void errorSteps(); + +private: + JSWritableStreamDefaultController(JSC::VM&, JSC::Structure*); + ~JSWritableStreamDefaultController(); + void finishCreation(JSC::VM&); +}; + +// Construct throws `TypeError: Illegal constructor`; the constructor object is still +// installed on globalThis so instanceof / .prototype work. +using JSWritableStreamDefaultControllerConstructor = JSDOMConstructorNotConstructable; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultWriter.cpp b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultWriter.cpp new file mode 100644 index 000000000000..e5afaf70c332 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultWriter.cpp @@ -0,0 +1,481 @@ +#include "config.h" +#include "JSWritableStreamDefaultWriter.h" + +#include "BunClientData.h" +#include "DOMClientIsoSubspaces.h" +#include "DOMIsoSubspaces.h" +#include "ErrorCode.h" +#include "JSDOMBinding.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMGlobalObjectInlines.h" +#include "JSDOMWrapperCache.h" +#include "JSStreamPipeToOperation.h" +#include "JSWritableStream.h" +#include "JSWritableStreamDefaultController.h" +#include "WebCoreJSClientData.h" +#include "WebStreamsInternals.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +JSPromise* writableStreamDefaultWriterAbort(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = writer->m_stream.get(); + ASSERT(stream); + RELEASE_AND_RETURN(scope, writableStreamAbort(globalObject, stream, reason)); +} + +JSPromise* writableStreamDefaultWriterClose(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = writer->m_stream.get(); + ASSERT(stream); + RELEASE_AND_RETURN(scope, writableStreamClose(globalObject, stream)); +} + +JSPromise* writableStreamDefaultWriterCloseWithErrorPropagation(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = writer->m_stream.get(); + ASSERT(stream); + auto state = stream->m_state; + if (writableStreamCloseQueuedOrInFlight(stream) || state == WritableStreamState::Closed) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + if (state == WritableStreamState::Errored) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, stream->m_storedError.get())); + ASSERT(state == WritableStreamState::Writable || state == WritableStreamState::Erroring); + RELEASE_AND_RETURN(scope, writableStreamDefaultWriterClose(globalObject, writer)); +} + +void writableStreamDefaultWriterEnsureClosedPromiseRejected(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* closedPromise = writer->m_closedPromise.get(); + if (closedPromise->status() == JSPromise::Status::Pending) { + rejectPromise(globalObject, closedPromise, error); + RETURN_IF_EXCEPTION(scope, ); + } else { + closedPromise = promiseRejectedWith(globalObject, error); + RETURN_IF_EXCEPTION(scope, ); + writer->m_closedPromise.set(vm, writer, closedPromise); + } + markPromiseAsHandled(vm, closedPromise); +} + +void writableStreamDefaultWriterEnsureReadyPromiseRejected(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* readyPromise = writer->m_readyPromise.get(); + if (readyPromise->status() == JSPromise::Status::Pending) { + rejectPromise(globalObject, readyPromise, error); + RETURN_IF_EXCEPTION(scope, ); + } else { + readyPromise = promiseRejectedWith(globalObject, error); + RETURN_IF_EXCEPTION(scope, ); + writer->m_readyPromise.set(vm, writer, readyPromise); + } + markPromiseAsHandled(vm, readyPromise); +} + +// Provably-non-throwing leaf: reads members and does queue arithmetic only. +std::optional writableStreamDefaultWriterGetDesiredSize(JSWritableStreamDefaultWriter* writer) +{ + auto* stream = writer->m_stream.get(); + switch (stream->m_state) { + case WritableStreamState::Errored: + case WritableStreamState::Erroring: + return std::nullopt; + case WritableStreamState::Closed: + return 0; + case WritableStreamState::Writable: + break; + } + return writableStreamDefaultControllerGetDesiredSize(stream->m_controller.get()); +} + +void writableStreamDefaultWriterRelease(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = writer->m_stream.get(); + ASSERT(stream); + ASSERT(stream->m_writer.get() == writer); + JSValue releasedError = Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Writer has been released"_s); + writableStreamDefaultWriterEnsureReadyPromiseRejected(globalObject, writer, releasedError); + RETURN_IF_EXCEPTION(scope, ); + writableStreamDefaultWriterEnsureClosedPromiseRejected(globalObject, writer, releasedError); + RETURN_IF_EXCEPTION(scope, ); + stream->m_writer.clear(); + writer->m_stream.clear(); +} + +JSPromise* writableStreamDefaultWriterWrite(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = writer->m_stream.get(); + ASSERT(stream); + auto* controller = stream->m_controller.get(); + // Runs the user size(); it never throws out, but a VM termination still propagates. + double chunkSize = writableStreamDefaultControllerGetChunkSize(globalObject, controller, chunk); + RETURN_IF_EXCEPTION(scope, nullptr); + if (writer->m_stream.get() != stream) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createTypeError(globalObject, "This WritableStreamDefaultWriter was released while the queuing strategy's size() was running"_s))); + auto state = stream->m_state; + if (state == WritableStreamState::Errored) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, stream->m_storedError.get())); + if (writableStreamCloseQueuedOrInFlight(stream) || state == WritableStreamState::Closed) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createTypeError(globalObject, "Cannot write to a WritableStream that is closing or closed"_s))); + if (state == WritableStreamState::Erroring) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, stream->m_storedError.get())); + ASSERT(state == WritableStreamState::Writable); + auto* promise = writableStreamAddWriteRequest(globalObject, stream); + writableStreamDefaultControllerWrite(globalObject, controller, chunk, chunkSize); + RETURN_IF_EXCEPTION(scope, nullptr); + return promise; +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_abort); +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_close); +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_releaseLock); +static JSC_DECLARE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_write); +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_closed); +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_desiredSize); +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_ready); +static JSC_DECLARE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_constructor); + +class JSWritableStreamDefaultWriterPrototype final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static JSWritableStreamDefaultWriterPrototype* create(JSC::VM& vm, JSDOMGlobalObject* globalObject, JSC::Structure* structure) + { + JSWritableStreamDefaultWriterPrototype* ptr = new (NotNull, JSC::allocateCell(vm)) JSWritableStreamDefaultWriterPrototype(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultWriterPrototype, Base); + return &vm.plainObjectSpace(); + } + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +private: + JSWritableStreamDefaultWriterPrototype(JSC::VM& vm, JSC::Structure* structure) + : JSC::JSNonFinalObject(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; +STATIC_ASSERT_ISO_SUBSPACE_SHARABLE(JSWritableStreamDefaultWriterPrototype, JSWritableStreamDefaultWriterPrototype::Base); + +// JSWritableStreamDefaultWriterConstructor = JSStreamConstructor. + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWritableStreamDefaultWriterConstructor::construct(JSGlobalObject*, CallFrame*); +template<> JSValue JSWritableStreamDefaultWriterConstructor::prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); +template<> void JSWritableStreamDefaultWriterConstructor::finishCreation(JSC::VM&, JSDOMGlobalObject&); +template<> GCClient::IsoSubspace* JSWritableStreamDefaultWriterConstructor::subspaceForImpl(JSC::VM&); +template<> void JSWritableStreamDefaultWriterConstructor::visitChildren(JSCell*, JSC::AbstractSlotVisitor&); +template<> void JSWritableStreamDefaultWriterConstructor::visitChildren(JSCell*, JSC::SlotVisitor&); +template<> +template +void JSWritableStreamDefaultWriterConstructor::visitChildrenImpl(JSCell*, Visitor&); + +template<> const ClassInfo JSWritableStreamDefaultWriterConstructor::s_info = { "WritableStreamDefaultWriter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultWriterConstructor) }; + +template<> JSValue JSWritableStreamDefaultWriterConstructor::prototypeForStructure(JSC::VM& vm, const JSDOMGlobalObject& globalObject) +{ + return globalObject.functionPrototype(); +} + +template<> +template +void JSWritableStreamDefaultWriterConstructor::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_instanceStructure); +} +DEFINE_VISIT_CHILDREN_WITH_MODIFIER(template<>, JSWritableStreamDefaultWriterConstructor); + +template<> GCClient::IsoSubspace* JSWritableStreamDefaultWriterConstructor::subspaceForImpl(JSC::VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForWritableStreamDefaultWriterConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStreamDefaultWriterConstructor = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForWritableStreamDefaultWriterConstructor.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStreamDefaultWriterConstructor = std::forward(space); }); +} + +template<> void JSWritableStreamDefaultWriterConstructor::finishCreation(VM& vm, JSDOMGlobalObject& globalObject) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + putDirect(vm, vm.propertyNames->length, jsNumber(1), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + JSString* nameString = jsNontrivialString(vm, "WritableStreamDefaultWriter"_s); + m_originalName.set(vm, this, nameString); + putDirect(vm, vm.propertyNames->name, nameString, JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum); + putDirect(vm, vm.propertyNames->prototype, JSWritableStreamDefaultWriter::prototype(vm, globalObject), JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::DontDelete); + m_instanceStructure.set(vm, this, getDOMStructure(vm, globalObject)); +} + +static Structure* structureForNewTarget(JSC::VM& vm, JSWritableStreamDefaultWriterConstructor* constructor, JSGlobalObject* lexicalGlobalObject, JSObject* newTarget) +{ + if (newTarget == constructor) [[likely]] + return constructor->instanceStructure(); + + auto scope = DECLARE_THROW_SCOPE(vm); + auto* newTargetGlobalObject = JSC::getFunctionRealm(lexicalGlobalObject, newTarget); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* baseStructure = getDOMStructure(vm, *uncheckedDowncast(newTargetGlobalObject)); + RELEASE_AND_RETURN(scope, JSC::InternalFunction::createSubclassStructure(lexicalGlobalObject, newTarget, baseStructure)); +} + +template<> JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES JSWritableStreamDefaultWriterConstructor::construct(JSGlobalObject* lexicalGlobalObject, CallFrame* callFrame) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* constructor = uncheckedDowncast(callFrame->jsCallee()); + + auto* stream = dynamicDowncast(callFrame->argument(0)); + if (!stream) + return throwVMTypeError(lexicalGlobalObject, scope, "WritableStreamDefaultWriter constructor requires a WritableStream as its first argument"_s); + + auto* structure = structureForNewTarget(vm, constructor, lexicalGlobalObject, asObject(callFrame->newTarget())); + RETURN_IF_EXCEPTION(scope, {}); + auto* writer = JSWritableStreamDefaultWriter::create(vm, structure); + setUpWritableStreamDefaultWriter(lexicalGlobalObject, writer, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(writer); +} +JSC_ANNOTATE_HOST_FUNCTION(JSWritableStreamDefaultWriterConstructorConstruct, JSWritableStreamDefaultWriterConstructor::construct); + +// JSWritableStreamDefaultWriterPrototype + +static const HashTableValue JSWritableStreamDefaultWriterPrototypeTableValues[] = { + { "constructor"_s, static_cast(JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultWriterPrototypeGetter_constructor, 0 } }, + { "closed"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultWriterPrototypeGetter_closed, 0 } }, + { "desiredSize"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultWriterPrototypeGetter_desiredSize, 0 } }, + { "ready"_s, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsWritableStreamDefaultWriterPrototypeGetter_ready, 0 } }, + { "abort"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamDefaultWriterPrototypeFunction_abort, 0 } }, + { "close"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamDefaultWriterPrototypeFunction_close, 0 } }, + { "releaseLock"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamDefaultWriterPrototypeFunction_releaseLock, 0 } }, + { "write"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsWritableStreamDefaultWriterPrototypeFunction_write, 0 } }, +}; + +const ClassInfo JSWritableStreamDefaultWriterPrototype::s_info = { "WritableStreamDefaultWriter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultWriterPrototype) }; + +void JSWritableStreamDefaultWriterPrototype::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + reifyStaticProperties(vm, JSWritableStreamDefaultWriter::info(), JSWritableStreamDefaultWriterPrototypeTableValues, *this); + JSC_TO_STRING_TAG_WITHOUT_TRANSITION(); +} + +// JSWritableStreamDefaultWriter + +const ClassInfo JSWritableStreamDefaultWriter::s_info = { "WritableStreamDefaultWriter"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSWritableStreamDefaultWriter) }; + +JSWritableStreamDefaultWriter::JSWritableStreamDefaultWriter(VM& vm, Structure* structure) + : Base(vm, structure) +{ +} + +void JSWritableStreamDefaultWriter::finishCreation(VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); +} + +JSWritableStreamDefaultWriter* JSWritableStreamDefaultWriter::create(VM& vm, Structure* structure) +{ + auto* writer = new (NotNull, allocateCell(vm)) JSWritableStreamDefaultWriter(vm, structure); + writer->finishCreation(vm); + return writer; +} + +Structure* JSWritableStreamDefaultWriter::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype) +{ + return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info()); +} + +JSObject* JSWritableStreamDefaultWriter::createPrototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + auto* structure = JSWritableStreamDefaultWriterPrototype::createStructure(vm, &globalObject, globalObject.objectPrototype()); + structure->setMayBePrototype(true); + return JSWritableStreamDefaultWriterPrototype::create(vm, &globalObject, structure); +} + +JSObject* JSWritableStreamDefaultWriter::prototype(VM& vm, JSDOMGlobalObject& globalObject) +{ + return getDOMPrototype(vm, globalObject); +} + +JSValue JSWritableStreamDefaultWriter::getConstructor(VM& vm, const JSGlobalObject* globalObject) +{ + return getDOMConstructor(vm, *uncheckedDowncast(globalObject)); +} + +GCClient::IsoSubspace* JSWritableStreamDefaultWriter::subspaceForImpl(VM& vm) +{ + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForWritableStreamDefaultWriter.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForWritableStreamDefaultWriter = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForWritableStreamDefaultWriter.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForWritableStreamDefaultWriter = std::forward(space); }); +} + +DEFINE_VISIT_CHILDREN(JSWritableStreamDefaultWriter); + +template +void JSWritableStreamDefaultWriter::visitChildrenImpl(JSCell* cell, Visitor& visitor) +{ + auto* thisObject = uncheckedDowncast(cell); + ASSERT_GC_OBJECT_INHERITS(thisObject, info()); + Base::visitChildren(thisObject, visitor); + visitor.append(thisObject->m_stream); + visitor.append(thisObject->m_closedPromise); + visitor.append(thisObject->m_readyPromise); + visitor.append(thisObject->m_pipeOperation); +} + +// Prototype accessors and host functions + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_constructor, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* prototype = dynamicDowncast(JSValue::decode(thisValue)); + if (!prototype) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope); + return JSValue::encode(JSWritableStreamDefaultWriter::getConstructor(vm, prototype->globalObject())); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_closed, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto* writer = dynamicDowncast(JSValue::decode(thisValue)); + if (!writer) [[unlikely]] + return JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The 'closed' getter can only be used on a WritableStreamDefaultWriter"_s))); + return JSValue::encode(writer->m_closedPromise.get()); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_desiredSize, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* writer = dynamicDowncast(JSValue::decode(thisValue)); + if (!writer) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "WritableStreamDefaultWriter"_s); + if (!writer->m_stream) + return Bun::throwError(lexicalGlobalObject, scope, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Writer is not bound to a WritableStream"_s); + auto desiredSize = writableStreamDefaultWriterGetDesiredSize(writer); + if (!desiredSize) + return JSValue::encode(jsNull()); + return JSValue::encode(jsNumber(*desiredSize)); +} + +JSC_DEFINE_CUSTOM_GETTER(jsWritableStreamDefaultWriterPrototypeGetter_ready, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName)) +{ + auto* writer = dynamicDowncast(JSValue::decode(thisValue)); + if (!writer) [[unlikely]] + return JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "The 'ready' getter can only be used on a WritableStreamDefaultWriter"_s))); + return JSValue::encode(writer->m_readyPromise.get()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_abort, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* writer = dynamicDowncast(callFrame->thisValue()); + if (!writer) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "WritableStreamDefaultWriter.prototype.abort can only be called on a WritableStreamDefaultWriter"_s)))); + if (!writer->m_stream) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Writer is not bound to a WritableStream"_s)))); + auto* promise = writableStreamDefaultWriterAbort(lexicalGlobalObject, writer, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_close, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* writer = dynamicDowncast(callFrame->thisValue()); + if (!writer) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "WritableStreamDefaultWriter.prototype.close can only be called on a WritableStreamDefaultWriter"_s)))); + auto* stream = writer->m_stream.get(); + if (!stream) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Writer is not bound to a WritableStream"_s)))); + if (writableStreamCloseQueuedOrInFlight(stream)) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "Cannot close a WritableStream that is already closing"_s)))); + auto* promise = writableStreamDefaultWriterClose(lexicalGlobalObject, writer); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_releaseLock, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* writer = dynamicDowncast(callFrame->thisValue()); + if (!writer) [[unlikely]] + return Bun::ERR::INVALID_THIS(scope, lexicalGlobalObject, "WritableStreamDefaultWriter"_s); + auto* stream = writer->m_stream.get(); + if (!stream) + return JSValue::encode(jsUndefined()); + ASSERT(stream->m_writer); + writableStreamDefaultWriterRelease(lexicalGlobalObject, writer); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWritableStreamDefaultWriterPrototypeFunction_write, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(lexicalGlobalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* writer = dynamicDowncast(callFrame->thisValue()); + if (!writer) [[unlikely]] + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, createTypeError(lexicalGlobalObject, "WritableStreamDefaultWriter.prototype.write can only be called on a WritableStreamDefaultWriter"_s)))); + if (!writer->m_stream) + RELEASE_AND_RETURN(scope, JSValue::encode(promiseRejectedWith(lexicalGlobalObject, Bun::createError(lexicalGlobalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Writer is not bound to a WritableStream"_s)))); + auto* promise = writableStreamDefaultWriterWrite(lexicalGlobalObject, writer, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(promise); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultWriter.h b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultWriter.h new file mode 100644 index 000000000000..e6d72fb9c6b0 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/JSWritableStreamDefaultWriter.h @@ -0,0 +1,59 @@ +// JSWritableStreamDefaultWriter — the WritableStreamDefaultWriter instance cell. +// Non-destructible. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include "JSDOMGlobalObject.h" +#include "StreamConstructor.h" +#include +#include + +namespace WebCore { + +class JSWritableStreamDefaultWriter final : public JSC::JSNonFinalObject { +public: + using Base = JSC::JSNonFinalObject; + static constexpr unsigned StructureFlags = Base::StructureFlags; + static constexpr JSC::DestructionMode needsDestruction = JSC::DoesNotNeedDestruction; + + // Internal allocation entry point (acquireWritableStreamDefaultWriter). + static JSWritableStreamDefaultWriter* create(JSC::VM&, JSC::Structure*); + + static JSC::JSObject* createPrototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSObject* prototype(JSC::VM&, JSDOMGlobalObject&); + static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*); + static JSC::Structure* createStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype); + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_stream, m_closedPromise, m_readyPromise, m_pipeOperation. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // [[stream]] — null = released / not attached. + JSC::WriteBarrier m_stream; + // [[closedPromise]] — spec-required at construction; NOT lazy. Replaced on release. + JSC::WriteBarrier m_closedPromise; + // [[readyPromise]] — replaced on backpressure changes / erroring. + JSC::WriteBarrier m_readyPromise; + // The writer→pipe-operation liveness back-edge, set when a pipe acquires this writer and + // cleared in the pipe's "finalize". Visited. + JSC::WriteBarrier m_pipeOperation; + +private: + JSWritableStreamDefaultWriter(JSC::VM&, JSC::Structure*); + void finishCreation(JSC::VM&); +}; + +using JSWritableStreamDefaultWriterConstructor = JSStreamConstructor; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp b/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp new file mode 100644 index 000000000000..29f3e74400b5 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp @@ -0,0 +1,1407 @@ +#include "root.h" +#include "ErrorCode.h" +#include "AsyncStackTrace.h" + +#include "WebStreamsInternals.h" + +#include "BunClientData.h" +#include "BunStreamSource.h" +#include "JSDOMWrapperCache.h" +#include "JSDirectStreamController.h" +#include "JSReadRequest.h" +#include "JSReadableByteStreamController.h" +#include "JSReadableStream.h" +#include "JSReadableStreamAsyncIterator.h" +#include "JSReadableStreamBYOBReader.h" +#include "JSReadableStreamBYOBRequest.h" +#include "JSReadableStreamDefaultController.h" +#include "JSReadableStreamDefaultReader.h" +#include "JSStreamAlgorithmContexts.h" +#include "JSStreamPipeToOperation.h" +#include "JSStreamTeeState.h" +#include "JSStreamsRuntime.h" +#include "JSWritableStream.h" +#include "JSWritableStreamDefaultWriter.h" +#include "ZigGlobalObject.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSStreamsRuntime; + +// Every switch over ControllerKind is TOTAL; these two are the only casts of the erased +// stream->m_controller slot in this file. +static JSReadableStreamDefaultController* defaultControllerOf(JSReadableStream* stream) +{ + switch (stream->m_controllerKind) { + case ControllerKind::Default: + return uncheckedDowncast(stream->m_controller.get()); + case ControllerKind::None: + case ControllerKind::Byte: + case ControllerKind::Direct: + case ControllerKind::NativeSink: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +static JSReadableByteStreamController* byteControllerOf(JSReadableStream* stream) +{ + switch (stream->m_controllerKind) { + case ControllerKind::Byte: + return uncheckedDowncast(stream->m_controller.get()); + case ControllerKind::None: + case ControllerKind::Default: + case ControllerKind::Direct: + case ControllerKind::NativeSink: + break; + } + RELEASE_ASSERT_NOT_REACHED(); + return nullptr; +} + +// The byte tee's mutable reader slot is erased to JSCell; recover the non-polymorphic +// reader base through the two concrete classes. +static JSReadableStreamReaderBase* teeReader(JSStreamTeeState* teeState) +{ + JSCell* cell = teeState->m_reader.get(); + if (auto* byobReader = dynamicDowncast(cell)) + return byobReader; + return uncheckedDowncast(cell); +} + +// [reaction-convention] deferral: runs handler(value, context) as its own microtask, +// carrying the current async context, without allocating a promise. +static void queueReactionJob(JSC::VM& vm, JSGlobalObject* globalObject, JSFunction* handler, JSValue value, JSValue context) +{ + JSValue asyncContext = globalObject->m_asyncContextData.get()->getInternalField(0); + if (asyncContext.isEmpty()) + asyncContext = jsUndefined(); + QueuedTask task { nullptr, InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, handler, asyncContext, value, context }; + vm.queueMicrotask(WTF::move(task)); +} + +// "Let startPromise be a promise resolved with startResult. Upon fulfillment / rejection of +// startPromise, ...". A non-object startResult cannot be a thenable, so no promise is needed. +static void reactToStartResult(JSC::VM& vm, JSGlobalObject* globalObject, JSValue startResult, JSFunction* onFulfilled, JSFunction* onRejected, JSCell* context) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (!startResult.isObject()) { + queueReactionJob(vm, globalObject, onFulfilled, startResult, context); + return; + } + auto* startPromise = promiseResolvedWith(globalObject, startResult); + RETURN_IF_EXCEPTION(scope, void()); + startPromise->performPromiseThenWithContext(vm, globalObject, onFulfilled, onRejected, jsUndefined(), context); + RETURN_IF_EXCEPTION(scope, void()); +} + +// Detaches the reader's request list before dispatch, per the spec's "set to an empty list, +// then iterate". A MarkedArgumentBuffer is the only GC-visible holder once the requests +// leave the visited deque. +template +static void detachReadRequests(JSC::VM& vm, JSGlobalObject* globalObject, Reader* reader, MarkedArgumentBuffer& out) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + { + WTF::Locker locker { reader->cellLock() }; + if constexpr (std::is_same_v) { + for (auto& request : reader->m_readRequests) + out.append(request.get()); + reader->m_readRequests.clear(); + } else { + for (auto& request : reader->m_readIntoRequests) + out.append(request.get()); + reader->m_readIntoRequests.clear(); + } + } + if (out.hasOverflowed()) [[unlikely]] + throwOutOfMemoryError(globalObject, scope); +} + +// InitializeReadableStream(stream) +void initializeReadableStream(JSReadableStream* stream) +{ + stream->m_state = ReadableStreamState::Readable; + stream->m_reader.clear(); + stream->m_storedError.clear(); + stream->m_disturbed = false; +} + +// IsReadableStreamLocked(stream), widened by Bun's reader-less lock states. +bool isReadableStreamLocked(JSReadableStream* stream) +{ + return !!stream->m_reader || stream->m_lockedWithoutReader || stream->nativeHandleDetached(); +} + +// ReadableStreamHasDefaultReader(stream) +bool readableStreamHasDefaultReader(JSReadableStream* stream) +{ + auto* reader = stream->m_reader.get(); + return reader && !reader->isBYOB(); +} + +// ReadableStreamHasBYOBReader(stream) +bool readableStreamHasBYOBReader(JSReadableStream* stream) +{ + auto* reader = stream->m_reader.get(); + return reader && reader->isBYOB(); +} + +// ReadableStreamGetNumReadRequests(stream). NULL-SAFE by design: resolving a read result +// runs user JS (a patched Object.prototype.then) that can release the reader between a +// caller's check and its use, so a missing reader reads as "no pending requests". +size_t readableStreamGetNumReadRequests(JSReadableStream* stream) +{ + if (!readableStreamHasDefaultReader(stream)) [[unlikely]] + return 0; + return static_cast(stream->m_reader.get())->m_readRequests.size(); +} + +// ReadableStreamGetNumReadIntoRequests(stream). Null-safe: see readableStreamGetNumReadRequests. +size_t readableStreamGetNumReadIntoRequests(JSReadableStream* stream) +{ + if (!readableStreamHasBYOBReader(stream)) [[unlikely]] + return 0; + return static_cast(stream->m_reader.get())->m_readIntoRequests.size(); +} + +// ReadableStreamAddReadRequest(stream, readRequest) +void readableStreamAddReadRequest(VM& vm, JSReadableStream* stream, JSReadRequest* readRequest) +{ + ASSERT(readableStreamHasDefaultReader(stream)); + ASSERT(stream->m_state == ReadableStreamState::Readable); + auto* reader = static_cast(stream->m_reader.get()); + WTF::Locker locker { reader->cellLock() }; + reader->m_readRequests.append(WriteBarrier(vm, reader, readRequest)); +} + +// ReadableStreamAddReadIntoRequest(stream, readRequest) +void readableStreamAddReadIntoRequest(VM& vm, JSReadableStream* stream, JSReadIntoRequest* readRequest) +{ + ASSERT(readableStreamHasBYOBReader(stream)); + ASSERT(stream->m_state == ReadableStreamState::Readable || stream->m_state == ReadableStreamState::Closed); + auto* reader = static_cast(stream->m_reader.get()); + WTF::Locker locker { reader->cellLock() }; + reader->m_readIntoRequests.append(WriteBarrier(vm, reader, readRequest)); +} + +// ReadableStreamFulfillReadRequest(stream, chunk, done). A user-installed +// Object.prototype.then can release the reader while an earlier request in the same batch +// is being resolved; its remaining requests were already rejected, so there is nothing to do. +void readableStreamFulfillReadRequest(JSGlobalObject* globalObject, JSReadableStream* stream, JSValue chunk, bool done) +{ + if (!readableStreamHasDefaultReader(stream)) [[unlikely]] + return; + auto* reader = static_cast(stream->m_reader.get()); + if (reader->m_readRequests.isEmpty()) [[unlikely]] + return; + JSReadRequest* readRequest = nullptr; + { + WTF::Locker locker { reader->cellLock() }; + readRequest = reader->m_readRequests.takeFirst().get(); + } + if (done) + readRequest->closeSteps(globalObject); + else + readRequest->chunkSteps(globalObject, chunk); +} + +// ReadableStreamFulfillReadIntoRequest(stream, chunk, done). Null-safe like +// readableStreamFulfillReadRequest: a reader released mid-batch already rejected these. +void readableStreamFulfillReadIntoRequest(JSGlobalObject* globalObject, JSReadableStream* stream, JSArrayBufferView* chunk, bool done) +{ + if (!readableStreamHasBYOBReader(stream)) [[unlikely]] + return; + auto* reader = static_cast(stream->m_reader.get()); + if (reader->m_readIntoRequests.isEmpty()) [[unlikely]] + return; + JSReadIntoRequest* readIntoRequest = nullptr; + { + WTF::Locker locker { reader->cellLock() }; + readIntoRequest = reader->m_readIntoRequests.takeFirst().get(); + } + if (done) + readIntoRequest->closeSteps(globalObject, chunk); + else + readIntoRequest->chunkSteps(globalObject, chunk); +} + +// ReadableStreamClose(stream) +void readableStreamClose(JSGlobalObject* globalObject, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_state == ReadableStreamState::Readable); + stream->m_state = ReadableStreamState::Closed; + auto* reader = stream->m_reader.get(); + if (!reader) + return; + resolvePromise(globalObject, reader->m_closedPromise.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, void()); + if (reader->isBYOB()) + return; + auto* defaultReader = static_cast(reader); + MarkedArgumentBuffer readRequests; + detachReadRequests(vm, globalObject, defaultReader, readRequests); + RETURN_IF_EXCEPTION(scope, void()); + for (size_t i = 0; i < readRequests.size(); ++i) { + uncheckedDowncast(readRequests.at(i))->closeSteps(globalObject); + RETURN_IF_EXCEPTION(scope, void()); + } +} + +void readableStreamCloseIfPossible(JSGlobalObject* globalObject, JSReadableStream* stream) +{ + if (stream->m_state == ReadableStreamState::Readable) + readableStreamClose(globalObject, stream); +} + +// ReadableStreamError(stream, e) +void readableStreamError(JSGlobalObject* globalObject, JSReadableStream* stream, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_state == ReadableStreamState::Readable); + stream->m_state = ReadableStreamState::Errored; + stream->m_storedError.set(vm, stream, error); + auto* reader = stream->m_reader.get(); + if (!reader) + return; + // Errors created inside our own promise reactions have no JavaScript frames; borrow + // the awaiting async function's frames from the promise user code is blocked on: + // reader.read() and byobReader.read(view) promises, the async iterator's ongoing + // (Web IDL-transformed) promise for `for await`, and pipeTo()'s returned promise. + JSPromise* awaited = reader->m_closedPromise.get(); + if (!reader->isBYOB()) { + auto* defaultReader = static_cast(reader); + WTF::Locker locker { defaultReader->cellLock() }; + for (auto& request : defaultReader->m_readRequests) { + JSPromise* found = nullptr; + switch (request->kind()) { + case ReadRequestKind::Promise: + found = dynamicDowncast(request->m_context.get()); + break; + case ReadRequestKind::AsyncIterator: + if (auto* tuple = dynamicDowncast(request->m_context.get())) { + if (auto* iterator = dynamicDowncast(tuple->getInternalField(0))) + found = iterator->m_ongoingPromise.get(); + } + break; + case ReadRequestKind::PipeTo: + if (auto* op = dynamicDowncast(request->m_context.get())) + found = op->m_promise.get(); + break; + default: + break; + } + if (found) { + awaited = found; + break; + } + } + } else { + auto* byobReader = static_cast(reader); + WTF::Locker locker { byobReader->cellLock() }; + for (auto& request : byobReader->m_readIntoRequests) { + if (request->kind() == ReadIntoRequestKind::Promise) { + if (auto* promise = dynamicDowncast(request->m_context.get())) { + awaited = promise; + break; + } + } + } + } + Bun::attachAsyncStackFromPromise(globalObject, error, awaited); + rejectPromise(globalObject, reader->m_closedPromise.get(), error); + RETURN_IF_EXCEPTION(scope, void()); + markPromiseAsHandled(vm, reader->m_closedPromise.get()); + if (!reader->isBYOB()) + RELEASE_AND_RETURN(scope, readableStreamDefaultReaderErrorReadRequests(globalObject, static_cast(reader), error)); + RELEASE_AND_RETURN(scope, readableStreamBYOBReaderErrorReadIntoRequests(globalObject, static_cast(reader), error)); +} + +// ReadableStreamCancel(stream, reason) +JSPromise* readableStreamCancel(JSGlobalObject* globalObject, JSReadableStream* stream, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + + stream->m_disturbed = true; + if (stream->m_state == ReadableStreamState::Closed) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + if (stream->m_state == ReadableStreamState::Errored) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, stream->m_storedError.get())); + + readableStreamClose(globalObject, stream); + RETURN_IF_EXCEPTION(scope, nullptr); + + auto* reader = stream->m_reader.get(); + if (reader && reader->isBYOB()) { + auto* byobReader = static_cast(reader); + MarkedArgumentBuffer readIntoRequests; + detachReadRequests(vm, globalObject, byobReader, readIntoRequests); + RETURN_IF_EXCEPTION(scope, nullptr); + for (size_t i = 0; i < readIntoRequests.size(); ++i) { + uncheckedDowncast(readIntoRequests.at(i))->closeSteps(globalObject, nullptr); + RETURN_IF_EXCEPTION(scope, nullptr); + } + } + + JSPromise* sourceCancelPromise = nullptr; + switch (stream->m_controllerKind) { + case ControllerKind::None: + sourceCancelPromise = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + break; + case ControllerKind::Default: + sourceCancelPromise = defaultControllerOf(stream)->cancelSteps(globalObject, reason); + break; + case ControllerKind::Byte: + sourceCancelPromise = byteControllerOf(stream)->cancelSteps(globalObject, reason); + break; + case ControllerKind::Direct: { + auto* controller = uncheckedDowncast(stream->m_controller.get()); + controller->onClose(globalObject, reason); + RETURN_IF_EXCEPTION(scope, nullptr); + // readableStreamClose above already moved the stream out of Readable, so onClose + // early-returned; a direct read still pending on the controller settles as done here + // (a canceled read resolves with { value: undefined, done: true }). + if (auto* pendingRead = controller->m_pendingRead.get()) { + controller->m_pendingRead.clear(); + JSObject* doneResult = createIteratorResultObject(globalObject, jsUndefined(), true); + RETURN_IF_EXCEPTION(scope, nullptr); + pendingRead->fulfill(vm, doneResult); + RETURN_IF_EXCEPTION(scope, nullptr); + } + sourceCancelPromise = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + break; + } + case ControllerKind::NativeSink: { + auto* sinkController = stream->m_controller.get(); + JSValue closeFunction = sinkController->getIfPropertyExists(globalObject, builtinNames(vm).closePublicName()); + RETURN_IF_EXCEPTION(scope, nullptr); + if (!closeFunction || !closeFunction.isCallable()) { + throwTypeError(globalObject, scope, "The stream's native sink controller has no close method"_s); + return nullptr; + } + auto callData = JSC::getCallData(closeFunction); + MarkedArgumentBuffer args; + args.append(reason); + ASSERT(!args.hasOverflowed()); + JSValue closeResult = JSC::call(globalObject, closeFunction, callData, sinkController, args); + RETURN_IF_EXCEPTION(scope, nullptr); + sourceCancelPromise = promiseResolvedWith(globalObject, closeResult); + break; + } + } + RETURN_IF_EXCEPTION(scope, nullptr); + + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + sourceCancelPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReturnUndefined(), jsUndefined(), result, jsUndefined()); + RETURN_IF_EXCEPTION(scope, nullptr); + return result; +} + +// ReadableStreamReaderGenericInitialize(reader, stream) +void readableStreamReaderGenericInitialize(JSGlobalObject* globalObject, JSReadableStreamReaderBase* reader, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + reader->m_stream.set(vm, reader, stream); + stream->m_reader.set(vm, stream, reader); + switch (stream->m_state) { + case ReadableStreamState::Readable: + reader->m_closedPromise.set(vm, reader, JSPromise::create(vm, globalObject->promiseStructure())); + return; + case ReadableStreamState::Closed: { + auto* closedPromise = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, void()); + reader->m_closedPromise.set(vm, reader, closedPromise); + return; + } + case ReadableStreamState::Errored: { + auto* closedPromise = promiseRejectedWith(globalObject, stream->m_storedError.get()); + RETURN_IF_EXCEPTION(scope, void()); + reader->m_closedPromise.set(vm, reader, closedPromise); + markPromiseAsHandled(vm, closedPromise); + return; + } + } +} + +// ReadableStreamReaderGenericRelease(reader) +void readableStreamReaderGenericRelease(JSGlobalObject* globalObject, JSReadableStreamReaderBase* reader) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = reader->m_stream.get(); + ASSERT(stream); + ASSERT(stream->m_reader.get() == reader); + + JSObject* releaseError = Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Reader released"_s); + RETURN_IF_EXCEPTION(scope, void()); + if (stream->m_state == ReadableStreamState::Readable) { + rejectPromise(globalObject, reader->m_closedPromise.get(), releaseError); + RETURN_IF_EXCEPTION(scope, void()); + } else { + auto* rejected = promiseRejectedWith(globalObject, releaseError); + RETURN_IF_EXCEPTION(scope, void()); + reader->m_closedPromise.set(vm, reader, rejected); + } + markPromiseAsHandled(vm, reader->m_closedPromise.get()); + + switch (stream->m_controllerKind) { + case ControllerKind::None: + case ControllerKind::NativeSink: + break; + case ControllerKind::Direct: { + // A direct stream's in-flight read lives on the controller (not in the reader's + // read-request queue), so releasing the reader must settle it here. + auto* controller = uncheckedDowncast(stream->m_controller.get()); + if (auto* pendingRead = controller->m_pendingRead.get()) { + controller->m_pendingRead.clear(); + JSObject* pendingReadError = Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: Releasing reader"_s); + RETURN_IF_EXCEPTION(scope, void()); + pendingRead->reject(vm, pendingReadError); + RETURN_IF_EXCEPTION(scope, void()); + } + break; + } + case ControllerKind::Default: { + auto* controller = defaultControllerOf(stream); + controller->releaseSteps(); + // Bun: drop the native handle's event-loop ref when its consumer releases the lock. + if (stream->m_nativePtr && controller->m_algorithms.kind == SourceKind::Native) { + auto* adapter = uncheckedDowncast(controller->m_algorithms.algorithmContext.get()); + if (auto* handle = adapter->m_handle.get()) { + JSValue updateRef = handle->getIfPropertyExists(globalObject, builtinNames(vm).updateRefPublicName()); + RETURN_IF_EXCEPTION(scope, void()); + if (updateRef && updateRef.isCallable()) { + auto callData = JSC::getCallData(updateRef); + MarkedArgumentBuffer args; + args.append(jsBoolean(false)); + ASSERT(!args.hasOverflowed()); + JSC::call(globalObject, updateRef, callData, handle, args); + RETURN_IF_EXCEPTION(scope, void()); + } + } + } + break; + } + case ControllerKind::Byte: + byteControllerOf(stream)->releaseSteps(); + break; + } + stream->m_reader.clear(); + reader->m_stream.clear(); +} + +// ReadableStreamReaderGenericCancel(reader, reason) +JSPromise* readableStreamReaderGenericCancel(JSGlobalObject* globalObject, JSReadableStreamReaderBase* reader, JSValue reason) +{ + auto* stream = reader->m_stream.get(); + ASSERT(stream); + return readableStreamCancel(globalObject, stream, reason); +} + +// SetUpReadableStreamDefaultReader(reader, stream) +void setUpReadableStreamDefaultReader(JSGlobalObject* globalObject, JSReadableStreamDefaultReader* reader, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (isReadableStreamLocked(stream)) { + throwException(globalObject, scope, Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream is locked"_s)); + return; + } + RELEASE_AND_RETURN(scope, readableStreamReaderGenericInitialize(globalObject, reader, stream)); +} + +// SetUpReadableStreamBYOBReader(reader, stream) +void setUpReadableStreamBYOBReader(JSGlobalObject* globalObject, JSReadableStreamBYOBReader* reader, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (isReadableStreamLocked(stream)) { + throwException(globalObject, scope, Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: ReadableStream is locked"_s)); + return; + } + if (stream->m_controllerKind != ControllerKind::Byte) { + throwTypeError(globalObject, scope, "A BYOB reader requires a ReadableStream with an underlying byte source"_s); + return; + } + RELEASE_AND_RETURN(scope, readableStreamReaderGenericInitialize(globalObject, reader, stream)); +} + +// AcquireReadableStreamDefaultReader(stream) +JSReadableStreamDefaultReader* acquireReadableStreamDefaultReader(JSGlobalObject* globalObject, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* reader = JSReadableStreamDefaultReader::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + setUpReadableStreamDefaultReader(globalObject, reader, stream); + RETURN_IF_EXCEPTION(scope, nullptr); + return reader; +} + +// AcquireReadableStreamBYOBReader(stream) +JSReadableStreamBYOBReader* acquireReadableStreamBYOBReader(JSGlobalObject* globalObject, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* reader = JSReadableStreamBYOBReader::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + setUpReadableStreamBYOBReader(globalObject, reader, stream); + RETURN_IF_EXCEPTION(scope, nullptr); + return reader; +} + +// SetUpReadableStreamDefaultController steps 1-8. The caller populated the controller's +// algorithm slots; the start reaction (steps 10-12) is registered by the caller. +static void installDefaultController(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStream* stream, JSReadableStreamDefaultController* controller, double highWaterMark) +{ + ASSERT(stream->m_controllerKind == ControllerKind::None && !stream->m_controller); + controller->m_stream.set(vm, controller, stream); + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.resetQueue(locker); + } + controller->m_started = false; + controller->m_closeRequested = false; + controller->m_pullAgain = false; + controller->m_pulling = false; + controller->m_strategyHWM = highWaterMark; + stream->m_controller.set(vm, stream, controller); + stream->m_controllerKind = ControllerKind::Default; +} + +void setUpReadableStreamDefaultController(JSGlobalObject* globalObject, JSReadableStream* stream, JSReadableStreamDefaultController* controller, JSValue startResult, double highWaterMark) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + installDefaultController(vm, globalObject, stream, controller, highWaterMark); + RELEASE_AND_RETURN(scope, reactToStartResult(vm, globalObject, startResult, runtime->onRSDefaultControllerStartFulfilled(), runtime->onRSDefaultControllerStartRejected(), controller)); +} + +void setUpReadableStreamDefaultControllerFromUnderlyingSource(JSGlobalObject* globalObject, JSReadableStream* stream, JSValue underlyingSource, const UnderlyingSourceDict& dict, double highWaterMark, JSObject* sizeAlgorithm) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* domGlobalObject = defaultGlobalObject(globalObject); + + auto* controller = JSReadableStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = SourceKind::JavaScript; + controller->m_algorithms.underlyingObject.set(vm, controller, underlyingSource); + if (dict.pull) + controller->m_algorithms.method1.set(vm, controller, asObject(dict.pull)); + if (dict.cancel) + controller->m_algorithms.method2.set(vm, controller, asObject(dict.cancel)); + if (sizeAlgorithm) + controller->m_strategySizeAlgorithm.set(vm, controller, sizeAlgorithm); + + installDefaultController(vm, globalObject, stream, controller, highWaterMark); + + JSValue startResult = jsUndefined(); + if (dict.start) { + auto callData = JSC::getCallData(dict.start); + MarkedArgumentBuffer args; + args.append(controller); + ASSERT(!args.hasOverflowed()); + startResult = JSC::call(globalObject, dict.start, callData, underlyingSource, args); + RETURN_IF_EXCEPTION(scope, void()); + } + RELEASE_AND_RETURN(scope, reactToStartResult(vm, globalObject, startResult, runtime->onRSDefaultControllerStartFulfilled(), runtime->onRSDefaultControllerStartRejected(), controller)); +} + +// SetUpReadableByteStreamController steps 1-13. +static void installByteController(JSC::VM& vm, JSGlobalObject* globalObject, JSReadableStream* stream, JSReadableByteStreamController* controller, double highWaterMark, std::optional autoAllocateChunkSize) +{ + ASSERT(stream->m_controllerKind == ControllerKind::None && !stream->m_controller); + if (autoAllocateChunkSize) + ASSERT(*autoAllocateChunkSize > 0); + controller->m_stream.set(vm, controller, stream); + controller->m_pullAgain = false; + controller->m_pulling = false; + controller->m_byobRequest.clear(); + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.resetQueue(locker); + controller->m_pendingPullIntos.clear(); + } + controller->m_closeRequested = false; + controller->m_started = false; + controller->m_strategyHWM = highWaterMark; + controller->m_autoAllocateChunkSize = autoAllocateChunkSize.value_or(0); + stream->m_controller.set(vm, stream, controller); + stream->m_controllerKind = ControllerKind::Byte; +} + +void setUpReadableByteStreamController(JSGlobalObject* globalObject, JSReadableStream* stream, JSReadableByteStreamController* controller, JSValue startResult, double highWaterMark, std::optional autoAllocateChunkSize) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + installByteController(vm, globalObject, stream, controller, highWaterMark, autoAllocateChunkSize); + RELEASE_AND_RETURN(scope, reactToStartResult(vm, globalObject, startResult, runtime->onRSByteControllerStartFulfilled(), runtime->onRSByteControllerStartRejected(), controller)); +} + +void setUpReadableByteStreamControllerFromUnderlyingSource(JSGlobalObject* globalObject, JSReadableStream* stream, JSValue underlyingSource, const UnderlyingSourceDict& dict, double highWaterMark) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* domGlobalObject = defaultGlobalObject(globalObject); + + auto* controller = JSReadableByteStreamController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = SourceKind::JavaScript; + controller->m_algorithms.underlyingObject.set(vm, controller, underlyingSource); + if (dict.pull) + controller->m_algorithms.method1.set(vm, controller, asObject(dict.pull)); + if (dict.cancel) + controller->m_algorithms.method2.set(vm, controller, asObject(dict.cancel)); + + if (dict.autoAllocateChunkSize && !*dict.autoAllocateChunkSize) { + throwTypeError(globalObject, scope, "autoAllocateChunkSize must be greater than 0"_s); + return; + } + installByteController(vm, globalObject, stream, controller, highWaterMark, dict.autoAllocateChunkSize); + + JSValue startResult = jsUndefined(); + if (dict.start) { + auto callData = JSC::getCallData(dict.start); + MarkedArgumentBuffer args; + args.append(controller); + ASSERT(!args.hasOverflowed()); + startResult = JSC::call(globalObject, dict.start, callData, underlyingSource, args); + RETURN_IF_EXCEPTION(scope, void()); + } + RELEASE_AND_RETURN(scope, reactToStartResult(vm, globalObject, startResult, runtime->onRSByteControllerStartFulfilled(), runtime->onRSByteControllerStartRejected(), controller)); +} + +// CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm[, highWaterMark[, sizeAlgorithm]]) +JSReadableStream* createReadableStream(JSGlobalObject* globalObject, SourceKind kind, JSCell* algorithmContext, JSValue startResult, double highWaterMark, JSObject* sizeAlgorithm) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + ASSERT(isNonNegativeNumber(jsNumber(highWaterMark))); + + auto* stream = JSReadableStream::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + initializeReadableStream(stream); + auto* controller = JSReadableStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = kind; + if (algorithmContext) + controller->m_algorithms.algorithmContext.set(vm, controller, algorithmContext); + if (sizeAlgorithm) + controller->m_strategySizeAlgorithm.set(vm, controller, sizeAlgorithm); + setUpReadableStreamDefaultController(globalObject, stream, controller, startResult, highWaterMark); + RETURN_IF_EXCEPTION(scope, nullptr); + return stream; +} + +// CreateReadableByteStream(startAlgorithm, pullAlgorithm, cancelAlgorithm) +JSReadableStream* createReadableByteStream(JSGlobalObject* globalObject, SourceKind kind, JSCell* algorithmContext) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + + auto* stream = JSReadableStream::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + initializeReadableStream(stream); + auto* controller = JSReadableByteStreamController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = kind; + if (algorithmContext) + controller->m_algorithms.algorithmContext.set(vm, controller, algorithmContext); + setUpReadableByteStreamController(globalObject, stream, controller, jsUndefined(), 0, std::nullopt); + RETURN_IF_EXCEPTION(scope, nullptr); + return stream; +} + +// GetMethod(value, propertyName): a [[Get]] on the boxed value (GetV — legal on primitives), +// yielding undefined for undefined/null and a TypeError only for a non-callable value. +static JSValue getMethodOnValue(JSC::VM& vm, JSGlobalObject* globalObject, JSValue value, PropertyName propertyName, ASCIILiteral notCallableMessage) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue method = value.get(globalObject, propertyName); + RETURN_IF_EXCEPTION(scope, {}); + if (method.isUndefinedOrNull()) + return jsUndefined(); + if (!method.isCallable()) { + throwTypeError(globalObject, scope, notCallableMessage); + return {}; + } + return method; +} + +// GetIterator(obj, ASYNC). JSC's getAsyncIterator requires an object, but GetIterator does not: +// primitives (a string) are valid sync iterables here, so ReadableStream.from("ab") must stream +// its code points. The sync fallback wraps the sync iterator in JSC's AsyncFromSyncIterator. +static IterationRecord getIteratorAsync(JSC::VM& vm, JSGlobalObject* globalObject, JSValue iterable) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + JSValue asyncMethod = getMethodOnValue(vm, globalObject, iterable, vm.propertyNames->asyncIteratorSymbol, "@@asyncIterator must be a function"_s); + RETURN_IF_EXCEPTION(scope, {}); + if (asyncMethod.isUndefined()) { + JSValue syncMethod = getMethodOnValue(vm, globalObject, iterable, vm.propertyNames->iteratorSymbol, "@@iterator must be a function"_s); + RETURN_IF_EXCEPTION(scope, {}); + if (syncMethod.isUndefined()) { + throwTypeError(globalObject, scope, "The argument to ReadableStream.from() is not iterable: it has no @@asyncIterator or @@iterator method"_s); + return {}; + } + auto callData = JSC::getCallData(syncMethod); + JSValue syncIterator = JSC::call(globalObject, syncMethod, callData, iterable, ArgList()); + RETURN_IF_EXCEPTION(scope, {}); + if (!syncIterator.isObject()) { + throwTypeError(globalObject, scope, "The @@iterator method must return an object"_s); + return {}; + } + IterationRecord syncRecord = iteratorDirect(globalObject, syncIterator); + RETURN_IF_EXCEPTION(scope, {}); + auto* asyncFromSyncIterator = JSAsyncFromSyncIterator::create(vm, globalObject->asyncFromSyncIteratorStructure(), syncRecord.iterator, syncRecord.nextMethod); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, iteratorDirect(globalObject, asyncFromSyncIterator)); + } + auto callData = JSC::getCallData(asyncMethod); + JSValue iterator = JSC::call(globalObject, asyncMethod, callData, iterable, ArgList()); + RETURN_IF_EXCEPTION(scope, {}); + if (!iterator.isObject()) { + throwTypeError(globalObject, scope, "The @@asyncIterator method must return an object"_s); + return {}; + } + RELEASE_AND_RETURN(scope, iteratorDirect(globalObject, iterator)); +} + +// ReadableStreamFromIterable(asyncIterable) +JSReadableStream* readableStreamFromIterable(JSGlobalObject* globalObject, JSValue asyncIterable) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* domGlobalObject = defaultGlobalObject(globalObject); + + IterationRecord iteratorRecord = getIteratorAsync(vm, globalObject, asyncIterable); + RETURN_IF_EXCEPTION(scope, nullptr); + + auto* context = WebCore::JSStreamFromIterableContext::create(vm, runtime->fromIterableContextStructure(domGlobalObject)); + context->m_iterator.set(vm, context, asObject(iteratorRecord.iterator)); + context->m_nextMethod.set(vm, context, iteratorRecord.nextMethod); + RELEASE_AND_RETURN(scope, createReadableStream(globalObject, SourceKind::FromIterable, context, jsUndefined(), 0, nullptr)); +} + +// ReadableStream.from's pullAlgorithm. +JSPromise* fromIterablePullAlgorithm(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* context = uncheckedDowncast(controller->m_algorithms.algorithmContext.get()); + IterationRecord iteratorRecord { context->m_iterator.get(), context->m_nextMethod.get() }; + + JSValue nextResult; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + nextResult = iteratorNextExported(globalObject, iteratorRecord); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + } + } + auto* nextPromise = promiseResolvedWith(globalObject, nextResult); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + nextPromise->performPromiseThenWithContext(vm, globalObject, runtime->onFromIterablePullFulfilled(), jsUndefined(), result, controller); + RETURN_IF_EXCEPTION(scope, nullptr); + return result; +} + +// ReadableStream.from's cancelAlgorithm. +JSPromise* fromIterableCancelAlgorithm(JSGlobalObject* globalObject, JSReadableStreamDefaultController* controller, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* context = uncheckedDowncast(controller->m_algorithms.algorithmContext.get()); + JSObject* iterator = context->m_iterator.get(); + + JSValue returnMethod; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + returnMethod = iterator->get(globalObject, vm.propertyNames->returnKeyword); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + } + } + if (returnMethod.isUndefinedOrNull()) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + if (!returnMethod.isCallable()) { + JSObject* notCallable = createTypeError(globalObject, "The async iterator's return property must be callable"_s); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, notCallable)); + } + + JSValue returnResult; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = JSC::getCallData(returnMethod); + MarkedArgumentBuffer args; + args.append(reason); + ASSERT(!args.hasOverflowed()); + returnResult = JSC::call(globalObject, returnMethod, callData, iterator, args); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + } + } + auto* returnPromise = promiseResolvedWith(globalObject, returnResult); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + returnPromise->performPromiseThenWithContext(vm, globalObject, runtime->onFromIterableCancelFulfilled(), jsUndefined(), result, controller); + RETURN_IF_EXCEPTION(scope, nullptr); + return result; +} + +// The [reaction-convention] body of onFromIterablePullFulfilled(iterResult, controller). +static EncodedJSValue fromIterablePullFulfilled(JSGlobalObject* globalObject, JSValue iterResult, JSReadableStreamDefaultController* controller) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!iterResult.isObject()) + return throwVMTypeError(globalObject, scope, "The promise returned by the async iterator's next() method must fulfill with an object"_s); + bool done = iteratorCompleteExported(globalObject, iterResult); + RETURN_IF_EXCEPTION(scope, {}); + if (done) { + readableStreamDefaultControllerClose(globalObject, controller); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); + } + JSValue value = iteratorValue(globalObject, iterResult); + RETURN_IF_EXCEPTION(scope, {}); + readableStreamDefaultControllerEnqueue(globalObject, controller, value); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + +// The [reaction-convention] body of onFromIterableCancelFulfilled(iterResult, controller). +static EncodedJSValue fromIterableCancelFulfilled(JSGlobalObject* globalObject, JSValue iterResult) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!iterResult.isObject()) + return throwVMTypeError(globalObject, scope, "The promise returned by the async iterator's return() method must fulfill with an object"_s); + return JSValue::encode(jsUndefined()); +} + +// Bun: `$structuredCloneForStream(chunk)` — the shared native host function installed as a +// private static global; the default tee's cloneForBranch2 path is its only caller here. +static JSValue structuredCloneChunk(JSC::VM& vm, JSGlobalObject* globalObject, JSValue chunk) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + JSValue cloneFunction = domGlobalObject->get(globalObject, WebCore::builtinNames(vm).structuredCloneForStreamPrivateName()); + RETURN_IF_EXCEPTION(scope, {}); + auto callData = JSC::getCallData(cloneFunction); + MarkedArgumentBuffer args; + args.append(chunk); + ASSERT(!args.hasOverflowed()); + RELEASE_AND_RETURN(scope, JSC::call(globalObject, cloneFunction, callData, jsUndefined(), args)); +} + +// ReadableStreamDefaultTee's shared pullAlgorithm. +JSPromise* defaultTeePullAlgorithm(JSGlobalObject* globalObject, JSStreamTeeState* teeState, uint8_t) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + if (teeState->m_reading) { + teeState->m_readAgain1 = true; + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + } + teeState->m_reading = true; + auto* readRequest = WebCore::JSReadRequest::create(vm, runtime->readRequestStructure(defaultGlobalObject(globalObject)), ReadRequestKind::DefaultTee, teeState); + readableStreamDefaultReaderRead(globalObject, uncheckedDowncast(teeState->m_reader.get()), readRequest); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +// ReadableStreamDefaultTee's cancel1Algorithm / cancel2Algorithm. +JSPromise* defaultTeeCancelAlgorithm(JSGlobalObject* globalObject, JSStreamTeeState* teeState, uint8_t branch, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!branch) { + teeState->m_canceled1 = true; + teeState->m_reason1.set(vm, teeState, reason); + } else { + teeState->m_canceled2 = true; + teeState->m_reason2.set(vm, teeState, reason); + } + if ((!branch && teeState->m_canceled2) || (branch && teeState->m_canceled1)) { + JSArray* compositeReason = constructArrayPair(globalObject, teeState->m_reason1.get(), teeState->m_reason2.get()); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* cancelResult = readableStreamCancel(globalObject, teeState->m_stream.get(), compositeReason); + RETURN_IF_EXCEPTION(scope, nullptr); + resolvePromise(globalObject, teeState->m_cancelPromise.get(), cancelResult); + RETURN_IF_EXCEPTION(scope, nullptr); + } + return teeState->m_cancelPromise.get(); +} + +// The default-tee read request's chunk steps run as a microtask +// (onDefaultTeeReadChunkMicrotask). Each canceled flag is re-read live, as the spec does. +static EncodedJSValue defaultTeeChunkStepsMicrotask(JSGlobalObject* globalObject, JSValue chunk, JSStreamTeeState* teeState) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + teeState->m_readAgain1 = false; + JSValue chunk1 = chunk; + JSValue chunk2 = chunk; + if (!teeState->m_canceled2 && teeState->m_shouldClone) { + JSValue cloneResult; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + cloneResult = structuredCloneChunk(vm, globalObject, chunk2); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return {}; + readableStreamDefaultControllerError(globalObject, defaultControllerOf(teeState->m_branch1.get()), thrown); + RETURN_IF_EXCEPTION(scope, {}); + readableStreamDefaultControllerError(globalObject, defaultControllerOf(teeState->m_branch2.get()), thrown); + RETURN_IF_EXCEPTION(scope, {}); + auto* cancelResult = readableStreamCancel(globalObject, teeState->m_stream.get(), thrown); + RETURN_IF_EXCEPTION(scope, {}); + resolvePromise(globalObject, teeState->m_cancelPromise.get(), cancelResult); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); + } + } + chunk2 = cloneResult; + } + if (!teeState->m_canceled1) { + readableStreamDefaultControllerEnqueue(globalObject, defaultControllerOf(teeState->m_branch1.get()), chunk1); + RETURN_IF_EXCEPTION(scope, {}); + } + if (!teeState->m_canceled2) { + readableStreamDefaultControllerEnqueue(globalObject, defaultControllerOf(teeState->m_branch2.get()), chunk2); + RETURN_IF_EXCEPTION(scope, {}); + } + teeState->m_reading = false; + if (teeState->m_readAgain1) { + defaultTeePullAlgorithm(globalObject, teeState, 0); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(jsUndefined()); +} + +// "Upon rejection of reader.[[closedPromise]] with reason r" (default tee). +static EncodedJSValue defaultTeeReaderClosedRejected(JSGlobalObject* globalObject, JSValue reason, JSStreamTeeState* teeState) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + readableStreamDefaultControllerError(globalObject, defaultControllerOf(teeState->m_branch1.get()), reason); + RETURN_IF_EXCEPTION(scope, {}); + readableStreamDefaultControllerError(globalObject, defaultControllerOf(teeState->m_branch2.get()), reason); + RETURN_IF_EXCEPTION(scope, {}); + if (!teeState->m_canceled1 || !teeState->m_canceled2) { + resolvePromise(globalObject, teeState->m_cancelPromise.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(jsUndefined()); +} + +// ReadableStreamDefaultTee(stream, cloneForBranch2) +std::pair readableStreamDefaultTee(JSGlobalObject* globalObject, JSReadableStream* stream, bool cloneForBranch2) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* domGlobalObject = defaultGlobalObject(globalObject); + std::pair failure { nullptr, nullptr }; + + auto* reader = acquireReadableStreamDefaultReader(globalObject, stream); + RETURN_IF_EXCEPTION(scope, failure); + + auto* teeState = WebCore::JSStreamTeeState::create(vm, runtime->teeStateStructure(domGlobalObject)); + teeState->m_stream.set(vm, teeState, stream); + teeState->m_reader.set(vm, teeState, reader); + teeState->m_shouldClone = cloneForBranch2; + teeState->m_cancelPromise.set(vm, teeState, JSPromise::create(vm, globalObject->promiseStructure())); + + auto* branch1 = createReadableStream(globalObject, SourceKind::TeeBranch, teeState, jsUndefined()); + RETURN_IF_EXCEPTION(scope, failure); + defaultControllerOf(branch1)->m_algorithms.teeBranchIndex = 0; + teeState->m_branch1.set(vm, teeState, branch1); + + auto* branch2 = createReadableStream(globalObject, SourceKind::TeeBranch, teeState, jsUndefined()); + RETURN_IF_EXCEPTION(scope, failure); + defaultControllerOf(branch2)->m_algorithms.teeBranchIndex = 1; + teeState->m_branch2.set(vm, teeState, branch2); + + reader->m_closedPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReturnUndefined(), runtime->onDefaultTeeReaderClosedRejected(), jsUndefined(), teeState); + RETURN_IF_EXCEPTION(scope, failure); + return { branch1, branch2 }; +} + +// ReadableByteStreamTee's forwardReaderError(thisReader). +static void byteTeeForwardReaderError(JSC::VM& vm, JSGlobalObject* globalObject, JSStreamTeeState* teeState, JSReadableStreamReaderBase* thisReader) +{ + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), teeState, thisReader); + thisReader->m_closedPromise->performPromiseThenWithContext(vm, globalObject, runtime->onReturnUndefined(), runtime->onByteTeeReaderClosedRejected(), jsUndefined(), context); +} + +// ReadableByteStreamTee's pullWithDefaultReader. +static void byteTeePullWithDefaultReader(JSC::VM& vm, JSGlobalObject* globalObject, JSStreamTeeState* teeState) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* reader = teeReader(teeState); + if (reader->isBYOB()) { + auto* byobReader = static_cast(reader); + ASSERT(byobReader->m_readIntoRequests.isEmpty()); + readableStreamBYOBReaderRelease(globalObject, byobReader); + RETURN_IF_EXCEPTION(scope, void()); + auto* defaultReader = acquireReadableStreamDefaultReader(globalObject, teeState->m_stream.get()); + RETURN_IF_EXCEPTION(scope, void()); + teeState->m_reader.set(vm, teeState, defaultReader); + byteTeeForwardReaderError(vm, globalObject, teeState, defaultReader); + RETURN_IF_EXCEPTION(scope, void()); + reader = defaultReader; + } + auto* readRequest = WebCore::JSReadRequest::create(vm, runtime->readRequestStructure(defaultGlobalObject(globalObject)), ReadRequestKind::ByteTee, teeState); + RELEASE_AND_RETURN(scope, readableStreamDefaultReaderRead(globalObject, static_cast(reader), readRequest)); +} + +// ReadableByteStreamTee's pullWithBYOBReader(view, forBranch2). +static void byteTeePullWithBYOBReader(JSC::VM& vm, JSGlobalObject* globalObject, JSStreamTeeState* teeState, JSArrayBufferView* view, bool forBranch2) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* reader = teeReader(teeState); + if (!reader->isBYOB()) { + auto* defaultReader = static_cast(reader); + ASSERT(defaultReader->m_readRequests.isEmpty()); + readableStreamDefaultReaderRelease(globalObject, defaultReader); + RETURN_IF_EXCEPTION(scope, void()); + auto* byobReader = acquireReadableStreamBYOBReader(globalObject, teeState->m_stream.get()); + RETURN_IF_EXCEPTION(scope, void()); + teeState->m_reader.set(vm, teeState, byobReader); + byteTeeForwardReaderError(vm, globalObject, teeState, byobReader); + RETURN_IF_EXCEPTION(scope, void()); + reader = byobReader; + } + // The read-into request's chunk/close steps need `forBranch2`; the context is therefore + // the InternalFieldTuple {teeState, forBranch2}. + auto* context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), teeState, jsBoolean(forBranch2)); + auto* readIntoRequest = WebCore::JSReadIntoRequest::create(vm, runtime->readIntoRequestStructure(defaultGlobalObject(globalObject)), ReadIntoRequestKind::ByteTee, context); + RELEASE_AND_RETURN(scope, readableStreamBYOBReaderRead(globalObject, static_cast(reader), view, 1, readIntoRequest)); +} + +// ReadableByteStreamTee's pull1Algorithm / pull2Algorithm. +JSPromise* byteTeePullAlgorithm(JSGlobalObject* globalObject, JSStreamTeeState* teeState, uint8_t branch) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (teeState->m_reading) { + if (!branch) + teeState->m_readAgain1 = true; + else + teeState->m_readAgain2 = true; + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + } + teeState->m_reading = true; + auto* branchStream = branch ? teeState->m_branch2.get() : teeState->m_branch1.get(); + auto* byobRequest = readableByteStreamControllerGetBYOBRequest(globalObject, byteControllerOf(branchStream)); + RETURN_IF_EXCEPTION(scope, nullptr); + if (!byobRequest) + byteTeePullWithDefaultReader(vm, globalObject, teeState); + else + byteTeePullWithBYOBReader(vm, globalObject, teeState, byobRequest->m_view.get(), !!branch); + RETURN_IF_EXCEPTION(scope, nullptr); + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +// ReadableByteStreamTee's cancel1Algorithm / cancel2Algorithm. +JSPromise* byteTeeCancelAlgorithm(JSGlobalObject* globalObject, JSStreamTeeState* teeState, uint8_t branch, JSValue reason) +{ + return defaultTeeCancelAlgorithm(globalObject, teeState, branch, reason); +} + +// The byte tee's default-reader chunk steps microtask (onByteTeeReadChunkMicrotask). +static EncodedJSValue byteTeeChunkStepsMicrotask(JSGlobalObject* globalObject, JSValue chunk, JSStreamTeeState* teeState) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + teeState->m_readAgain1 = false; + teeState->m_readAgain2 = false; + auto* chunk1 = uncheckedDowncast(chunk); + JSArrayBufferView* chunk2 = chunk1; + if (!teeState->m_canceled1 && !teeState->m_canceled2) { + JSUint8Array* cloneResult = nullptr; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + cloneResult = cloneAsUint8Array(globalObject, chunk1); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return {}; + readableByteStreamControllerError(globalObject, byteControllerOf(teeState->m_branch1.get()), thrown); + RETURN_IF_EXCEPTION(scope, {}); + readableByteStreamControllerError(globalObject, byteControllerOf(teeState->m_branch2.get()), thrown); + RETURN_IF_EXCEPTION(scope, {}); + auto* cancelResult = readableStreamCancel(globalObject, teeState->m_stream.get(), thrown); + RETURN_IF_EXCEPTION(scope, {}); + resolvePromise(globalObject, teeState->m_cancelPromise.get(), cancelResult); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); + } + } + chunk2 = cloneResult; + } + if (!teeState->m_canceled1) { + readableByteStreamControllerEnqueue(globalObject, byteControllerOf(teeState->m_branch1.get()), chunk1); + RETURN_IF_EXCEPTION(scope, {}); + } + if (!teeState->m_canceled2) { + readableByteStreamControllerEnqueue(globalObject, byteControllerOf(teeState->m_branch2.get()), chunk2); + RETURN_IF_EXCEPTION(scope, {}); + } + teeState->m_reading = false; + if (teeState->m_readAgain1) { + byteTeePullAlgorithm(globalObject, teeState, 0); + RETURN_IF_EXCEPTION(scope, {}); + } else if (teeState->m_readAgain2) { + byteTeePullAlgorithm(globalObject, teeState, 1); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(jsUndefined()); +} + +// The byte tee's BYOB-reader chunk steps microtask (onByteTeeReadIntoChunkMicrotask). +static EncodedJSValue byteTeeReadIntoChunkStepsMicrotask(JSGlobalObject* globalObject, JSValue chunkValue, InternalFieldTuple* context) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* teeState = uncheckedDowncast(context->getInternalField(0)); + bool forBranch2 = context->getInternalField(1).asBoolean(); + auto* chunk = uncheckedDowncast(chunkValue); + + teeState->m_readAgain1 = false; + teeState->m_readAgain2 = false; + auto* byobBranch = forBranch2 ? teeState->m_branch2.get() : teeState->m_branch1.get(); + auto* otherBranch = forBranch2 ? teeState->m_branch1.get() : teeState->m_branch2.get(); + bool byobCanceled = forBranch2 ? teeState->m_canceled2 : teeState->m_canceled1; + bool otherCanceled = forBranch2 ? teeState->m_canceled1 : teeState->m_canceled2; + + if (!otherCanceled) { + JSUint8Array* clonedChunk = nullptr; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + clonedChunk = cloneAsUint8Array(globalObject, chunk); + if (catchScope.exception()) [[unlikely]] { + JSValue thrown = takeAbruptCompletion(globalObject, catchScope); + if (thrown.isEmpty()) + return {}; + readableByteStreamControllerError(globalObject, byteControllerOf(byobBranch), thrown); + RETURN_IF_EXCEPTION(scope, {}); + readableByteStreamControllerError(globalObject, byteControllerOf(otherBranch), thrown); + RETURN_IF_EXCEPTION(scope, {}); + auto* cancelResult = readableStreamCancel(globalObject, teeState->m_stream.get(), thrown); + RETURN_IF_EXCEPTION(scope, {}); + resolvePromise(globalObject, teeState->m_cancelPromise.get(), cancelResult); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); + } + } + if (!byobCanceled) { + readableByteStreamControllerRespondWithNewView(globalObject, byteControllerOf(byobBranch), chunk); + RETURN_IF_EXCEPTION(scope, {}); + } + readableByteStreamControllerEnqueue(globalObject, byteControllerOf(otherBranch), clonedChunk); + RETURN_IF_EXCEPTION(scope, {}); + } else if (!byobCanceled) { + readableByteStreamControllerRespondWithNewView(globalObject, byteControllerOf(byobBranch), chunk); + RETURN_IF_EXCEPTION(scope, {}); + } + teeState->m_reading = false; + if (teeState->m_readAgain1) { + byteTeePullAlgorithm(globalObject, teeState, 0); + RETURN_IF_EXCEPTION(scope, {}); + } else if (teeState->m_readAgain2) { + byteTeePullAlgorithm(globalObject, teeState, 1); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(jsUndefined()); +} + +// forwardReaderError's rejection handler (onByteTeeReaderClosedRejected). +static EncodedJSValue byteTeeReaderClosedRejected(JSGlobalObject* globalObject, JSValue reason, InternalFieldTuple* context) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* teeState = uncheckedDowncast(context->getInternalField(0)); + if (context->getInternalField(1) != teeState->m_reader.get()) + return JSValue::encode(jsUndefined()); + readableByteStreamControllerError(globalObject, byteControllerOf(teeState->m_branch1.get()), reason); + RETURN_IF_EXCEPTION(scope, {}); + readableByteStreamControllerError(globalObject, byteControllerOf(teeState->m_branch2.get()), reason); + RETURN_IF_EXCEPTION(scope, {}); + if (!teeState->m_canceled1 || !teeState->m_canceled2) { + resolvePromise(globalObject, teeState->m_cancelPromise.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + } + return JSValue::encode(jsUndefined()); +} + +// ReadableByteStreamTee(stream) +std::pair readableByteStreamTee(JSGlobalObject* globalObject, JSReadableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* domGlobalObject = defaultGlobalObject(globalObject); + std::pair failure { nullptr, nullptr }; + ASSERT(stream->m_controllerKind == ControllerKind::Byte); + + auto* reader = acquireReadableStreamDefaultReader(globalObject, stream); + RETURN_IF_EXCEPTION(scope, failure); + + auto* teeState = WebCore::JSStreamTeeState::create(vm, runtime->teeStateStructure(domGlobalObject)); + teeState->m_stream.set(vm, teeState, stream); + teeState->m_reader.set(vm, teeState, reader); + teeState->m_cancelPromise.set(vm, teeState, JSPromise::create(vm, globalObject->promiseStructure())); + + auto* branch1 = createReadableByteStream(globalObject, SourceKind::ByteTeeBranch, teeState); + RETURN_IF_EXCEPTION(scope, failure); + byteControllerOf(branch1)->m_algorithms.teeBranchIndex = 0; + teeState->m_branch1.set(vm, teeState, branch1); + + auto* branch2 = createReadableByteStream(globalObject, SourceKind::ByteTeeBranch, teeState); + RETURN_IF_EXCEPTION(scope, failure); + byteControllerOf(branch2)->m_algorithms.teeBranchIndex = 1; + teeState->m_branch2.set(vm, teeState, branch2); + + byteTeeForwardReaderError(vm, globalObject, teeState, reader); + RETURN_IF_EXCEPTION(scope, failure); + return { branch1, branch2 }; +} + +// ReadableStreamTee(stream, cloneForBranch2) +std::pair readableStreamTee(JSGlobalObject* globalObject, JSReadableStream* stream, bool cloneForBranch2) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + std::pair failure { nullptr, nullptr }; + stream->materializeIfNeeded(globalObject); + RETURN_IF_EXCEPTION(scope, failure); + if (stream->m_controllerKind == ControllerKind::Byte) + RELEASE_AND_RETURN(scope, readableByteStreamTee(globalObject, stream)); + RELEASE_AND_RETURN(scope, readableStreamDefaultTee(globalObject, stream, cloneForBranch2)); +} + +// ReadableStreamPipeTo(source, dest, preventClose, preventAbort, preventCancel[, signal]). +// Validates, allocates + populates the operation cell, then hands it to startPipeToOperation. +JSPromise* readableStreamPipeTo(JSGlobalObject* globalObject, JSReadableStream* source, JSWritableStream* destination, bool preventClose, bool preventAbort, bool preventCancel, JSObject* signal) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* domGlobalObject = defaultGlobalObject(globalObject); + + source->materializeIfNeeded(globalObject); + RETURN_IF_EXCEPTION(scope, nullptr); + ASSERT(!isReadableStreamLocked(source)); + ASSERT(!isWritableStreamLocked(destination)); + + auto* reader = acquireReadableStreamDefaultReader(globalObject, source); + RETURN_IF_EXCEPTION(scope, nullptr); + auto* writer = acquireWritableStreamDefaultWriter(globalObject, destination); + RETURN_IF_EXCEPTION(scope, nullptr); + source->m_disturbed = true; + + auto* operation = WebCore::JSStreamPipeToOperation::create(vm, runtime->pipeToOperationStructure(domGlobalObject)); + operation->m_source.set(vm, operation, source); + operation->m_destination.set(vm, operation, destination); + operation->m_reader.set(vm, operation, reader); + operation->m_writer.set(vm, operation, writer); + operation->m_preventClose = preventClose; + operation->m_preventAbort = preventAbort; + operation->m_preventCancel = preventCancel; + if (signal) + operation->m_signal.set(vm, operation, signal); + operation->m_promise.set(vm, operation, JSPromise::create(vm, globalObject->promiseStructure())); + reader->m_pipeOperation.set(vm, reader, operation); + writer->m_pipeOperation.set(vm, writer, operation); + + startPipeToOperation(globalObject, operation); + RETURN_IF_EXCEPTION(scope, nullptr); + return operation->m_promise.get(); +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +namespace Streams = Bun::WebStreams; + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onFromIterablePullFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto* controller = uncheckedDowncast(callFrame->argument(1)); + return Streams::fromIterablePullFulfilled(globalObject, callFrame->argument(0), controller); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onFromIterableCancelFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return Streams::fromIterableCancelFulfilled(globalObject, callFrame->argument(0)); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onDefaultTeeReadChunkMicrotask, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return Streams::defaultTeeChunkStepsMicrotask(globalObject, callFrame->argument(0), uncheckedDowncast(callFrame->argument(1))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onDefaultTeeReaderClosedRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return Streams::defaultTeeReaderClosedRejected(globalObject, callFrame->argument(0), uncheckedDowncast(callFrame->argument(1))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onByteTeeReadChunkMicrotask, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return Streams::byteTeeChunkStepsMicrotask(globalObject, callFrame->argument(0), uncheckedDowncast(callFrame->argument(1))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onByteTeeReadIntoChunkMicrotask, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return Streams::byteTeeReadIntoChunkStepsMicrotask(globalObject, callFrame->argument(0), uncheckedDowncast(callFrame->argument(1))); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onByteTeeReaderClosedRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + return Streams::byteTeeReaderClosedRejected(globalObject, callFrame->argument(0), uncheckedDowncast(callFrame->argument(1))); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/StreamConstructor.h b/src/jsc/bindings/webcore/streams/StreamConstructor.h new file mode 100644 index 000000000000..17fa45dc332b --- /dev/null +++ b/src/jsc/bindings/webcore/streams/StreamConstructor.h @@ -0,0 +1,66 @@ +// StreamConstructor.h — JSStreamConstructor, the ONE constructor class shared by +// every user-constructible Web Streams class (`using JSFooConstructor = +// JSStreamConstructor;` in each class header). Same shape as WebCore::JSDOMConstructor +// plus the cached instance Structure. Each owner .cpp defines the specialization's s_info, +// visitChildrenImpl, subspaceForImpl, `construct`, and `prototypeForStructure`. +#pragma once + +#include "JSDOMConstructorBase.h" +#include "ErrorCode.h" +#include + +namespace WebCore { + +template +class JSStreamConstructor : public JSDOMConstructorBase { +public: + using Base = JSDOMConstructorBase; + + static JSStreamConstructor* create(JSC::VM& vm, JSC::Structure* structure, JSDOMGlobalObject& globalObject) + { + JSStreamConstructor* constructor = new (NotNull, JSC::allocateCell(vm)) JSStreamConstructor(vm, structure); + constructor->finishCreation(vm, globalObject); + return constructor; + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject& globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, &globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); + } + + DECLARE_INFO; + // visitChildrenImpl MUST visit: m_instanceStructure. + DECLARE_VISIT_CHILDREN; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return subspaceForImpl(vm); + } + static JSC::GCClient::IsoSubspace* subspaceForImpl(JSC::VM&); + + // Must be defined for each specialization class. + static JSC::JSValue prototypeForStructure(JSC::VM&, const JSDOMGlobalObject&); + + // Must be defined for each specialization class. + static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*); + + // The cached instance Structure (from getDOMStructure()), set in finishCreation + // so construct() does zero hashmap lookups. Visited. + JSC::Structure* instanceStructure() const { return m_instanceStructure.get(); } + +private: + JSStreamConstructor(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure, construct, nullptr, errorCodeIfCalled) + { + } + + // Defined for each specialization class (it populates m_instanceStructure). + void finishCreation(JSC::VM&, JSDOMGlobalObject&); + + JSC::WriteBarrier m_instanceStructure; +}; + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/StreamQueue.h b/src/jsc/bindings/webcore/streams/StreamQueue.h new file mode 100644 index 000000000000..f2c55bc3fe9c --- /dev/null +++ b/src/jsc/bindings/webcore/streams/StreamQueue.h @@ -0,0 +1,212 @@ +// StreamQueue.h — the spec's "queue-with-sizes" container, plus the shared algorithm-slot +// structs embedded by value in the controllers. HEADER-ONLY by design: the spec ops +// EnqueueValueWithSize / DequeueValue / PeekQueueValue / ResetQueue are inline methods here. +// +// cellLock DISCIPLINE (same pattern as src/jsc/bindings/WriteBarrierList.h): every mutation +// of `m_queue` AND the visitChildren iteration run under +// `WTF::Locker locker { owner->cellLock() }`, where `owner` is the GC cell embedding this +// queue. The lock is taken by the CALLER and proven by the `const WTF::AbstractLocker&` +// first parameter of every mutator and of visit(), with ONE exception: enqueueValueWithSize +// validates (and can throw, a GC allocation) BEFORE taking the owner's cell lock itself, so +// callers must NOT hold the lock around it (JSCellLock is non-recursive). +// +// *** JSCellLock (`cellLock()`) is NON-RECURSIVE. *** +// An internal-lock design would either deadlock (the owning cell takes cellLock() around +// ALL of its barrier containers and a self-locking queue re-acquires it) or force the +// owner to visit its sibling `Deque>` members OUTSIDE the lock (a +// concurrent-marking race on the deque's backing buffer). The rule is therefore: +// the OWNING cell's visitChildrenImpl takes `Locker locker { cellLock() }` exactly ONCE, +// around ALL of its barrier containers (this queue AND every sibling barrier deque), and +// passes that ONE locker down. Mutating ops on the owner do the same. Keep the locked +// scope tight: never run user JS or GC-allocation-heavy work while holding it. +// +// A `WTF::Deque` member makes the owning cell DESTRUCTIBLE. +// NEVER hold a pointer/reference to an entry across any call that can run user JS — +// re-fetch first() after such a call. +#pragma once + +#include "root.h" +#include "StreamsForward.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +// One entry of a default (value-with-size) queue. +// An EMPTY `value` barrier is the WritableStream close sentinel (`undefined` is a legal +// chunk and must never be conflated with it). +struct ValueWithSize { + JSC::WriteBarrier value; // "value" + double size; // "size" — a double, never an integer +}; + +// One entry of a readable byte stream queue. The buffer is the ArrayBuffer IMPL (always a +// transferred, exclusively-owned block): no JSArrayBuffer wrapper cell exists for it unless +// user code reads `.buffer` off a view handed out over it. +struct ByteQueueEntry { + RefPtr buffer; // "buffer" + size_t byteOffset; // "byte offset" + size_t byteLength; // "byte length" +}; + +// The readable controllers' algorithm slots, embedded BY VALUE as `m_algorithms` by +// JSReadableStreamDefaultController and JSReadableByteStreamController. Replaces the spec's +// [[pullAlgorithm]] and [[cancelAlgorithm]] closures; the start algorithm is never stored. +// The owning cell's visitChildrenImpl MUST visit every barrier inside it. +struct SourceAlgorithmSlots { + // Which arm runs pull/cancel. + SourceKind kind { SourceKind::Nothing }; + // TeeBranch / ByteTeeBranch only: which branch this controller is (0 or 1). + uint8_t teeBranchIndex { 0 }; + // JavaScript kind only: the user underlyingSource object (the call `this`). + JSC::WriteBarrier underlyingObject; + // JavaScript kind only: the converted `pull` method ([[pullAlgorithm]]); + // null ⇒ the trivial algorithm. + JSC::WriteBarrier method1; + // JavaScript kind only: the converted `cancel` method ([[cancelAlgorithm]]); + // null ⇒ the trivial algorithm. + JSC::WriteBarrier method2; + // NON-JavaScript kinds only: Transform → JSTransformStream; TeeBranch/ByteTeeBranch → + // JSStreamTeeState; FromIterable → JSStreamFromIterableContext; CrossRealm → + // JSCrossRealmTransformState; Native → JSNativeStreamSourceAdapter. + JSC::WriteBarrier algorithmContext; +}; + +// The writable controller's algorithm slots, embedded BY VALUE as `m_algorithms` by +// JSWritableStreamDefaultController. Replaces the spec's [[writeAlgorithm]], +// [[closeAlgorithm]], and [[abortAlgorithm]] closures. +// The owning cell's visitChildrenImpl MUST visit every barrier inside it. +struct SinkAlgorithmSlots { + // Which arm runs write/close/abort. + SinkKind kind { SinkKind::Nothing }; + // JavaScript kind only: the user underlyingSink object (the call `this`). + JSC::WriteBarrier underlyingObject; + // JavaScript kind only: the converted `write` method ([[writeAlgorithm]]); + // null ⇒ the trivial algorithm. + JSC::WriteBarrier method1; + // JavaScript kind only: the converted `close` method ([[closeAlgorithm]]); + // null ⇒ the trivial algorithm. + JSC::WriteBarrier method2; + // JavaScript kind only: the converted `abort` method ([[abortAlgorithm]]); + // null ⇒ the trivial algorithm. + JSC::WriteBarrier method3; + // NON-JavaScript kinds only: Transform → JSTransformStream; + // CrossRealm → JSCrossRealmTransformState. + JSC::WriteBarrier algorithmContext; +}; + +// The [[queue]] + [[queueTotalSize]] pair. +// Instantiated as StreamQueue and StreamQueue. +// A `const WTF::AbstractLocker&` parameter proves the CALLER holds the owning cell's +// cellLock(); enqueueValueWithSize is the one self-locking exception (see the class +// comment). `owner` is the embedding GC cell (for the write barrier). +template +class StreamQueue { + WTF_MAKE_NONCOPYABLE(StreamQueue); + +public: + StreamQueue() = default; + + // spec: EnqueueValueWithSize(container, value, size). Throws RangeError if `size` is not + // a non-negative finite number. The size was computed by the CALLER's size algorithm — + // this op runs no user JS. The throw (a GC allocation) happens BEFORE this takes the + // owner's cell lock; only the queue mutation runs under it. (ValueWithSize only.) + void enqueueValueWithSize(JSC::JSGlobalObject* globalObject, JSC::JSCell* owner, JSC::JSValue value, double size) + { + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + // spec step 2-3: ! IsNonNegativeNumber(size) and size !== +Infinity. + if (!(size >= 0) || std::isinf(size)) { + JSC::throwRangeError(globalObject, scope, "The queuing strategy's chunk size must be a non-negative, finite number"_s); + return; + } + WTF::Locker locker { owner->cellLock() }; + m_queue.append(Entry { JSC::WriteBarrier(vm, owner, value), size }); + m_totalSize += size; + } + + // spec: DequeueValue(container) — clamps [[queueTotalSize]] at 0. (ValueWithSize only.) + JSC::JSValue dequeueValue(const WTF::AbstractLocker&) + { + ASSERT(!m_queue.isEmpty()); + Entry entry = m_queue.takeFirst(); + JSC::JSValue value = entry.value.get(); + m_totalSize -= entry.size; + // spec: "This can occur due to rounding errors." + if (m_totalSize < 0) + m_totalSize = 0; + return value; + } + + // spec: PeekQueueValue(container). (ValueWithSize only.) + JSC::JSValue peekQueueValue() const + { + ASSERT(!m_queue.isEmpty()); + return m_queue.first().value.get(); + } + + // spec: ResetQueue(container) — clears the list and sets [[queueTotalSize]] to 0. + void resetQueue(const WTF::AbstractLocker&) + { + m_queue.clear(); + m_totalSize = 0; + } + + // Byte-queue manual mutators (the byte controller updates its two slots by hand). + // Callers adjust [[queueTotalSize]] separately via adjustTotalSize(). + void append(const WTF::AbstractLocker&, Entry&& entry) + { + m_queue.append(WTF::move(entry)); + } + void prepend(const WTF::AbstractLocker&, Entry&& entry) + { + m_queue.prepend(WTF::move(entry)); + } + // The returned reference is INVALID after any call that can run user JS or mutate the + // queue — re-fetch. + Entry& first() { return m_queue.first(); } + const Entry& first() const { return m_queue.first(); } + void removeFirst(const WTF::AbstractLocker&) + { + m_queue.removeFirst(); + } + + bool isEmpty() const { return m_queue.isEmpty(); } + size_t size() const { return m_queue.size(); } + double totalSize() const { return m_totalSize; } // [[queueTotalSize]] + void setTotalSize(double totalSize) { m_totalSize = totalSize; } + void adjustTotalSize(double delta) { m_totalSize += delta; } + + // GC: called from the owner's visitChildrenImpl, inside the SAME single + // `Locker { owner->cellLock() }` scope that covers the owner's sibling barrier deques. + template + void visit(const WTF::AbstractLocker&, Visitor& visitor) + { + for (auto& entry : m_queue) + visitEntry(visitor, entry); + } + +private: + template + static void visitEntry(Visitor& visitor, ValueWithSize& entry) { visitor.append(entry.value); } + template + static void visitEntry(Visitor&, ByteQueueEntry&) {} // RefPtr impl: nothing for the GC + + // Backing container. 4 inline entries covers the common shallow queue. + WTF::Deque m_queue; + double m_totalSize { 0 }; // [[queueTotalSize]] — a double, never an integer +}; + +} // namespace WebStreams +} // namespace Bun diff --git a/src/jsc/bindings/webcore/streams/StreamsForward.h b/src/jsc/bindings/webcore/streams/StreamsForward.h new file mode 100644 index 000000000000..fec80cec736b --- /dev/null +++ b/src/jsc/bindings/webcore/streams/StreamsForward.h @@ -0,0 +1,226 @@ +// StreamsForward.h — forward declarations + the shared scoped enums for the Web Streams C++ +// implementation. Class headers include THIS file instead of each other so there are no +// include cycles: it contains NO class definitions and NO function declarations (those live +// in WebStreamsInternals.h) and is safe to include from anywhere. +// +// Namespaces: +// - JS cell classes live in `namespace WebCore` (required by the reused registration +// plumbing: WEBCORE_GENERATED_CONSTRUCTOR_GETTER expands to `WebCore::JS`). +// - enums / structs / free abstract ops live in `namespace Bun::WebStreams`. +#pragma once + +#include + +namespace JSC { +class JSGlobalObject; +class VM; +class CallFrame; +class JSCell; +class JSObject; +class JSValue; +class JSPromise; +class JSArrayBuffer; +class JSArrayBufferView; +class JSFunction; +class Structure; +class InternalFieldTuple; +} + +namespace Zig { +class GlobalObject; +} + +namespace WebCore { + +class AbortSignal; +// NOTE: JSDOMGlobalObject is deliberately NOT forward-declared here. In Bun it is not a +// class but a type alias (`using JSDOMGlobalObject = Zig::GlobalObject;` in +// ZigGlobalObject.h), so `class JSDOMGlobalObject;` is a typedef-redefinition error. +// Any header that names it must `#include "JSDOMGlobalObject.h"` (they all already do). + +// The public (globalThis-exposed) classes. +class JSReadableStream; +class JSReadableStreamDefaultReader; +class JSReadableStreamBYOBReader; +class JSReadableStreamDefaultController; +class JSReadableByteStreamController; +class JSReadableStreamBYOBRequest; +class JSWritableStream; +class JSWritableStreamDefaultWriter; +class JSWritableStreamDefaultController; +class JSTransformStream; +class JSTransformStreamDefaultController; +class JSByteLengthQueuingStrategy; +class JSCountQueuingStrategy; +class JSReadableStreamAsyncIterator; + +// The shared, NON-polymorphic reader base (the ReadableStreamGenericReader mixin). +class JSReadableStreamReaderBase; + +// Internal (non-exposed) cells. +class JSReadRequest; +class JSReadIntoRequest; +class JSPullIntoDescriptor; +class JSStreamPipeToOperation; +class JSStreamTeeState; +class JSCrossRealmTransformState; +class JSStreamFromIterableContext; +class JSStreamsRuntime; + +// The Bun-native layer cells & classes. +class JSDirectStreamController; +class JSBunStandaloneTextSink; // the standalone Text sink (BunStandaloneTextSink.h) +class JSOneShotDirectSink; // consumeDirectStreamToArrayBuffer's throwaway controller +class JSReadableStreamIntoArrayOperation; // the array pump's reader/chunks/result state +class JSNativeStreamSourceAdapter; +class JSDirectSinkCloseState; +class JSAsyncIteratorSourceOperation; +class JSReadStreamIntoSinkOperation; +class JSResumableSinkPumpOperation; +class JSTextEncoderStream; +class JSTextDecoderStream; + +} // namespace WebCore + +namespace Bun { +namespace WebStreams { + +// [[state]] machines + +// ReadableStream.[[state]]: "readable" | "closed" | "errored" +enum class ReadableStreamState : uint8_t { + Readable, + Closed, + Errored, +}; + +// WritableStream.[[state]]: "writable" | "erroring" | "errored" | "closed" +enum class WritableStreamState : uint8_t { + Writable, + Erroring, + Errored, + Closed, +}; + +// Algorithm kind tags. SourceKind::Direct deliberately DOES NOT EXIST: a Bun `type:"direct"` +// stream is a JSDirectStreamController (ControllerKind::Direct), never a spec controller. + +// Which arm runs a readable controller's pull/cancel/start algorithms. No closures are stored. +enum class SourceKind : uint8_t { + JavaScript, // new ReadableStream({...}) — user underlyingSource (underlyingObject + methods) + Nothing, // new ReadableStream() with no source, or an already-drained native stream + Transform, // the readable half of a TransformStream (context = the JSTransformStream) + TeeBranch, // a ReadableStreamDefaultTee branch (context = the JSStreamTeeState) + ByteTeeBranch, // a ReadableByteStreamTee branch (context = the JSStreamTeeState) + FromIterable, // ReadableStream.from(asyncIterable) (context = JSStreamFromIterableContext) + CrossRealm, // receiving end of a postMessage transfer (context = JSCrossRealmTransformState) + Native, // Bun: lazily-materialized native source on a DEFAULT controller + // (context = JSNativeStreamSourceAdapter) +}; + +// Which arm runs a writable controller's write/close/abort algorithms. +// The Bun JSSink layer never uses a WritableStream, so it adds no arm. +enum class SinkKind : uint8_t { + JavaScript, // user underlyingSink + Nothing, // new WritableStream() with no sink + Transform, // the writable half of a TransformStream (context = the JSTransformStream) + CrossRealm, // SetUpCrossRealmTransformWritable (context = JSCrossRealmTransformState) +}; + +// Which arm runs a transform controller's transform/flush/cancel algorithms. +enum class TransformerKind : uint8_t { + JavaScript, // user transformer + Identity, // new TransformStream() with no `transform` member: enqueue the chunk unchanged + TextEncoder, // TextEncoderStream (context = the JSTextEncoderStream cell) + TextDecoder, // TextDecoderStream (context = the JSTextDecoderStream cell) +}; + +// JSReadableStream Bun-mode members + +// Replaces the `$start` thunk. Tells materializeIfNeeded() what to do. +enum class BunStreamMode : uint8_t { + Default, // an ordinary spec stream (controller may still be None) + DirectPending, // type:"direct", not yet consumed + NativePending, // $lazy native stream, not yet consumed +}; + +// The tag for JSReadableStream::m_controller — the subsystem's ONE erased back-pointer. +// Every switch over this enum is TOTAL. +enum class ControllerKind : uint8_t { + None, // no controller installed (unmaterialized / drained) + Default, // JSReadableStreamDefaultController + Byte, // JSReadableByteStreamController + Direct, // JSDirectStreamController (Bun `type:"direct"`, JS-consumption path) + NativeSink, // a generated JSReadable*Controller JSSink cell (Bun native-sink path) +}; + +// Readers & read requests + +// Pull-into descriptor / release bookkeeping "reader type": "default" / "byob" / "none". +enum class ReaderType : uint8_t { + Default, + Byob, + None, +}; + +// JSReadRequest::m_kind — ONE concrete cell, a kind tag, no C++ virtuals. The Bun layer adds +// NO kind: its pumps react to read()'s promise, and readMany() uses the Promise kind. +enum class ReadRequestKind : uint8_t { + Promise, // public reader.read(): context = the JSPromise it resolves + PipeTo, // context = the JSStreamPipeToOperation + DefaultTee, // context = the JSStreamTeeState + ByteTee, // context = the JSStreamTeeState (byte tee's default-reader read request) + AsyncIterator, // context = InternalFieldTuple{asyncIterator, the next() result promise} +}; + +// JSReadIntoRequest::m_kind (the BYOB parallel of ReadRequestKind). +enum class ReadIntoRequestKind : uint8_t { + Promise, // public byobReader.read(view): context = the JSPromise + ByteTee, // the byte tee's BYOB read-into request: context = the JSStreamTeeState +}; + +// Bun `type:"direct"` + +// The 3 direct sink flavors carried by ONE JSDirectStreamController. +enum class DirectSinkKind : uint8_t { + ArrayBuffer, // a real Bun.ArrayBufferSink + Text, // the rope + pieces accumulator + Array, // chunks pushed into a JSArray +}; + +// WebIDL enums & small closed sets + +// WebIDL `enum ReadableStreamType { "bytes" }`; an unknown string throws TypeError during +// dictionary conversion. +enum class ReadableStreamType : uint8_t { Bytes }; + +// WebIDL `enum ReadableStreamReaderMode { "byob" }` (getReader(options).mode) +enum class ReadableStreamReaderMode : uint8_t { Byob }; + +// Cross-realm transform protocol message `type`: "chunk" | "pull" | "error" | "close". +enum class CrossRealmMessageType : uint8_t { Chunk, + Pull, + Error, + Close }; + +} // namespace WebStreams +} // namespace Bun + +// The class headers (namespace WebCore) use the enum names unqualified. Import EXACTLY the +// streams enums into WebCore — never `using namespace Bun::WebStreams` in a header. +namespace WebCore { +using Bun::WebStreams::BunStreamMode; +using Bun::WebStreams::ControllerKind; +using Bun::WebStreams::CrossRealmMessageType; +using Bun::WebStreams::DirectSinkKind; +using Bun::WebStreams::ReadableStreamReaderMode; +using Bun::WebStreams::ReadableStreamState; +using Bun::WebStreams::ReadableStreamType; +using Bun::WebStreams::ReaderType; +using Bun::WebStreams::ReadIntoRequestKind; +using Bun::WebStreams::ReadRequestKind; +using Bun::WebStreams::SinkKind; +using Bun::WebStreams::SourceKind; +using Bun::WebStreams::TransformerKind; +using Bun::WebStreams::WritableStreamState; +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/TransformStreamOperations.cpp b/src/jsc/bindings/webcore/streams/TransformStreamOperations.cpp new file mode 100644 index 000000000000..7cc05807083c --- /dev/null +++ b/src/jsc/bindings/webcore/streams/TransformStreamOperations.cpp @@ -0,0 +1,435 @@ +#include "config.h" +#include "WebStreamsInternals.h" + +#include "JSDOMBinding.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStream.h" +#include "JSReadableStreamDefaultController.h" +#include "JSStreamsRuntime.h" +#include "JSTextDecoderStream.h" +#include "JSTextEncoderStream.h" +#include "JSTransformStream.h" +#include "JSTransformStreamDefaultController.h" +#include "JSWritableStream.h" +#include "JSWritableStreamDefaultController.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSStreamsRuntime; + +// The transform's readable half always carries a default controller. +static JSReadableStreamDefaultController* transformReadableController(JSTransformStream* stream) +{ + auto* readable = stream->m_readable.get(); + ASSERT(readable && readable->m_controllerKind == ControllerKind::Default); + return uncheckedDowncast(readable->m_controller.get()); +} + +// WebIDL callback invoke returning Promise: an abrupt completion becomes a +// rejected promise (a sanctioned completion-record catch). Returns nullptr on VM termination. +static JSPromise* invokePromiseReturningMethod(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* method, JSValue thisValue, const MarkedArgumentBuffer& args) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue result; + JSValue thrown; + { + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto callData = getCallData(method); + ASSERT(callData.type != CallData::Type::None); + result = call(globalObject, method, callData, thisValue, args); + if (catchScope.exception()) [[unlikely]] + thrown = takeAbruptCompletion(globalObject, catchScope); + } + if (!thrown.isEmpty()) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, thrown)); + if (result.isEmpty()) + return nullptr; + RELEASE_AND_RETURN(scope, promiseResolvedWith(globalObject, result)); +} + +// [[flushAlgorithm]] dispatch (needed only by the default sink close algorithm below). +static JSPromise* performFlushAlgorithm(JSC::VM& vm, JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + switch (controller->m_transformerKind) { + case TransformerKind::JavaScript: + if (auto* method = controller->m_flushMethod.get()) { + MarkedArgumentBuffer args; + args.append(controller); + ASSERT(!args.hasOverflowed()); + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, method, controller->m_transformer.get(), args)); + } + break; + case TransformerKind::Identity: + break; + case TransformerKind::TextEncoder: + RELEASE_AND_RETURN(scope, textEncoderStreamFlush(globalObject, uncheckedDowncast(controller->m_algorithmContext.get()), controller)); + case TransformerKind::TextDecoder: + RELEASE_AND_RETURN(scope, textDecoderStreamFlush(globalObject, uncheckedDowncast(controller->m_algorithmContext.get()), controller)); + } + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +// [[cancelAlgorithm]] dispatch. The TextEncoder/TextDecoder kinds have no cancel algorithm. +static JSPromise* performCancelAlgorithm(JSC::VM& vm, JSGlobalObject* globalObject, JSTransformStreamDefaultController* controller, JSValue reason) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (controller->m_transformerKind == TransformerKind::JavaScript) { + if (auto* method = controller->m_cancelMethod.get()) { + MarkedArgumentBuffer args; + args.append(reason); + ASSERT(!args.hasOverflowed()); + RELEASE_AND_RETURN(scope, invokePromiseReturningMethod(vm, globalObject, method, controller->m_transformer.get(), args)); + } + } + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); +} + +JSTransformStream* createTransformStream(JSGlobalObject* globalObject, TransformerKind kind, JSCell* algorithmContext, double writableHighWaterMark, JSObject* writableSizeAlgorithm, double readableHighWaterMark, JSObject* readableSizeAlgorithm) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(writableHighWaterMark >= 0); + ASSERT(readableHighWaterMark >= 0); + auto* domGlobalObject = defaultGlobalObject(globalObject); + + auto* stream = JSTransformStream::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + auto* startPromise = JSPromise::create(vm, globalObject->promiseStructure()); + initializeTransformStream(globalObject, stream, startPromise, writableHighWaterMark, writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm); + RETURN_IF_EXCEPTION(scope, nullptr); + + auto* controller = JSTransformStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_transformerKind = kind; + if (algorithmContext) + controller->m_algorithmContext.set(vm, controller, algorithmContext); + setUpTransformStreamDefaultController(vm, stream, controller); + + // The internal kinds' start algorithm is trivial. + resolvePromise(globalObject, startPromise, jsUndefined()); + RETURN_IF_EXCEPTION(scope, nullptr); + return stream; +} + +void initializeTransformStream(JSGlobalObject* globalObject, JSTransformStream* stream, JSPromise* startPromise, double writableHighWaterMark, JSObject* writableSizeAlgorithm, double readableHighWaterMark, JSObject* readableSizeAlgorithm) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* writable = createWritableStream(globalObject, SinkKind::Transform, stream, startPromise, writableHighWaterMark, writableSizeAlgorithm); + RETURN_IF_EXCEPTION(scope, void()); + stream->m_writable.set(vm, stream, writable); + + auto* readable = createReadableStream(globalObject, SourceKind::Transform, stream, startPromise, readableHighWaterMark, readableSizeAlgorithm); + RETURN_IF_EXCEPTION(scope, void()); + stream->m_readable.set(vm, stream, readable); + + stream->m_backpressure = false; + stream->m_backpressureChangePromise.clear(); + transformStreamSetBackpressure(globalObject, stream, true); + // Setting backpressure on a fresh stream resolves no promise and cannot throw. + scope.assertNoException(); + stream->m_controller.clear(); +} + +void transformStreamError(JSGlobalObject* globalObject, JSTransformStream* stream, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + readableStreamDefaultControllerError(globalObject, transformReadableController(stream), error); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, transformStreamErrorWritableAndUnblockWrite(globalObject, stream, error)); +} + +void transformStreamErrorWritableAndUnblockWrite(JSGlobalObject* globalObject, JSTransformStream* stream, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + transformStreamDefaultControllerClearAlgorithms(stream->m_controller.get()); + writableStreamDefaultControllerErrorIfNeeded(globalObject, stream->m_writable->m_controller.get(), error); + RETURN_IF_EXCEPTION(scope, void()); + RELEASE_AND_RETURN(scope, transformStreamUnblockWrite(globalObject, stream)); +} + +void transformStreamSetBackpressure(JSGlobalObject* globalObject, JSTransformStream* stream, bool backpressure) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_backpressure != backpressure); + if (auto* previous = stream->m_backpressureChangePromise.get()) { + resolvePromise(globalObject, previous, jsUndefined()); + // Resolving with `undefined` performs no thenable lookup and cannot throw. + scope.assertNoException(); + } + stream->m_backpressureChangePromise.set(vm, stream, JSPromise::create(vm, globalObject->promiseStructure())); + stream->m_backpressure = backpressure; +} + +void transformStreamUnblockWrite(JSGlobalObject* globalObject, JSTransformStream* stream) +{ + if (stream->m_backpressure) + transformStreamSetBackpressure(globalObject, stream, false); +} + +void setUpTransformStreamDefaultController(VM& vm, JSTransformStream* stream, JSTransformStreamDefaultController* controller) +{ + ASSERT(!stream->m_controller); + controller->m_stream.set(vm, controller, stream); + stream->m_controller.set(vm, stream, controller); +} + +void setUpTransformStreamDefaultControllerFromTransformer(JSGlobalObject* globalObject, JSTransformStream* stream, JSValue transformer, const TransformerDict& transformerDict) +{ + auto& vm = getVM(globalObject); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* controller = JSTransformStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + + if (transformer.isObject()) { + controller->m_transformerKind = TransformerKind::JavaScript; + controller->m_transformer.set(vm, controller, transformer); + if (!transformerDict.transform.isEmpty()) + controller->m_transformMethod.set(vm, controller, asObject(transformerDict.transform)); + if (!transformerDict.flush.isEmpty()) + controller->m_flushMethod.set(vm, controller, asObject(transformerDict.flush)); + if (!transformerDict.cancel.isEmpty()) + controller->m_cancelMethod.set(vm, controller, asObject(transformerDict.cancel)); + } else + controller->m_transformerKind = TransformerKind::Identity; + + setUpTransformStreamDefaultController(vm, stream, controller); +} + +JSPromise* transformStreamDefaultSinkWriteAlgorithm(JSGlobalObject* globalObject, JSTransformStream* stream, JSValue chunk) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_writable->m_state == WritableStreamState::Writable); + auto* controller = stream->m_controller.get(); + if (stream->m_backpressure) { + auto* backpressureChangePromise = stream->m_backpressureChangePromise.get(); + ASSERT(backpressureChangePromise); + auto* result = JSPromise::create(vm, globalObject->promiseStructure()); + auto* context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), stream, chunk); + auto* runtime = JSStreamsRuntime::from(globalObject); + backpressureChangePromise->performPromiseThenWithContext(vm, globalObject, runtime->onTSSinkWriteBackpressureChangeFulfilled(), jsUndefined(), result, context); + return result; + } + RELEASE_AND_RETURN(scope, transformStreamDefaultControllerPerformTransform(globalObject, controller, chunk)); +} + +JSPromise* transformStreamDefaultSinkAbortAlgorithm(JSGlobalObject* globalObject, JSTransformStream* stream, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = stream->m_controller.get(); + if (auto* finishPromise = controller->m_finishPromise.get()) + return finishPromise; + auto* finishPromise = JSPromise::create(vm, globalObject->promiseStructure()); + controller->m_finishPromise.set(vm, controller, finishPromise); + + auto* cancelPromise = performCancelAlgorithm(vm, globalObject, controller, reason); + RETURN_IF_EXCEPTION(scope, nullptr); + transformStreamDefaultControllerClearAlgorithms(controller); + + auto* context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), stream, reason); + auto* runtime = JSStreamsRuntime::from(globalObject); + cancelPromise->performPromiseThenWithContext(vm, globalObject, runtime->onTSSinkAbortCancelFulfilled(), runtime->onTSSinkAbortCancelRejected(), jsUndefined(), context); + return controller->m_finishPromise.get(); +} + +JSPromise* transformStreamDefaultSinkCloseAlgorithm(JSGlobalObject* globalObject, JSTransformStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = stream->m_controller.get(); + if (auto* finishPromise = controller->m_finishPromise.get()) + return finishPromise; + auto* finishPromise = JSPromise::create(vm, globalObject->promiseStructure()); + controller->m_finishPromise.set(vm, controller, finishPromise); + + auto* flushPromise = performFlushAlgorithm(vm, globalObject, controller); + RETURN_IF_EXCEPTION(scope, nullptr); + transformStreamDefaultControllerClearAlgorithms(controller); + + auto* runtime = JSStreamsRuntime::from(globalObject); + flushPromise->performPromiseThenWithContext(vm, globalObject, runtime->onTSSinkCloseFlushFulfilled(), runtime->onTSSinkCloseFlushRejected(), jsUndefined(), stream); + return controller->m_finishPromise.get(); +} + +JSPromise* transformStreamDefaultSourceCancelAlgorithm(JSGlobalObject* globalObject, JSTransformStream* stream, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* controller = stream->m_controller.get(); + if (auto* finishPromise = controller->m_finishPromise.get()) + return finishPromise; + auto* finishPromise = JSPromise::create(vm, globalObject->promiseStructure()); + controller->m_finishPromise.set(vm, controller, finishPromise); + + auto* cancelPromise = performCancelAlgorithm(vm, globalObject, controller, reason); + RETURN_IF_EXCEPTION(scope, nullptr); + transformStreamDefaultControllerClearAlgorithms(controller); + + auto* context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), stream, reason); + auto* runtime = JSStreamsRuntime::from(globalObject); + cancelPromise->performPromiseThenWithContext(vm, globalObject, runtime->onTSSourceCancelFulfilled(), runtime->onTSSourceCancelRejected(), jsUndefined(), context); + return controller->m_finishPromise.get(); +} + +JSPromise* transformStreamDefaultSourcePullAlgorithm(JSGlobalObject* globalObject, JSTransformStream* stream) +{ + ASSERT(stream->m_backpressure); + ASSERT(stream->m_backpressureChangePromise); + transformStreamSetBackpressure(globalObject, stream, false); + return stream->m_backpressureChangePromise.get(); +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; +using namespace Bun::WebStreams; + +// [reaction-convention]: handler(resolutionValue, contextCell). + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSSinkWriteBackpressureChangeFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->argument(1)); + auto* stream = uncheckedDowncast(context->getInternalField(0)); + JSValue chunk = context->getInternalField(1); + + auto* writable = stream->m_writable.get(); + if (writable->m_state == WritableStreamState::Erroring) { + throwException(globalObject, scope, writable->m_storedError.get()); + return {}; + } + ASSERT(writable->m_state == WritableStreamState::Writable); + auto* result = transformStreamDefaultControllerPerformTransform(globalObject, stream->m_controller.get(), chunk); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(result); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSSinkAbortCancelFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->argument(1)); + auto* stream = uncheckedDowncast(context->getInternalField(0)); + JSValue reason = context->getInternalField(1); + auto* finishPromise = stream->m_controller->m_finishPromise.get(); + + auto* readable = stream->m_readable.get(); + if (readable->m_state == ReadableStreamState::Errored) { + rejectPromise(globalObject, finishPromise, readable->m_storedError.get()); + return JSValue::encode(jsUndefined()); + } + readableStreamDefaultControllerError(globalObject, transformReadableController(stream), reason); + RETURN_IF_EXCEPTION(scope, {}); + resolvePromise(globalObject, finishPromise, jsUndefined()); + // Resolving with `undefined` performs no thenable lookup and cannot throw. + scope.assertNoException(); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSSinkAbortCancelRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue rejection = callFrame->argument(0); + auto* stream = uncheckedDowncast(uncheckedDowncast(callFrame->argument(1))->getInternalField(0)); + auto* finishPromise = stream->m_controller->m_finishPromise.get(); + + readableStreamDefaultControllerError(globalObject, transformReadableController(stream), rejection); + RETURN_IF_EXCEPTION(scope, {}); + rejectPromise(globalObject, finishPromise, rejection); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSSinkCloseFlushFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = uncheckedDowncast(callFrame->argument(1)); + auto* finishPromise = stream->m_controller->m_finishPromise.get(); + + auto* readable = stream->m_readable.get(); + if (readable->m_state == ReadableStreamState::Errored) { + rejectPromise(globalObject, finishPromise, readable->m_storedError.get()); + return JSValue::encode(jsUndefined()); + } + readableStreamDefaultControllerClose(globalObject, transformReadableController(stream)); + RETURN_IF_EXCEPTION(scope, {}); + resolvePromise(globalObject, finishPromise, jsUndefined()); + // Resolving with `undefined` performs no thenable lookup and cannot throw. + scope.assertNoException(); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSSinkCloseFlushRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue rejection = callFrame->argument(0); + auto* stream = uncheckedDowncast(callFrame->argument(1)); + auto* finishPromise = stream->m_controller->m_finishPromise.get(); + + readableStreamDefaultControllerError(globalObject, transformReadableController(stream), rejection); + RETURN_IF_EXCEPTION(scope, {}); + rejectPromise(globalObject, finishPromise, rejection); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSSourceCancelFulfilled, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->argument(1)); + auto* stream = uncheckedDowncast(context->getInternalField(0)); + JSValue reason = context->getInternalField(1); + auto* finishPromise = stream->m_controller->m_finishPromise.get(); + + auto* writable = stream->m_writable.get(); + if (writable->m_state == WritableStreamState::Errored) { + rejectPromise(globalObject, finishPromise, writable->m_storedError.get()); + return JSValue::encode(jsUndefined()); + } + writableStreamDefaultControllerErrorIfNeeded(globalObject, writable->m_controller.get(), reason); + RETURN_IF_EXCEPTION(scope, {}); + transformStreamUnblockWrite(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + resolvePromise(globalObject, finishPromise, jsUndefined()); + // Resolving with `undefined` performs no thenable lookup and cannot throw. + scope.assertNoException(); + return JSValue::encode(jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onTSSourceCancelRejected, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue rejection = callFrame->argument(0); + auto* stream = uncheckedDowncast(uncheckedDowncast(callFrame->argument(1))->getInternalField(0)); + auto* finishPromise = stream->m_controller->m_finishPromise.get(); + + writableStreamDefaultControllerErrorIfNeeded(globalObject, stream->m_writable->m_controller.get(), rejection); + RETURN_IF_EXCEPTION(scope, {}); + transformStreamUnblockWrite(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + rejectPromise(globalObject, finishPromise, rejection); + return JSValue::encode(jsUndefined()); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/WebStreamsExports.cpp b/src/jsc/bindings/webcore/streams/WebStreamsExports.cpp new file mode 100644 index 000000000000..b9fe8315aeac --- /dev/null +++ b/src/jsc/bindings/webcore/streams/WebStreamsExports.cpp @@ -0,0 +1,297 @@ +#include "config.h" +#include "WebStreamsInternals.h" + +#include "ErrorCode.h" +#include "ExceptionCode.h" +#include "JSDOMExceptionHandling.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMWrapperCache.h" +#include "JSReadableStream.h" +#include "WebCoreJSBuiltins.h" +#include "ZigGeneratedClasses.h" +#include "ZigGlobalObject.h" + +#include +#include +#include +#include +#include + +// The extern "C" / Rust FFI surface. Every symbol name, signature, and ReadableStreamTag +// discriminant (Invalid=-1, JavaScript=0, Blob=1, File=2, Direct=3 [never emitted], Bytes=4) +// is frozen by ReadableStream.rs's assert_ffi_discr!. + +namespace Bun { +namespace WebStreams { + +using namespace JSC; +using WebCore::JSReadableStream; + +// Shared brand check of every consumer entry point; throws ERR_INVALID_ARG_TYPE. +static JSReadableStream* toReadableStream(Zig::GlobalObject* globalObject, ThrowScope& scope, EncodedJSValue encodedStream) +{ + JSValue streamValue = JSValue::decode(encodedStream); + auto* stream = dynamicDowncast(streamValue); + if (!stream) [[unlikely]] + Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "stream"_s, "ReadableStream"_s, streamValue); + return stream; +} + +} // namespace WebStreams +} // namespace Bun + +using namespace JSC; +using namespace WebCore; +using namespace Bun::WebStreams; + +extern "C" int32_t ReadableStreamTag__tagged(Zig::GlobalObject* globalObject, JSC::EncodedJSValue* possibleReadableStream, void** ptr) +{ + *ptr = nullptr; + JSValue value = JSValue::decode(*possibleReadableStream); + if (value.isEmpty() || !value.isCell()) + return -1; + JSObject* object = value.getObject(); + if (!object) + return -1; + + auto& vm = JSC::getVM(globalObject); + + if (auto* stream = dynamicDowncast(object)) { + // The RAW handle slot, not nativePtrForJS(): a transferred stream still tags. + JSValue handle = stream->m_nativePtr.get(); + if (handle.isEmpty() || !handle.isCell()) + return 0; + JSCell* handleCell = handle.asCell(); + if (auto* blobSource = dynamicDowncast(handleCell)) { + *ptr = blobSource->wrapped(); + return 1; + } + if (auto* fileSource = dynamicDowncast(handleCell)) { + *ptr = fileSource->wrapped(); + return 2; + } + if (auto* bytesSource = dynamicDowncast(handleCell)) { + *ptr = bytesSource->wrapped(); + return 4; + } + return 0; + } + + auto scope = DECLARE_THROW_SCOPE(vm); + if (!isNonHostAsyncGeneratorFunction(object)) { + JSValue iteratorMethod = object->getIfPropertyExists(globalObject, vm.propertyNames->asyncIteratorSymbol); + RETURN_IF_EXCEPTION(scope, -1); + if (!iteratorMethod || !iteratorMethod.isCallable()) + return -1; + } + + auto* stream = readableStreamFromAsyncIterator(globalObject, object); + RETURN_IF_EXCEPTION(scope, -1); + *possibleReadableStream = JSValue::encode(stream); + return 0; +} + +extern "C" bool ReadableStream__tee(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject, JSC::EncodedJSValue* possibleReadableStream1, JSC::EncodedJSValue* possibleReadableStream2) +{ + auto* stream = dynamicDowncast(JSValue::decode(possibleReadableStream)); + if (!stream) [[unlikely]] + return false; + + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto branches = readableStreamTee(globalObject, stream, /* cloneForBranch2 */ true); + RETURN_IF_EXCEPTION(scope, false); + + *possibleReadableStream1 = JSValue::encode(branches.first); + *possibleReadableStream2 = JSValue::encode(branches.second); + return true; +} + +extern "C" bool ReadableStream__is(JSC::EncodedJSValue value) +{ + return !!dynamicDowncast(JSValue::decode(value)); +} + +extern "C" bool ReadableStream__isDisturbed(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*) +{ + auto* stream = dynamicDowncast(JSValue::decode(possibleReadableStream)); + return stream && stream->m_disturbed; +} + +extern "C" bool ReadableStream__isLocked(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*) +{ + auto* stream = dynamicDowncast(JSValue::decode(possibleReadableStream)); + return stream && isReadableStreamLocked(stream); +} + +extern "C" void ReadableStream__cancel(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject) +{ + auto* stream = dynamicDowncast(JSValue::decode(possibleReadableStream)); + if (!stream) [[unlikely]] + return; + // A direct/native consumer locks the stream without a reader; its teardown is owned by + // the controller close/detach path, never by readableStreamCancel. + if (!stream->m_reader) + return; + + auto& vm = JSC::getVM(globalObject); + // The native caller cannot observe VM exception state, so nothing may stay pending + // here (a termination does, by design). + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + JSValue reason = WebCore::createDOMException(globalObject, WebCore::ExceptionCode::AbortError); + if (catchScope.exception()) [[unlikely]] { + catchScope.clearExceptionExceptTermination(); + return; + } + auto* result = readableStreamCancel(globalObject, stream, reason); + if (catchScope.exception()) [[unlikely]] { + catchScope.clearExceptionExceptTermination(); + return; + } + markPromiseAsHandled(vm, result); +} + +extern "C" void ReadableStream__cancelWithReason(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject, JSC::EncodedJSValue reason) +{ + auto* stream = dynamicDowncast(JSValue::decode(possibleReadableStream)); + if (!stream) [[unlikely]] + return; + + auto& vm = JSC::getVM(globalObject); + // See ReadableStream__cancel: never return to the native caller with a pending exception. + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto* result = readableStreamCancel(globalObject, stream, JSValue::decode(reason)); + if (catchScope.exception()) [[unlikely]] { + catchScope.clearExceptionExceptTermination(); + return; + } + markPromiseAsHandled(vm, result); +} + +extern "C" void ReadableStream__detach(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject* globalObject) +{ + auto* stream = dynamicDowncast(JSValue::decode(possibleReadableStream)); + if (!stream) [[unlikely]] + return; + stream->m_nativePtr.set(globalObject->vm(), stream, jsNumber(-1)); + stream->m_nativeType = 0; + stream->m_disturbed = true; +} + +extern "C" JSC::EncodedJSValue ReadableStream__empty(Zig::GlobalObject* globalObject) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = createReadableStream(globalObject, SourceKind::Nothing, nullptr, jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + readableStreamClose(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(stream); +} + +extern "C" JSC::EncodedJSValue ReadableStream__used(Zig::GlobalObject* globalObject) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = createReadableStream(globalObject, SourceKind::Nothing, nullptr, jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + acquireReadableStreamDefaultReader(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(stream); +} + +extern "C" JSC::EncodedJSValue ReadableStream__errored(Zig::GlobalObject* globalObject, JSC::EncodedJSValue reason) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = createReadableStream(globalObject, SourceKind::Nothing, nullptr, jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + readableStreamError(globalObject, stream, JSValue::decode(reason)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(stream); +} + +extern "C" JSC::EncodedJSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalObject* globalObject, JSC::EncodedJSValue nativePtr) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = JSReadableStream::create(vm, WebCore::getDOMStructure(vm, *globalObject)); + RETURN_IF_EXCEPTION(scope, {}); + initializeReadableStream(stream); + // Nothing native runs until a consumer materializes the stream. + stream->m_bunMode = BunStreamMode::NativePending; + stream->m_nativePtr.set(vm, stream, JSValue::decode(nativePtr)); + return JSValue::encode(stream); +} + +extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBuffer(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = toReadableStream(globalObject, scope, streamValue); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToArrayBuffer(globalObject, stream))); +} + +extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBytes(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = toReadableStream(globalObject, scope, streamValue); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToBytes(globalObject, stream))); +} + +extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToText(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = toReadableStream(globalObject, scope, streamValue); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToText(globalObject, stream))); +} + +extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToJSON(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = toReadableStream(globalObject, scope, streamValue); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToJSON(globalObject, stream))); +} + +extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = toReadableStream(globalObject, scope, streamValue); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToBlob(globalObject, stream))); +} + +extern "C" JSC::EncodedJSValue ZigGlobalObject__readableStreamToFormData(Zig::GlobalObject* globalObject, JSC::EncodedJSValue streamValue, JSC::EncodedJSValue contentType) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* stream = toReadableStream(globalObject, scope, streamValue); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, JSValue::encode(readableStreamToFormData(globalObject, stream, JSValue::decode(contentType)))); +} + +extern "C" JSC::EncodedJSValue Bun__assignStreamIntoResumableSink(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue streamValue, JSC::EncodedJSValue sinkValue) +{ + auto& vm = JSC::getVM(globalObject); + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto* stream = dynamicDowncast(JSValue::decode(streamValue)); + JSObject* sink = JSValue::decode(sinkValue).getObject(); + if (!stream || !sink) [[unlikely]] + return JSValue::encode(jsUndefined()); + JSValue result = assignStreamIntoResumableSink(globalObject, stream, sink); + if (auto* exception = catchScope.exception()) [[unlikely]] { + // The native caller cannot observe VM exception state: hand back the Exception + // cell and leave nothing pending (a termination stays pending by design). + catchScope.clearExceptionExceptTermination(); + return JSValue::encode(exception); + } + return JSValue::encode(result); +} diff --git a/src/jsc/bindings/webcore/streams/WebStreamsInternals.h b/src/jsc/bindings/webcore/streams/WebStreamsInternals.h new file mode 100644 index 000000000000..16d27a3f6e11 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/WebStreamsInternals.h @@ -0,0 +1,591 @@ +// WebStreamsInternals.h — THE FROZEN ABI of the Web Streams C++ implementation. +// Every cross-file free function of the subsystem is declared here, EXACTLY ONCE, grouped +// by the .cpp that OWNS its body. NO definitions live here. +// +// Every declaration carries: // userJS: yes|no — +// "userJS: yes" = the op can synchronously run arbitrary user JS (directly, through a +// thenable, or transitively) — callers must re-validate every reentrantly-mutable piece of +// controller/stream state after the call. +// "userJS: no" = it never does (it may still allocate / throw unless noted "pure"). +// +// The reaction / bound-callable handler lists (the OTHER half of the ABI) live in +// JSStreamsRuntime.h. The queue ops (EnqueueValueWithSize / DequeueValue / PeekQueueValue / +// ResetQueue) are StreamQueue<> methods (StreamQueue.h). The controller internal methods +// ([[PullSteps]] / [[CancelSteps]] / [[ReleaseSteps]] / [[AbortSteps]] / [[ErrorSteps]]) are +// members of their controller class. +#pragma once + +#include "root.h" +#include "StreamsForward.h" +#include "BunStreamConsumers.h" + +// These three are used by name below (`JSC::JSUint8Array*` is a typedef and cannot be +// forward-declared; `const JSC::Identifier&`; `WTF::String`) — do not rely on transitive +// includes from root.h for them. MarkedVector.h supplies JSC::MarkedArgumentBuffer. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "helpers.h" +#include + +namespace WebCore { +class MessagePort; +class AbortSignal; +} + +namespace Bun { +namespace WebStreams { + +// Building a string past this limit would abort the process inside WTF; text consumers +// check it and throw a catchable out-of-memory error instead. Mirrors the predicate +// Bun's string constructors use (helpers.h), including the synthetic limit that +// `bun:internal-for-testing` can lower. +inline bool exceedsStringLimit(size_t length) +{ + return length > Bun__stringSyntheticAllocationLimit || length > WTF::StringImpl::MaxLength; +} + +// Reduce noise: every class name below is a WebCore JS cell (StreamsForward.h). +using WebCore::JSCrossRealmTransformState; +using WebCore::JSDirectSinkCloseState; +using WebCore::JSDirectStreamController; +using WebCore::JSNativeStreamSourceAdapter; +using WebCore::JSPullIntoDescriptor; +using WebCore::JSReadableByteStreamController; +using WebCore::JSReadableStream; +using WebCore::JSReadableStreamAsyncIterator; +using WebCore::JSReadableStreamBYOBReader; +using WebCore::JSReadableStreamBYOBRequest; +using WebCore::JSReadableStreamDefaultController; +using WebCore::JSReadableStreamDefaultReader; +using WebCore::JSReadableStreamReaderBase; +using WebCore::JSReadIntoRequest; +using WebCore::JSReadRequest; +using WebCore::JSReadStreamIntoSinkOperation; +using WebCore::JSResumableSinkPumpOperation; +using WebCore::JSStreamFromIterableContext; +using WebCore::JSStreamPipeToOperation; +using WebCore::JSStreamsRuntime; +using WebCore::JSStreamTeeState; +using WebCore::JSTextDecoderStream; +using WebCore::JSTextEncoderStream; +using WebCore::JSTransformStream; +using WebCore::JSTransformStreamDefaultController; +using WebCore::JSWritableStream; +using WebCore::JSWritableStreamDefaultController; +using WebCore::JSWritableStreamDefaultWriter; + +// Converted WebIDL dictionaries. STACK-ONLY carriers: the JSValues are rooted by the +// conservative stack scan for the constructor's duration and are NEVER stored. A member is +// the empty JSValue when the dictionary member is absent. The conversion itself (below, +// WebStreamsMisc.cpp) performs the observable alphabetical-order [[Get]]s and the +// callability TypeErrors. + +struct UnderlyingSourceDict { + JSC::JSValue start; // callable or empty + JSC::JSValue pull; // callable or empty + JSC::JSValue cancel; // callable or empty + std::optional type; // "bytes" or absent + std::optional autoAllocateChunkSize; // [EnforceRange] unsigned long long +}; +struct UnderlyingSinkDict { + JSC::JSValue start; // callable or empty + JSC::JSValue write; // callable or empty + JSC::JSValue close; // callable or empty + JSC::JSValue abort; // callable or empty + bool hasType { false }; // presence alone triggers the constructor's RangeError +}; +struct TransformerDict { + JSC::JSValue start; // callable or empty + JSC::JSValue transform; // callable or empty + JSC::JSValue flush; // callable or empty + JSC::JSValue cancel; // callable or empty + bool hasReadableType { false }; // presence alone triggers the constructor's RangeError + bool hasWritableType { false }; // presence alone triggers the constructor's RangeError +}; +struct QueuingStrategyDict { + std::optional highWaterMark; // absent vs present-NaN are distinct states + JSC::JSValue size; // callable or empty (empty ⇒ the default `() => 1`) +}; + +// WebStreamsMisc.cpp — shared utilities, promise helpers, dictionary conversion, and the ONE +// sanctioned catch helper. + +// spec ExtractHighWaterMark(strategy, defaultHWM). Throws RangeError (NaN / negative). +double extractHighWaterMark(JSC::JSGlobalObject*, const QueuingStrategyDict&, double defaultHWM); // userJS: no — WebStreamsMisc.cpp +// spec ExtractSizeAlgorithm(strategy) → the converted callback object; nullptr = `() => 1`. +JSC::JSObject* extractSizeAlgorithm(const QueuingStrategyDict&); // userJS: no — WebStreamsMisc.cpp +// spec IsNonNegativeNumber(v) — pure type + range test, NO coercion. +bool isNonNegativeNumber(JSC::JSValue); // userJS: no — WebStreamsMisc.cpp +// spec TransferArrayBuffer(O). Throws TypeError on a non-transferable buffer. +// (Runs no JS, but DETACHES `buffer`: callers must re-read any cached view length/vector() +// of the SOURCE buffer afterward.) +RefPtr transferArrayBufferImpl(JSC::JSGlobalObject*, JSC::ArrayBuffer&); // userJS: no — WebStreamsMisc.cpp +bool canTransferArrayBuffer(JSC::ArrayBuffer&); // userJS: no — WebStreamsMisc.cpp +// spec CanTransferArrayBuffer(O) — pure. +// spec CloneAsUint8Array(O) — allocation-throws only. +JSC::JSUint8Array* cloneAsUint8Array(JSC::JSGlobalObject*, JSC::JSArrayBufferView*); // userJS: no — WebStreamsMisc.cpp +// spec StructuredClone(v): use the EXISTING WebCore::structuredCloneForStream +// (src/jsc/bindings/webcore/StructuredClone.h). No streams-local duplicate is declared. +// spec CanCopyDataBlockBytes(toBuffer, toIndex, fromBuffer, fromIndex, count) — pure. +bool canCopyDataBlockBytes(JSC::ArrayBuffer& toBuffer, size_t toIndex, JSC::ArrayBuffer& fromBuffer, size_t fromIndex, size_t count); // userJS: no — WebStreamsMisc.cpp + +// The WebIDL dictionary conversions (alphabetical member order; real [[Get]]s; TypeError on +// a present-but-not-callable member; ReadableStreamType TypeError on an unknown `type`). +UnderlyingSinkDict convertUnderlyingSinkDict(JSC::JSGlobalObject*, JSC::JSValue underlyingSink); // userJS: yes — WebStreamsMisc.cpp +TransformerDict convertTransformerDict(JSC::JSGlobalObject*, JSC::JSValue transformer); // userJS: yes — WebStreamsMisc.cpp +QueuingStrategyDict convertQueuingStrategyDict(JSC::JSGlobalObject*, JSC::JSValue strategy); // userJS: yes — WebStreamsMisc.cpp + +// Promise helpers (thin, named after the spec phrases). +// "a promise resolved with v" — resolving with ANY OBJECT (not only a user thenable) performs +// Get(v, "then"), so a user-installed `Object.prototype.then` getter runs synchronously — +// even for OUR fresh `{value, done}` result objects. Only primitive resolutions +// (undefined / true / ...) are exempt. Do NOT "optimize" a fulfillment site to skip +// re-validation on the grounds that the resolution value is internally constructed. +JSC::JSPromise* promiseFulfilledWith(JSC::JSGlobalObject*, JSC::JSValue); // userJS: no — WebStreamsMisc.cpp +// [bound-convention] wrapper: target(contextCell, ...callArgs). userJS: no — WebStreamsMisc.cpp +JSC::JSBoundFunction* createStreamsBoundHandler(JSC::JSGlobalObject*, JSC::JSFunction* target, JSC::JSCell* context); +// obj.name(...args); returns the EMPTY value when `name` is not callable. userJS: yes — WebStreamsMisc.cpp +JSC::JSValue invokeOptionalMethod(JSC::JSGlobalObject*, JSC::JSObject*, const JSC::Identifier& name, const JSC::MarkedArgumentBuffer&); +// error.code === code, swallowing any lookup exception. userJS: yes — WebStreamsMisc.cpp +bool errorCodeIs(JSC::JSGlobalObject*, JSC::JSValue error, WTF::ASCIILiteral code); +JSC::JSPromise* promiseResolvedWith(JSC::JSGlobalObject*, JSC::JSValue); // userJS: yes — WebStreamsMisc.cpp +// "a promise rejected with r" (rejection never does a `then` lookup) +JSC::JSPromise* promiseRejectedWith(JSC::JSGlobalObject*, JSC::JSValue); // userJS: no — WebStreamsMisc.cpp +// "resolve promise with v" — SAME `Object.prototype.then` hazard as promiseResolvedWith: +// resolving with ANY object (user-controlled or our own) runs user JS. +void resolvePromise(JSC::JSGlobalObject*, JSC::JSPromise*, JSC::JSValue); // userJS: yes — WebStreamsMisc.cpp +// "reject promise with r" +void rejectPromise(JSC::JSGlobalObject*, JSC::JSPromise*, JSC::JSValue); // userJS: no — WebStreamsMisc.cpp +// "Set promise.[[PromiseIsHandled]] to true" +void markPromiseAsHandled(JSC::VM&, JSC::JSPromise*); // userJS: no — WebStreamsMisc.cpp +// {value,done} results: use JSC::createIteratorResultObject +// (; VM-cached structure). + +// THE ONE SANCTIONED CATCH of the subsystem. Returns the thrown value after +// clearExceptionExceptTermination(); returns the EMPTY JSValue if the exception is a VM +// termination (which the caller must propagate, never consume). Never call bare +// clearException() anywhere in the subsystem. +JSC::JSValue takeAbruptCompletion(JSC::JSGlobalObject*, JSC::TopExceptionScope&); // userJS: no — WebStreamsMisc.cpp + +// ReadableStreamOperations.cpp — stream-level RS ops, reader set-up, controller set-up, +// tee, from-iterable. + +// Internal creation. +// `startResult` = the value "the start algorithm returned" (a pre-existing pending promise +// for the transform's inner streams; jsUndefined() for tee/from-iterable/cross-realm). +JSReadableStream* createReadableStream(JSC::JSGlobalObject*, SourceKind, JSC::JSCell* algorithmContext, JSC::JSValue startResult, double highWaterMark = 1, JSC::JSObject* sizeAlgorithm = nullptr); // userJS: yes — ReadableStreamOperations.cpp +JSReadableStream* createReadableByteStream(JSC::JSGlobalObject*, SourceKind, JSC::JSCell* algorithmContext); // userJS: yes — ReadableStreamOperations.cpp +void initializeReadableStream(JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp +bool isReadableStreamLocked(JSReadableStream*); // userJS: no (pure; includes Bun's m_lockedWithoutReader / detached-handle states) — ReadableStreamOperations.cpp + +// Readers. +JSReadableStreamDefaultReader* acquireReadableStreamDefaultReader(JSC::JSGlobalObject*, JSReadableStream*); // userJS: no (throws TypeError if locked) — ReadableStreamOperations.cpp +JSReadableStreamBYOBReader* acquireReadableStreamBYOBReader(JSC::JSGlobalObject*, JSReadableStream*); // userJS: no (throws TypeError) — ReadableStreamOperations.cpp +void setUpReadableStreamDefaultReader(JSC::JSGlobalObject*, JSReadableStreamDefaultReader*, JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp +void setUpReadableStreamBYOBReader(JSC::JSGlobalObject*, JSReadableStreamBYOBReader*, JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp +JSC::JSPromise* readableStreamReaderGenericCancel(JSC::JSGlobalObject*, JSReadableStreamReaderBase*, JSC::JSValue reason); // userJS: yes — ReadableStreamOperations.cpp +void readableStreamReaderGenericInitialize(JSC::JSGlobalObject*, JSReadableStreamReaderBase*, JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp +void readableStreamReaderGenericRelease(JSC::JSGlobalObject*, JSReadableStreamReaderBase*); // userJS: no (also runs Bun's native-handle updateRef(false) gate) — ReadableStreamOperations.cpp + +// Stream-level state ops. +JSC::JSPromise* readableStreamCancel(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue reason); // userJS: yes — ReadableStreamOperations.cpp +void readableStreamClose(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes (read-request close-steps dispatch) — ReadableStreamOperations.cpp +void readableStreamError(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue error); // userJS: yes (error-steps dispatch) — ReadableStreamOperations.cpp +// Bun helper used by every consumer teardown: closes the stream iff its state still allows +// it. Callers: BunStreamConsumers.cpp, BunStreamSource.cpp, JSDirectStreamController.cpp. +void readableStreamCloseIfPossible(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — ReadableStreamOperations.cpp +void readableStreamAddReadRequest(JSC::VM&, JSReadableStream*, JSReadRequest*); // userJS: no — ReadableStreamOperations.cpp +void readableStreamAddReadIntoRequest(JSC::VM&, JSReadableStream*, JSReadIntoRequest*); // userJS: no — ReadableStreamOperations.cpp +void readableStreamFulfillReadRequest(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue chunk, bool done); // userJS: yes (read-request dispatch) — ReadableStreamOperations.cpp +void readableStreamFulfillReadIntoRequest(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSArrayBufferView* chunk, bool done); // userJS: yes (read-into dispatch) — ReadableStreamOperations.cpp +size_t readableStreamGetNumReadRequests(JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp +size_t readableStreamGetNumReadIntoRequests(JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp +bool readableStreamHasDefaultReader(JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp +bool readableStreamHasBYOBReader(JSReadableStream*); // userJS: no — ReadableStreamOperations.cpp + +// Tee / from / pipe entry points. +// Bun: `cloneForBranch2` is Bun's `shouldClone` (Response.clone passes true; the public +// tee() passes false). ALSO runs materializeIfNeeded first. +std::pair readableStreamTee(JSC::JSGlobalObject*, JSReadableStream*, bool cloneForBranch2); // userJS: yes — ReadableStreamOperations.cpp +std::pair readableStreamDefaultTee(JSC::JSGlobalObject*, JSReadableStream*, bool cloneForBranch2); // userJS: yes — ReadableStreamOperations.cpp +std::pair readableByteStreamTee(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — ReadableStreamOperations.cpp +// spec ReadableStreamFromIterable(asyncIterable) — `ReadableStream.from`. +JSReadableStream* readableStreamFromIterable(JSC::JSGlobalObject*, JSC::JSValue asyncIterable); // userJS: yes — ReadableStreamOperations.cpp + +// Non-JavaScript SourceKind algorithm ARMS owned by THIS file. The controller's pull/cancel +// dispatch is a TOTAL `switch (m_algorithms.kind)` in JSReadableStreamDefaultController.cpp / +// JSReadableByteStreamController.cpp; every arm whose BODY lives in a different file (per the +// owner rule) is declared here so the two files have a declared bridge. `branch` is the +// controller's m_algorithms.teeBranchIndex (0 or 1). +// TeeBranch / ByteTeeBranch (context = the JSStreamTeeState): +JSC::JSPromise* defaultTeePullAlgorithm(JSC::JSGlobalObject*, JSStreamTeeState*, uint8_t branch); // userJS: yes — ReadableStreamOperations.cpp +JSC::JSPromise* defaultTeeCancelAlgorithm(JSC::JSGlobalObject*, JSStreamTeeState*, uint8_t branch, JSC::JSValue reason); // userJS: yes — ReadableStreamOperations.cpp +JSC::JSPromise* byteTeePullAlgorithm(JSC::JSGlobalObject*, JSStreamTeeState*, uint8_t branch); // userJS: yes — ReadableStreamOperations.cpp +JSC::JSPromise* byteTeeCancelAlgorithm(JSC::JSGlobalObject*, JSStreamTeeState*, uint8_t branch, JSC::JSValue reason); // userJS: yes — ReadableStreamOperations.cpp +// FromIterable (the controller's algorithmContext is the JSStreamFromIterableContext): +JSC::JSPromise* fromIterablePullAlgorithm(JSC::JSGlobalObject*, JSReadableStreamDefaultController*); // userJS: yes (iterator `next`) — ReadableStreamOperations.cpp +JSC::JSPromise* fromIterableCancelAlgorithm(JSC::JSGlobalObject*, JSReadableStreamDefaultController*, JSC::JSValue reason); // userJS: yes (iterator `return`) — ReadableStreamOperations.cpp +// (The Transform arm's cross-file targets are transformStreamDefaultSource{Pull,Cancel}Algorithm +// below; the Native arm's are nativeSource{Start,Pull,Cancel} in the BunStreamSource.cpp +// section; the CrossRealm arms are with the rest of CrossRealmTransform.cpp.) +// `signal` is the JSAbortSignal WRAPPER cell (nullptr = no signal); the pipe op roots it. +// Byte sources are supported: per spec, the pipe always acquires a DEFAULT reader. +JSC::JSPromise* readableStreamPipeTo(JSC::JSGlobalObject*, JSReadableStream* source, JSWritableStream* destination, bool preventClose, bool preventAbort, bool preventCancel, JSC::JSObject* signal = nullptr); // userJS: yes — ReadableStreamOperations.cpp (allocates + populates the op cell, then hands it to startPipeToOperation; the state machine lives in JSStreamPipeToOperation.cpp) + +// Controller set-up. Each takes the START RESULT, not a start method — the caller (the +// FromUnderlyingSource op or an internal Create*) already ran the start algorithm; this op +// only reacts to it. pull/cancel/size/kind/context members are populated on the controller +// by the CALLER. +void setUpReadableStreamDefaultController(JSC::JSGlobalObject*, JSReadableStream*, JSReadableStreamDefaultController*, JSC::JSValue startResult, double highWaterMark); // userJS: yes (thenable startResult) — ReadableStreamOperations.cpp +void setUpReadableStreamDefaultControllerFromUnderlyingSource(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue underlyingSource, const UnderlyingSourceDict&, double highWaterMark, JSC::JSObject* sizeAlgorithm); // userJS: yes (invokes the user `start`) — ReadableStreamOperations.cpp +void setUpReadableByteStreamController(JSC::JSGlobalObject*, JSReadableStream*, JSReadableByteStreamController*, JSC::JSValue startResult, double highWaterMark, std::optional autoAllocateChunkSize); // userJS: yes (thenable startResult) — ReadableStreamOperations.cpp +void setUpReadableByteStreamControllerFromUnderlyingSource(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue underlyingSource, const UnderlyingSourceDict&, double highWaterMark); // userJS: yes (invokes the user `start`) — ReadableStreamOperations.cpp + +// JSReadableStreamDefaultReader.cpp + +void readableStreamDefaultReaderRead(JSC::JSGlobalObject*, JSReadableStreamDefaultReader*, JSReadRequest*); // userJS: yes ([[PullSteps]] → user pull; the TOTAL ControllerKind dispatch) — JSReadableStreamDefaultReader.cpp +void queueStreamsMicrotask(JSC::JSGlobalObject*, JSC::JSFunction* handler, JSC::JSValue value, JSC::JSValue context); // userJS: no — WebStreamsMisc.cpp +JSC::JSValue readableStreamDefaultReaderTryReadFromQueue(JSC::JSGlobalObject*, JSReadableStreamDefaultReader*); // userJS: yes (a drained queue can pull) — JSReadableStreamDefaultReader.cpp +void readableStreamDefaultReaderRelease(JSC::JSGlobalObject*, JSReadableStreamDefaultReader*); // userJS: yes (error-steps dispatch) — JSReadableStreamDefaultReader.cpp +void readableStreamDefaultReaderErrorReadRequests(JSC::JSGlobalObject*, JSReadableStreamDefaultReader*, JSC::JSValue error); // userJS: yes — JSReadableStreamDefaultReader.cpp +// Bun public `reader.readMany()`: returns the `{value,size,done}` object synchronously OR +// a promise of one. +// Restores the stream's construction-time async-context snapshot around a user +// source callback (pull/cancel and the direct pull). Defined in WebStreamsMisc.cpp. +class StreamAsyncContextScope { + WTF_MAKE_NONCOPYABLE(StreamAsyncContextScope); + +public: + StreamAsyncContextScope(JSC::JSGlobalObject*, JSReadableStream*); + ~StreamAsyncContextScope(); + +private: + JSC::VM& m_vm; + JSC::InternalFieldTuple* m_asyncContextData { nullptr }; + JSC::JSValue m_previous; +}; + +enum class ConsumerFillStep : uint8_t { Done, + Pending }; +// The buffered-consumer pump step (BunStreamConsumers.cpp): bulk queue drain into `chunks`, +// or one pending spec read when the queue is empty. Throws on an errored stream. +ConsumerFillStep readableStreamDefaultReaderFillFromQueue(JSC::JSGlobalObject*, JSReadableStreamDefaultReader*, JSC::JSArray* chunks, JSC::JSPromise** pendingRead); // userJS: yes — JSReadableStreamDefaultReader.cpp +JSC::JSValue readableStreamDefaultReaderReadMany(JSC::JSGlobalObject*, JSReadableStreamDefaultReader*); // userJS: yes — JSReadableStreamDefaultReader.cpp + +// JSReadableStreamBYOBReader.cpp + +// `min` arrives via [EnforceRange] unsigned long long (already range-checked ≥ 1). +void readableStreamBYOBReaderRead(JSC::JSGlobalObject*, JSReadableStreamBYOBReader*, JSC::JSArrayBufferView* view, uint64_t min, JSReadIntoRequest*); // userJS: yes — JSReadableStreamBYOBReader.cpp +void readableStreamBYOBReaderRelease(JSC::JSGlobalObject*, JSReadableStreamBYOBReader*); // userJS: yes — JSReadableStreamBYOBReader.cpp +void readableStreamBYOBReaderErrorReadIntoRequests(JSC::JSGlobalObject*, JSReadableStreamBYOBReader*, JSC::JSValue error); // userJS: yes — JSReadableStreamBYOBReader.cpp + +// JSReadableStreamDefaultController.cpp + +void readableStreamDefaultControllerCallPullIfNeeded(JSC::JSGlobalObject*, JSReadableStreamDefaultController*); // userJS: yes (user pull) — JSReadableStreamDefaultController.cpp +bool readableStreamDefaultControllerShouldCallPull(JSReadableStreamDefaultController*); // userJS: no — JSReadableStreamDefaultController.cpp +void readableStreamDefaultControllerClearAlgorithms(JSReadableStreamDefaultController*); // userJS: no — JSReadableStreamDefaultController.cpp +void readableStreamDefaultControllerClose(JSC::JSGlobalObject*, JSReadableStreamDefaultController*); // userJS: yes — JSReadableStreamDefaultController.cpp +void readableStreamDefaultControllerEnqueue(JSC::JSGlobalObject*, JSReadableStreamDefaultController*, JSC::JSValue chunk); // userJS: yes (user size(); throws) — JSReadableStreamDefaultController.cpp +void readableStreamDefaultControllerError(JSC::JSGlobalObject*, JSReadableStreamDefaultController*, JSC::JSValue error); // userJS: yes — JSReadableStreamDefaultController.cpp +std::optional readableStreamDefaultControllerGetDesiredSize(JSReadableStreamDefaultController*); // userJS: no (nullopt = spec null) — JSReadableStreamDefaultController.cpp +bool readableStreamDefaultControllerHasBackpressure(JSReadableStreamDefaultController*); // userJS: no — JSReadableStreamDefaultController.cpp +bool readableStreamDefaultControllerCanCloseOrEnqueue(JSReadableStreamDefaultController*); // userJS: no — JSReadableStreamDefaultController.cpp + +// JSReadableByteStreamController.cpp + +void readableByteStreamControllerCallPullIfNeeded(JSC::JSGlobalObject*, JSReadableByteStreamController*); // userJS: yes (user pull) — JSReadableByteStreamController.cpp +bool readableByteStreamControllerShouldCallPull(JSReadableByteStreamController*); // userJS: no — JSReadableByteStreamController.cpp +void readableByteStreamControllerClearAlgorithms(JSReadableByteStreamController*); // userJS: no — JSReadableByteStreamController.cpp +void readableByteStreamControllerClearPendingPullIntos(JSReadableByteStreamController*); // userJS: no — JSReadableByteStreamController.cpp +void readableByteStreamControllerClose(JSC::JSGlobalObject*, JSReadableByteStreamController*); // userJS: yes — JSReadableByteStreamController.cpp +// The consumer of ProcessPullIntoDescriptorsUsingQueue's MarkedArgumentBuffer (see below): +// the descriptor is NO LONGER in [[pendingPullIntos]] when this runs; the caller's +// MarkedArgumentBuffer is what keeps it (and its later siblings) alive across this call. +void readableByteStreamControllerCommitPullIntoDescriptor(JSC::JSGlobalObject*, JSReadableStream*, JSPullIntoDescriptor*); // userJS: yes (fulfill dispatch) — JSReadableByteStreamController.cpp +JSC::JSArrayBufferView* readableByteStreamControllerConvertPullIntoDescriptor(JSC::JSGlobalObject*, JSPullIntoDescriptor*); // userJS: no (intrinsic view construction only) — JSReadableByteStreamController.cpp +void readableByteStreamControllerEnqueue(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSC::JSArrayBufferView* chunk); // userJS: yes; throws — JSReadableByteStreamController.cpp +void readableByteStreamControllerEnqueueChunkToQueue(JSReadableByteStreamController*, RefPtr&&, size_t byteOffset, size_t byteLength); // userJS: no — JSReadableByteStreamController.cpp +void readableByteStreamControllerEnqueueClonedChunkToQueue(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSC::ArrayBuffer&, size_t byteOffset, size_t byteLength); // userJS: yes (a takeAbruptCompletion catch site; errors the controller then rethrows) — JSReadableByteStreamController.cpp +void readableByteStreamControllerEnqueueDetachedPullIntoToQueue(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSPullIntoDescriptor*); // userJS: yes; throws — JSReadableByteStreamController.cpp +void readableByteStreamControllerError(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSC::JSValue error); // userJS: yes — JSReadableByteStreamController.cpp +void readableByteStreamControllerFillHeadPullIntoDescriptor(JSReadableByteStreamController*, size_t size, JSPullIntoDescriptor*); // userJS: no — JSReadableByteStreamController.cpp +bool readableByteStreamControllerFillPullIntoDescriptorFromQueue(JSReadableByteStreamController*, JSPullIntoDescriptor*); // userJS: no — JSReadableByteStreamController.cpp +void readableByteStreamControllerFillReadRequestFromQueue(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSReadRequest*); // userJS: yes — JSReadableByteStreamController.cpp +JSReadableStreamBYOBRequest* readableByteStreamControllerGetBYOBRequest(JSC::JSGlobalObject*, JSReadableByteStreamController*); // userJS: no (nullptr = spec null) — JSReadableByteStreamController.cpp +std::optional readableByteStreamControllerGetDesiredSize(JSReadableByteStreamController*); // userJS: no — JSReadableByteStreamController.cpp +void readableByteStreamControllerHandleQueueDrain(JSC::JSGlobalObject*, JSReadableByteStreamController*); // userJS: yes — JSReadableByteStreamController.cpp +void readableByteStreamControllerInvalidateBYOBRequest(JSReadableByteStreamController*); // userJS: no — JSReadableByteStreamController.cpp +// Fills `filledPullIntos` with every descriptor whose fill completes from the queue, SHIFTING +// each one out of the visited [[pendingPullIntos]] deque as the spec requires. From that +// moment `filledPullIntos` is those descriptors' ONLY root: nothing else reaches them, and a +// heap-spilled WTF::Vector is invisible to the conservative scan. That is exactly why the +// out-param is a JSC::MarkedArgumentBuffer — its overflow storage IS registered with the +// VM's mark-list set, so every entry (inline and spilled) stays GC-visible while the caller's +// commit loop runs user JS (Commit is userJS: yes). MarkedArgumentBuffer is non-copyable, +// hence the caller-provided out-param instead of a return value. +// CALLER CONTRACT: commit these one at a time via +// readableByteStreamControllerCommitPullIntoDescriptor +// (jsCast(filledPullIntos.at(i))); because each commit can run user +// JS, re-read all reentrantly-mutable controller/stream state after every commit — never +// cache a view of it across the loop. +void readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(JSReadableByteStreamController*, JSC::MarkedArgumentBuffer& filledPullIntos); // userJS: no — JSReadableByteStreamController.cpp +void readableByteStreamControllerProcessReadRequestsUsingQueue(JSC::JSGlobalObject*, JSReadableByteStreamController*); // userJS: yes — JSReadableByteStreamController.cpp +void readableByteStreamControllerPullInto(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSC::JSArrayBufferView* view, uint64_t min, JSReadIntoRequest*); // userJS: yes — JSReadableByteStreamController.cpp +void readableByteStreamControllerRespond(JSC::JSGlobalObject*, JSReadableByteStreamController*, uint64_t bytesWritten); // userJS: yes; throws — JSReadableByteStreamController.cpp +void readableByteStreamControllerRespondInClosedState(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSPullIntoDescriptor* firstDescriptor); // userJS: yes — JSReadableByteStreamController.cpp +void readableByteStreamControllerRespondInReadableState(JSC::JSGlobalObject*, JSReadableByteStreamController*, uint64_t bytesWritten, JSPullIntoDescriptor*); // userJS: yes; throws — JSReadableByteStreamController.cpp +void readableByteStreamControllerRespondInternal(JSC::JSGlobalObject*, JSReadableByteStreamController*, uint64_t bytesWritten); // userJS: yes; throws — JSReadableByteStreamController.cpp +void readableByteStreamControllerRespondWithNewView(JSC::JSGlobalObject*, JSReadableByteStreamController*, JSC::JSArrayBufferView* view); // userJS: yes; throws — JSReadableByteStreamController.cpp +JSPullIntoDescriptor* readableByteStreamControllerShiftPendingPullInto(JSReadableByteStreamController*); // userJS: no — JSReadableByteStreamController.cpp + +// WritableStreamOperations.cpp + +JSWritableStream* createWritableStream(JSC::JSGlobalObject*, SinkKind, JSC::JSCell* algorithmContext, JSC::JSValue startResult, double highWaterMark, JSC::JSObject* sizeAlgorithm); // userJS: yes — WritableStreamOperations.cpp +void initializeWritableStream(JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +bool isWritableStreamLocked(JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +JSWritableStreamDefaultWriter* acquireWritableStreamDefaultWriter(JSC::JSGlobalObject*, JSWritableStream*); // userJS: no (throws TypeError if locked) — WritableStreamOperations.cpp +void setUpWritableStreamDefaultWriter(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*, JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +// "signal abort on [[abortController]]" runs user `abort` listeners SYNCHRONOUSLY. +JSC::JSPromise* writableStreamAbort(JSC::JSGlobalObject*, JSWritableStream*, JSC::JSValue reason); // userJS: yes — WritableStreamOperations.cpp +JSC::JSPromise* writableStreamClose(JSC::JSGlobalObject*, JSWritableStream*); // userJS: yes — WritableStreamOperations.cpp +JSC::JSPromise* writableStreamAddWriteRequest(JSC::JSGlobalObject*, JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +bool writableStreamCloseQueuedOrInFlight(JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +void writableStreamDealWithRejection(JSC::JSGlobalObject*, JSWritableStream*, JSC::JSValue error); // userJS: yes — WritableStreamOperations.cpp +void writableStreamStartErroring(JSC::JSGlobalObject*, JSWritableStream*, JSC::JSValue reason); // userJS: yes — WritableStreamOperations.cpp +void writableStreamFinishErroring(JSC::JSGlobalObject*, JSWritableStream*); // userJS: yes (user abort algorithm) — WritableStreamOperations.cpp +void writableStreamFinishInFlightWrite(JSC::JSGlobalObject*, JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +void writableStreamFinishInFlightWriteWithError(JSC::JSGlobalObject*, JSWritableStream*, JSC::JSValue error); // userJS: yes — WritableStreamOperations.cpp +void writableStreamFinishInFlightClose(JSC::JSGlobalObject*, JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +void writableStreamFinishInFlightCloseWithError(JSC::JSGlobalObject*, JSWritableStream*, JSC::JSValue error); // userJS: yes — WritableStreamOperations.cpp +bool writableStreamHasOperationMarkedInFlight(JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +void writableStreamMarkCloseRequestInFlight(JSC::VM&, JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +void writableStreamMarkFirstWriteRequestInFlight(JSC::VM&, JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +void writableStreamRejectCloseAndClosedPromiseIfNeeded(JSC::JSGlobalObject*, JSWritableStream*); // userJS: no — WritableStreamOperations.cpp +void writableStreamUpdateBackpressure(JSC::JSGlobalObject*, JSWritableStream*, bool backpressure); // userJS: no — WritableStreamOperations.cpp +void setUpWritableStreamDefaultController(JSC::JSGlobalObject*, JSWritableStream*, JSWritableStreamDefaultController*, JSC::JSValue startResult, double highWaterMark); // userJS: yes (thenable startResult) — WritableStreamOperations.cpp +void setUpWritableStreamDefaultControllerFromUnderlyingSink(JSC::JSGlobalObject*, JSWritableStream*, JSC::JSValue underlyingSink, const UnderlyingSinkDict&, double highWaterMark, JSC::JSObject* sizeAlgorithm); // userJS: yes (invokes the user `start`) — WritableStreamOperations.cpp + +// JSWritableStreamDefaultWriter.cpp + +JSC::JSPromise* writableStreamDefaultWriterAbort(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*, JSC::JSValue reason); // userJS: yes — JSWritableStreamDefaultWriter.cpp +JSC::JSPromise* writableStreamDefaultWriterClose(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*); // userJS: yes — JSWritableStreamDefaultWriter.cpp +JSC::JSPromise* writableStreamDefaultWriterCloseWithErrorPropagation(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*); // userJS: yes — JSWritableStreamDefaultWriter.cpp +void writableStreamDefaultWriterEnsureClosedPromiseRejected(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*, JSC::JSValue error); // userJS: no — JSWritableStreamDefaultWriter.cpp +void writableStreamDefaultWriterEnsureReadyPromiseRejected(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*, JSC::JSValue error); // userJS: no — JSWritableStreamDefaultWriter.cpp +std::optional writableStreamDefaultWriterGetDesiredSize(JSWritableStreamDefaultWriter*); // userJS: no (nullopt = spec null) — JSWritableStreamDefaultWriter.cpp +void writableStreamDefaultWriterRelease(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*); // userJS: no — JSWritableStreamDefaultWriter.cpp +JSC::JSPromise* writableStreamDefaultWriterWrite(JSC::JSGlobalObject*, JSWritableStreamDefaultWriter*, JSC::JSValue chunk); // userJS: yes (user size() FIRST, then re-checks [[stream]]) — JSWritableStreamDefaultWriter.cpp + +// JSWritableStreamDefaultController.cpp + +void writableStreamDefaultControllerAdvanceQueueIfNeeded(JSC::JSGlobalObject*, JSWritableStreamDefaultController*); // userJS: yes — JSWritableStreamDefaultController.cpp +void writableStreamDefaultControllerClearAlgorithms(JSWritableStreamDefaultController*); // userJS: no — JSWritableStreamDefaultController.cpp +void writableStreamDefaultControllerClose(JSC::JSGlobalObject*, JSWritableStreamDefaultController*); // userJS: yes — JSWritableStreamDefaultController.cpp +void writableStreamDefaultControllerError(JSC::JSGlobalObject*, JSWritableStreamDefaultController*, JSC::JSValue error); // userJS: yes — JSWritableStreamDefaultController.cpp +void writableStreamDefaultControllerErrorIfNeeded(JSC::JSGlobalObject*, JSWritableStreamDefaultController*, JSC::JSValue error); // userJS: yes — JSWritableStreamDefaultController.cpp +bool writableStreamDefaultControllerGetBackpressure(JSWritableStreamDefaultController*); // userJS: no — JSWritableStreamDefaultController.cpp +// Calls the user size(); a sanctioned takeAbruptCompletion catch site (converts the abrupt +// completion into ErrorIfNeeded and returns 1 — it NEVER throws out). +double writableStreamDefaultControllerGetChunkSize(JSC::JSGlobalObject*, JSWritableStreamDefaultController*, JSC::JSValue chunk); // userJS: yes — JSWritableStreamDefaultController.cpp +double writableStreamDefaultControllerGetDesiredSize(JSWritableStreamDefaultController*); // userJS: no — JSWritableStreamDefaultController.cpp +void writableStreamDefaultControllerProcessClose(JSC::JSGlobalObject*, JSWritableStreamDefaultController*); // userJS: yes (user close algorithm) — JSWritableStreamDefaultController.cpp +void writableStreamDefaultControllerProcessWrite(JSC::JSGlobalObject*, JSWritableStreamDefaultController*, JSC::JSValue chunk); // userJS: yes (user write algorithm) — JSWritableStreamDefaultController.cpp +void writableStreamDefaultControllerWrite(JSC::JSGlobalObject*, JSWritableStreamDefaultController*, JSC::JSValue chunk, double chunkSize); // userJS: yes — JSWritableStreamDefaultController.cpp + +// TransformStreamOperations.cpp + +// The internal-creation parallel of createReadableStream. +JSTransformStream* createTransformStream(JSC::JSGlobalObject*, TransformerKind, JSC::JSCell* algorithmContext, double writableHighWaterMark = 1, JSC::JSObject* writableSizeAlgorithm = nullptr, double readableHighWaterMark = 0, JSC::JSObject* readableSizeAlgorithm = nullptr); // userJS: yes — TransformStreamOperations.cpp +void initializeTransformStream(JSC::JSGlobalObject*, JSTransformStream*, JSC::JSPromise* startPromise, double writableHighWaterMark, JSC::JSObject* writableSizeAlgorithm, double readableHighWaterMark, JSC::JSObject* readableSizeAlgorithm); // userJS: yes — TransformStreamOperations.cpp +void transformStreamError(JSC::JSGlobalObject*, JSTransformStream*, JSC::JSValue error); // userJS: yes — TransformStreamOperations.cpp +void transformStreamErrorWritableAndUnblockWrite(JSC::JSGlobalObject*, JSTransformStream*, JSC::JSValue error); // userJS: yes — TransformStreamOperations.cpp +void transformStreamSetBackpressure(JSC::JSGlobalObject*, JSTransformStream*, bool backpressure); // userJS: no — TransformStreamOperations.cpp +void transformStreamUnblockWrite(JSC::JSGlobalObject*, JSTransformStream*); // userJS: no — TransformStreamOperations.cpp +void setUpTransformStreamDefaultController(JSC::VM&, JSTransformStream*, JSTransformStreamDefaultController*); // userJS: no — TransformStreamOperations.cpp +void setUpTransformStreamDefaultControllerFromTransformer(JSC::JSGlobalObject*, JSTransformStream*, JSC::JSValue transformer, const TransformerDict&); // userJS: no — TransformStreamOperations.cpp +JSC::JSPromise* transformStreamDefaultSinkWriteAlgorithm(JSC::JSGlobalObject*, JSTransformStream*, JSC::JSValue chunk); // userJS: yes — TransformStreamOperations.cpp +JSC::JSPromise* transformStreamDefaultSinkAbortAlgorithm(JSC::JSGlobalObject*, JSTransformStream*, JSC::JSValue reason); // userJS: yes — TransformStreamOperations.cpp +JSC::JSPromise* transformStreamDefaultSinkCloseAlgorithm(JSC::JSGlobalObject*, JSTransformStream*); // userJS: yes (user flush) — TransformStreamOperations.cpp +JSC::JSPromise* transformStreamDefaultSourceCancelAlgorithm(JSC::JSGlobalObject*, JSTransformStream*, JSC::JSValue reason); // userJS: yes — TransformStreamOperations.cpp +JSC::JSPromise* transformStreamDefaultSourcePullAlgorithm(JSC::JSGlobalObject*, JSTransformStream*); // userJS: no — TransformStreamOperations.cpp + +// JSTransformStreamDefaultController.cpp + +void transformStreamDefaultControllerClearAlgorithms(JSTransformStreamDefaultController*); // userJS: no — JSTransformStreamDefaultController.cpp +// A sanctioned takeAbruptCompletion catch site (catches the readable-side enqueue's abrupt +// completion, errors the writable, then throws stream.[[readable]].[[storedError]]). +void transformStreamDefaultControllerEnqueue(JSC::JSGlobalObject*, JSTransformStreamDefaultController*, JSC::JSValue chunk); // userJS: yes; throws — JSTransformStreamDefaultController.cpp +void transformStreamDefaultControllerError(JSC::JSGlobalObject*, JSTransformStreamDefaultController*, JSC::JSValue error); // userJS: yes — JSTransformStreamDefaultController.cpp +JSC::JSPromise* transformStreamDefaultControllerPerformTransform(JSC::JSGlobalObject*, JSTransformStreamDefaultController*, JSC::JSValue chunk); // userJS: yes (user transform) — JSTransformStreamDefaultController.cpp +void transformStreamDefaultControllerTerminate(JSC::JSGlobalObject*, JSTransformStreamDefaultController*); // userJS: yes — JSTransformStreamDefaultController.cpp + +// JSTextEncoderStream.cpp — the TransformerKind::TextEncoder algorithm ARMS. Invoked from +// transformStreamDefaultControllerPerformTransform's / the flush dispatch's TOTAL +// `switch (m_transformerKind)` in JSTransformStreamDefaultController.cpp; declared here so +// the two files have a declared bridge. + +JSC::JSPromise* textEncoderStreamTransform(JSC::JSGlobalObject*, JSTextEncoderStream*, JSTransformStreamDefaultController*, JSC::JSValue chunk); // userJS: yes (enqueue can hit a user size algorithm) — JSTextEncoderStream.cpp +JSC::JSPromise* textEncoderStreamFlush(JSC::JSGlobalObject*, JSTextEncoderStream*, JSTransformStreamDefaultController*); // userJS: yes — JSTextEncoderStream.cpp + +// JSTextDecoderStream.cpp — the TransformerKind::TextDecoder algorithm ARMS. Same +// dispatch/bridge relationship as the TextEncoder arms above. + +JSC::JSPromise* textDecoderStreamTransform(JSC::JSGlobalObject*, JSTextDecoderStream*, JSTransformStreamDefaultController*, JSC::JSValue chunk); // userJS: yes — JSTextDecoderStream.cpp +JSC::JSPromise* textDecoderStreamFlush(JSC::JSGlobalObject*, JSTextDecoderStream*, JSTransformStreamDefaultController*); // userJS: yes — JSTextDecoderStream.cpp + +// CrossRealmTransform.cpp — transferable streams are NOT implemented. These signatures are +// FROZEN, but the .cpp may be a stub whose entry points assert / throw; the per-class +// transfer / transfer-receiving steps have no declarations here. + +void crossRealmTransformSendError(JSC::JSGlobalObject*, WebCore::MessagePort&, JSC::JSValue error); // userJS: yes — CrossRealmTransform.cpp +// Throws on serialization failure. `type` is the closed protocol set. +void packAndPostMessage(JSC::JSGlobalObject*, WebCore::MessagePort&, CrossRealmMessageType, JSC::JSValue value); // userJS: yes — CrossRealmTransform.cpp +// Returns true = normal completion. On false the error has already been forwarded via +// crossRealmTransformSendError and the abrupt completion is left on the throw scope +// (resolve it with takeAbruptCompletion above). +bool packAndPostMessageHandlingError(JSC::JSGlobalObject*, WebCore::MessagePort&, CrossRealmMessageType, JSC::JSValue value); // userJS: yes — CrossRealmTransform.cpp +void setUpCrossRealmTransformReadable(JSC::JSGlobalObject*, JSReadableStream*, WebCore::MessagePort&); // userJS: yes — CrossRealmTransform.cpp +void setUpCrossRealmTransformWritable(JSC::JSGlobalObject*, JSWritableStream*, WebCore::MessagePort&); // userJS: yes — CrossRealmTransform.cpp + +// JSStreamPipeToOperation.cpp — the pipeTo state machine. readableStreamPipeTo +// (ReadableStreamOperations.cpp, above) ONLY validates, allocates the JSStreamPipeToOperation +// cell, sets the reader/writer back-edges, and calls THIS entry point. Everything else — the +// loop, the four propagation checks, shutdown / shutdown-with-an-action / finalize, the +// onPipe* reaction bodies, and the signal's boundPipeAbortAlgorithm body — lives in +// JSStreamPipeToOperation.cpp as methods on the cell (JSStreamPipeToOperation.h). + +// Registers the source/dest [[closedPromise]] reactions and the GC-visited signal abort +// algorithm, then starts the read/write loop. The op cell was fully populated by the caller. +void startPipeToOperation(JSC::JSGlobalObject*, JSStreamPipeToOperation*); // userJS: yes — JSStreamPipeToOperation.cpp +// The PipeTo read request's steps. JSReadRequest.cpp's kind switch dispatches into the cell here. +void pipeToReadRequestChunkSteps(JSC::JSGlobalObject*, JSStreamPipeToOperation*, JSC::JSValue chunk); // userJS: yes — JSStreamPipeToOperation.cpp +void pipeToReadRequestCloseSteps(JSC::JSGlobalObject*, JSStreamPipeToOperation*); // userJS: yes — JSStreamPipeToOperation.cpp +void pipeToReadRequestErrorSteps(JSC::JSGlobalObject*, JSStreamPipeToOperation*, JSC::JSValue error); // userJS: yes — JSStreamPipeToOperation.cpp + +// JSReadableStreamAsyncIterator.cpp — its methods are on the cell; nothing is cross-file. + +// THE BUN LAYER + +// BunStreamSource.cpp — the lazy native source and the native-sink pumps. + +// lazyLoadStream: installs the Native default controller (or the empty fast path). +void materializeNativeSource(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamSource.cpp + +// The SourceKind::Native algorithm ARMS. The pull/cancel dispatch is a TOTAL +// `switch (m_algorithms.kind)` in JSReadableStreamDefaultController.cpp (a Native source is +// ALWAYS a default controller); these bodies live HERE per BunStreamSource.h's owner rule, +// so this is the declared bridge between the two files. The controller's algorithmContext is +// the JSNativeStreamSourceAdapter for all three. +JSC::JSValue nativeSourceStart(JSC::JSGlobalObject*, JSReadableStreamDefaultController*); // userJS: no (native handle.start; enqueues the drain value) — BunStreamSource.cpp +JSC::JSPromise* nativeSourcePull(JSC::JSGlobalObject*, JSReadableStreamDefaultController*); // userJS: no (native handle.pull; its promise's reactions are onNativePull*) — BunStreamSource.cpp +JSC::JSPromise* nativeSourceCancel(JSC::JSGlobalObject*, JSReadableStreamDefaultController*, JSC::JSValue reason); // userJS: no (native handle.cancel + teardown) — BunStreamSource.cpp +// The JSSink entry point (GlobalObject::assignToStream's body). Returns undefined or +// a JSPromise (the Signal protocol's value). +JSC::JSValue assignToStream(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue jsSinkController); // userJS: yes — BunStreamSource.cpp +// The direct-stream → native-JSSink path. Returns undefined | JSPromise. +JSC::JSValue readDirectStream(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSObject* sinkController, JSC::JSObject* underlyingSource); // userJS: yes — BunStreamSource.cpp +// The generic pump into a native JSSink controller. +JSC::JSPromise* readStreamIntoSink(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSObject* sink); // userJS: yes — BunStreamSource.cpp +// The ResumableSink protocol. Returns undefined (encoded). +JSC::JSValue assignStreamIntoResumableSink(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSObject* resumableSink); // userJS: yes — BunStreamSource.cpp + +// JSDirectStreamController.cpp — direct-stream materialization + the direct controller. + +// Installs a JSDirectStreamController of the given flavor on the stream, nulls the stream's +// m_directUnderlyingSource, and sets m_bunMode = Default. +void setUpDirectStreamController(JSC::JSGlobalObject*, JSReadableStream*, DirectSinkKind, double highWaterMark); // userJS: yes — JSDirectStreamController.cpp + +// BunStreamConsumers.cpp — Bun.readableStreamTo*, the buffered fast path, the direct +// consumers, and the generic accumulators. These are the native entry points; their +// host-function wrappers (installed on BunObject and reached from js2native) are declared in +// BunStreamConsumers.h. + +JSC::JSValue readableStreamToText(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +JSC::JSValue readableStreamToArray(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +JSC::JSValue readableStreamToArrayBuffer(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +JSC::JSValue readableStreamToBytes(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +JSC::JSValue readableStreamToJSON(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +JSC::JSValue readableStreamToBlob(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +JSC::JSValue readableStreamToFormData(JSC::JSGlobalObject*, JSReadableStream*, JSC::JSValue contentType); // userJS: yes — BunStreamConsumers.cpp + +// The buffered fast path: returns the native handle's own .text()/.arrayBuffer()/... promise, +// or the EMPTY JSValue if the fast path does not apply. `method` is the property name to +// [[Get]] on the handle ("text" | "arrayBuffer" | "bytes" | "json" | "blob"). MAY THROW +// (propagate without setting m_disturbed). +JSC::JSValue tryUseReadableStreamBufferedFastPath(JSC::JSGlobalObject*, JSReadableStream*, const JSC::Identifier& method); // userJS: yes — BunStreamConsumers.cpp + +// The generic toText path: the readMany array pump + a single chunk-array -> string +// conversion (BunStreamConsumers.cpp convertChunksToText); BOM-strips its result. +JSC::JSValue readableStreamIntoText(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +// toArray's generic path (getReader + readMany until done). +JSC::JSValue readableStreamIntoArray(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +// Drop ONE leading U+FEFF, and only on the generic toText path. +WTF::String withoutUTF8BOM(const WTF::String&); // userJS: no — BunStreamConsumers.cpp + +// The three *Direct conversion paths. +JSC::JSValue readableStreamToTextDirect(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +JSC::JSValue readableStreamToArrayDirect(JSC::JSGlobalObject*, JSReadableStream*); // userJS: yes — BunStreamConsumers.cpp +// The ONE-SHOT direct→ArrayBuffer/Uint8Array conversion: no persistent controller, no +// reader. Allocates a WebCore::JSOneShotDirectSink (JSOneShotDirectSink.h) as the throwaway +// `controller` handed to the user's `pull` exactly once; its start/write/end/close/flush are +// OWN JSBoundFunctions over the boundOneShot* targets (JSStreamsRuntime.h). It deliberately +// does NOT reuse boundDirect* / JSDirectStreamController. +JSC::JSValue consumeDirectStreamToArrayBuffer(JSC::JSGlobalObject*, JSReadableStream*, bool asUint8Array); // userJS: yes — BunStreamConsumers.cpp + +// (readableStreamCloseIfPossible is declared in the ReadableStreamOperations.cpp block above +// — that file owns its body. It is only USED throughout this file.) + +// WebStreamsExports.cpp — the extern "C" / Rust FFI surface. Every symbol keeps its EXACT +// name and signature; the ReadableStreamTag discriminants are FROZEN by assert_ffi_discr! on +// the Rust side (Invalid=-1, JavaScript=0, Blob=1, File=2, Direct=3 [never emitted], Bytes=4). + +// Builds a DirectPending stream that pulls from an async iterator / async-generator function +// (the ReadableStreamTag__tagged coercion path). This is Bun's direct-mode wrapper, NOT the +// spec's readableStreamFromIterable. Owned by WebStreamsExports.cpp: the tag protocol is that +// file's surface, and it is this function's only caller. +// An async-generator-function value is accepted directly (started eagerly). BunAsyncIterableSource.cpp +bool isNonHostAsyncGeneratorFunction(JSC::JSObject*); +JSReadableStream* readableStreamFromAsyncIterator(JSC::JSGlobalObject*, JSC::JSValue asyncIterableOrGeneratorFn); // userJS: yes — WebStreamsExports.cpp + +} // namespace WebStreams +} // namespace Bun + +// The extern "C" block is outside any namespace. +// All are DEFINED in WebStreamsExports.cpp. userJS: yes for all except the pure predicates. +extern "C" { + +// THE tag protocol. Writes the out-params; the async-iterator arm may REPLACE +// *possibleReadableStream with a newly-built DirectPending stream. userJS: yes. +int32_t ReadableStreamTag__tagged(Zig::GlobalObject*, JSC::EncodedJSValue* possibleReadableStream, void** ptr); + +// The ReadableStream__* set. +bool ReadableStream__tee(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*, JSC::EncodedJSValue* possibleReadableStream1, JSC::EncodedJSValue* possibleReadableStream2); // userJS: yes +bool ReadableStream__isDisturbed(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*); // userJS: no +bool ReadableStream__isLocked(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*); // userJS: no +// no-op unless the reader slot holds a REAL reader (the direct/native lock is a no-op here). +void ReadableStream__cancel(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*); // userJS: yes +// NO sentinel guard (reachable on a NativeSink-controlled stream). +void ReadableStream__cancelWithReason(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*, JSC::EncodedJSValue reason); // userJS: yes +void ReadableStream__detach(JSC::EncodedJSValue possibleReadableStream, Zig::GlobalObject*); // userJS: no +JSC::EncodedJSValue ReadableStream__empty(Zig::GlobalObject*); // userJS: no +JSC::EncodedJSValue ReadableStream__used(Zig::GlobalObject*); // userJS: no +JSC::EncodedJSValue ReadableStream__errored(Zig::GlobalObject*, JSC::EncodedJSValue reason); // userJS: no +JSC::EncodedJSValue ZigGlobalObject__createNativeReadableStream(Zig::GlobalObject*, JSC::EncodedJSValue nativePtr); // userJS: no +JSC::EncodedJSValue ZigGlobalObject__readableStreamToArrayBuffer(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes +JSC::EncodedJSValue ZigGlobalObject__readableStreamToBytes(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes +JSC::EncodedJSValue ZigGlobalObject__readableStreamToText(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes +JSC::EncodedJSValue ZigGlobalObject__readableStreamToJSON(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes +JSC::EncodedJSValue ZigGlobalObject__readableStreamToBlob(Zig::GlobalObject*, JSC::EncodedJSValue stream); // userJS: yes +JSC::EncodedJSValue ZigGlobalObject__readableStreamToFormData(Zig::GlobalObject*, JSC::EncodedJSValue stream, JSC::EncodedJSValue contentType); // userJS: yes +// Caller: ResumableSink.rs; returns encoded undefined. +JSC::EncodedJSValue Bun__assignStreamIntoResumableSink(JSC::JSGlobalObject*, JSC::EncodedJSValue stream, JSC::EncodedJSValue sink); // userJS: yes + +} // extern "C" diff --git a/src/jsc/bindings/webcore/streams/WebStreamsMisc.cpp b/src/jsc/bindings/webcore/streams/WebStreamsMisc.cpp new file mode 100644 index 000000000000..f4cf18118a89 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/WebStreamsMisc.cpp @@ -0,0 +1,390 @@ +#include "config.h" +#include +#include +#include "WebStreamsInternals.h" + +#include "JSReadableStream.h" + +#include "BunClientData.h" +#include "JSDOMConvertNumbers.h" +#include "JSStreamsRuntime.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +// spec ExtractHighWaterMark(strategy, defaultHWM) +double extractHighWaterMark(JSGlobalObject* globalObject, const QueuingStrategyDict& strategy, double defaultHWM) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!strategy.highWaterMark) + return defaultHWM; + double highWaterMark = *strategy.highWaterMark; + if (std::isnan(highWaterMark) || highWaterMark < 0) { + throwRangeError(globalObject, scope, "The queuing strategy's highWaterMark must be a non-negative, non-NaN number"_s); + return 0; + } + return highWaterMark; +} + +// spec ExtractSizeAlgorithm(strategy): nullptr means the default `() => 1` algorithm. +JSObject* extractSizeAlgorithm(const QueuingStrategyDict& strategy) +{ + if (strategy.size.isEmpty()) + return nullptr; + return asObject(strategy.size); +} + +// spec IsNonNegativeNumber(v). Non-throwing leaf: pure type + range test, no coercion. +bool isNonNegativeNumber(JSValue value) +{ + if (!value.isNumber()) + return false; + double number = value.asNumber(); + if (std::isnan(number)) + return false; + return number >= 0; +} + +// Queues handler(value, contextCell) — the reaction-convention argument order. +void queueStreamsMicrotask(JSGlobalObject* globalObject, JSFunction* handler, JSValue value, JSValue context) +{ + QueuedTask task { nullptr, InternalMicrotask::BunInvokeJobWithArguments, 0, globalObject, handler, value, context }; + globalObject->vm().queueMicrotask(WTF::move(task)); +} + +bool canTransferArrayBuffer(JSC::ArrayBuffer& buffer) +{ + return !buffer.isDetached() && buffer.isDetachable(); +} + +// spec TransferArrayBuffer(O) at the impl level: detach O (and every view over it) and +// return a fresh ArrayBuffer over the same block. No JSArrayBuffer wrapper is created — +// callers hand out views over the impl, and JSC materializes a wrapper only if user code +// reads `.buffer`. +RefPtr transferArrayBufferImpl(JSGlobalObject* globalObject, JSC::ArrayBuffer& buffer) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(!buffer.isDetached()); + if (!buffer.isDetachable()) [[unlikely]] { + throwTypeError(globalObject, scope, "Cannot transfer an ArrayBuffer that is not detachable"_s); + return nullptr; + } + JSC::ArrayBufferContents contents; + bool transferred = buffer.transferTo(vm, contents); + ASSERT_UNUSED(transferred, transferred); + return JSC::ArrayBuffer::create(WTF::move(contents)); +} + +// spec CloneAsUint8Array(O): CloneArrayBuffer(O.[[ViewedArrayBuffer]], O.[[ByteOffset]], +// O.[[ByteLength]], %ArrayBuffer%) then Construct(%Uint8Array%, « buffer »). +JSUint8Array* cloneAsUint8Array(JSGlobalObject* globalObject, JSArrayBufferView* view) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(!view->isDetached()); + size_t byteLength = view->byteLength(); + RefPtr cloned = ArrayBuffer::tryCreate(view->span()); + if (!cloned) [[unlikely]] { + throwRangeError(globalObject, scope, "Cannot allocate the cloned ArrayBuffer required by the readable byte stream"_s); + return nullptr; + } + RELEASE_AND_RETURN(scope, JSUint8Array::create(globalObject, globalObject->typedArrayStructure(TypeUint8, false), WTF::move(cloned), 0, byteLength)); +} + +// spec CanCopyDataBlockBytes(toBuffer, toIndex, fromBuffer, fromIndex, count). Non-throwing leaf. +bool canCopyDataBlockBytes(JSC::ArrayBuffer& toBuffer, size_t toIndex, JSC::ArrayBuffer& fromBuffer, size_t fromIndex, size_t count) +{ + ArrayBuffer* to = &toBuffer; + ArrayBuffer* from = &fromBuffer; + if (to == from) + return false; + if (to->isDetached() || from->isDetached()) + return false; + size_t toByteLength = to->byteLength(); + if (count > toByteLength || toIndex > toByteLength - count) + return false; + size_t fromByteLength = from->byteLength(); + if (count > fromByteLength || fromIndex > fromByteLength - count) + return false; + return true; +} + +// The WebIDL dictionary conversions. Each performs the observable, alphabetical-order +// [[Get]]s of the real conversion and throws the mandated TypeErrors. + +// WebIDL: a non-nullish, non-object value cannot be converted to a dictionary. +static bool checkDictionaryReceiver(JSC::VM& vm, JSGlobalObject* globalObject, JSValue value, ASCIILiteral message) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + if (value.isUndefinedOrNull()) + return false; + if (!value.isObject()) { + throwTypeError(globalObject, scope, message); + return false; + } + return true; +} + +// A present callback-typed member must be callable; returns the empty JSValue when absent. +static JSValue getCallbackMember(JSC::VM& vm, JSGlobalObject* globalObject, JSObject* object, JSC::PropertyName propertyName, ASCIILiteral message) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue value = object->get(globalObject, propertyName); + RETURN_IF_EXCEPTION(scope, {}); + if (value.isUndefined()) + return JSValue(); + if (!value.isCallable()) { + throwTypeError(globalObject, scope, message); + return {}; + } + return value; +} + +UnderlyingSinkDict convertUnderlyingSinkDict(JSGlobalObject* globalObject, JSValue underlyingSink) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto& names = WebCore::builtinNames(vm); + UnderlyingSinkDict result {}; + bool isObject = checkDictionaryReceiver(vm, globalObject, underlyingSink, "The underlying sink must be an object"_s); + RETURN_IF_EXCEPTION(scope, result); + if (!isObject) + return result; + auto* sinkObject = asObject(underlyingSink); + + result.abort = getCallbackMember(vm, globalObject, sinkObject, builtinNames(vm).abortPublicName(), "The underlying sink's 'abort' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + result.close = getCallbackMember(vm, globalObject, sinkObject, names.closePublicName(), "The underlying sink's 'close' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + result.start = getCallbackMember(vm, globalObject, sinkObject, names.startPublicName(), "The underlying sink's 'start' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + + // `type` is `any`: presence alone is recorded (the constructor's RangeError). + JSValue type = sinkObject->get(globalObject, vm.propertyNames->type); + RETURN_IF_EXCEPTION(scope, result); + result.hasType = !type.isUndefined(); + + result.write = getCallbackMember(vm, globalObject, sinkObject, names.writePublicName(), "The underlying sink's 'write' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + return result; +} + +TransformerDict convertTransformerDict(JSGlobalObject* globalObject, JSValue transformer) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto& names = WebCore::builtinNames(vm); + TransformerDict result {}; + bool isObject = checkDictionaryReceiver(vm, globalObject, transformer, "The transformer must be an object"_s); + RETURN_IF_EXCEPTION(scope, result); + if (!isObject) + return result; + auto* transformerObject = asObject(transformer); + + result.cancel = getCallbackMember(vm, globalObject, transformerObject, names.cancelPublicName(), "The transformer's 'cancel' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + result.flush = getCallbackMember(vm, globalObject, transformerObject, builtinNames(vm).flushPublicName(), "The transformer's 'flush' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + + // `readableType` / `writableType` are `any`: presence alone triggers the RangeError. + JSValue readableType = transformerObject->get(globalObject, builtinNames(vm).readableTypePublicName()); + RETURN_IF_EXCEPTION(scope, result); + result.hasReadableType = !readableType.isUndefined(); + + result.start = getCallbackMember(vm, globalObject, transformerObject, names.startPublicName(), "The transformer's 'start' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + result.transform = getCallbackMember(vm, globalObject, transformerObject, builtinNames(vm).transformPublicName(), "The transformer's 'transform' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + + JSValue writableType = transformerObject->get(globalObject, builtinNames(vm).writableTypePublicName()); + RETURN_IF_EXCEPTION(scope, result); + result.hasWritableType = !writableType.isUndefined(); + return result; +} + +QueuingStrategyDict convertQueuingStrategyDict(JSGlobalObject* globalObject, JSValue strategy) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto& names = WebCore::builtinNames(vm); + QueuingStrategyDict result {}; + bool isObject = checkDictionaryReceiver(vm, globalObject, strategy, "The queuing strategy must be an object"_s); + RETURN_IF_EXCEPTION(scope, result); + if (!isObject) + return result; + auto* strategyObject = asObject(strategy); + + JSValue highWaterMark = strategyObject->get(globalObject, names.highWaterMarkPublicName()); + RETURN_IF_EXCEPTION(scope, result); + if (!highWaterMark.isUndefined()) { + double value = highWaterMark.toNumber(globalObject); + RETURN_IF_EXCEPTION(scope, result); + result.highWaterMark = value; + } + + result.size = getCallbackMember(vm, globalObject, strategyObject, vm.propertyNames->size, "The queuing strategy's 'size' property must be a function"_s); + RETURN_IF_EXCEPTION(scope, result); + return result; +} + +// Promise helpers. + +// Web IDL "a promise resolved with v": a NEW promise resolved with v; a promise/thenable v is +// adopted through a job, one reaction later than ES PromiseResolve's identity would fire — the +// delay is observable (WPT transform abort/cancel-during-start races), so never use identity here. +// For values that are provably not thenables (undefined, internal arrays/objects we +// created): fulfill directly instead of running the observable resolve machinery. +StreamAsyncContextScope::StreamAsyncContextScope(JSGlobalObject* globalObject, JSReadableStream* stream) + : m_vm(globalObject->vm()) +{ + JSValue snapshot = stream->m_asyncContext.get(); + if (!snapshot || snapshot.isUndefinedOrNull()) + return; + m_asyncContextData = globalObject->m_asyncContextData.get(); + m_previous = m_asyncContextData->getInternalField(0); + m_asyncContextData->putInternalField(m_vm, 0, snapshot); +} + +StreamAsyncContextScope::~StreamAsyncContextScope() +{ + if (m_asyncContextData) + m_asyncContextData->putInternalField(m_vm, 0, m_previous); +} + +// obj.name(args...) with obj as |this|; the EMPTY value if `name` is not callable. +JSValue invokeOptionalMethod(JSGlobalObject* globalObject, JSObject* object, const Identifier& name, const MarkedArgumentBuffer& args) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + JSValue method = object->get(globalObject, name); + RETURN_IF_EXCEPTION(scope, {}); + if (!method.isCallable()) + return {}; + RELEASE_AND_RETURN(scope, JSC::call(globalObject, method, object, args, "method is not a function"_s)); +} + +bool errorCodeIs(JSGlobalObject* globalObject, JSValue error, ASCIILiteral code) +{ + auto& vm = getVM(globalObject); + auto catchScope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + if (!error || !error.isObject()) + return false; + JSValue codeValue = asObject(error)->getIfPropertyExists(globalObject, WebCore::builtinNames(vm).codePublicName()); + if (catchScope.exception()) [[unlikely]] { + catchScope.clearExceptionExceptTermination(); + return false; + } + if (!codeValue || !codeValue.isString()) + return false; + String codeString = asString(codeValue)->value(globalObject); + if (catchScope.exception()) [[unlikely]] { + catchScope.clearExceptionExceptTermination(); + return false; + } + return codeString == StringView(code); +} + +// Shared [bound-convention] wrapper: target(contextCell, ...callArgs). +JSC::JSBoundFunction* createStreamsBoundHandler(JSGlobalObject* globalObject, JSFunction* target, JSCell* context) +{ + auto& vm = getVM(globalObject); + MarkedArgumentBuffer boundArgs; + boundArgs.append(context); + ASSERT(!boundArgs.hasOverflowed()); + return JSBoundFunction::create(vm, globalObject, target, jsUndefined(), ArgList(boundArgs), 1, nullptr, + makeSource("streamsBoundHandler"_s, SourceOrigin(), SourceTaintedOrigin::Untainted)); +} + +JSPromise* promiseFulfilledWith(JSGlobalObject* globalObject, JSValue value) +{ + auto& vm = getVM(globalObject); + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + promise->fulfill(vm, value); + return promise; +} + +JSPromise* promiseResolvedWith(JSGlobalObject* globalObject, JSValue value) +{ + auto& vm = getVM(globalObject); + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + promise->resolve(globalObject, vm, value); + return promise; +} + +JSPromise* promiseRejectedWith(JSGlobalObject* globalObject, JSValue reason) +{ + return JSPromise::rejectedPromise(globalObject, reason); +} + +// "resolve promise with v": the same thenable lookup as promiseResolvedWith. +void resolvePromise(JSGlobalObject* globalObject, JSPromise* promise, JSValue value) +{ + promise->resolve(globalObject, getVM(globalObject), value); +} + +void rejectPromise(JSGlobalObject* globalObject, JSPromise* promise, JSValue reason) +{ + promise->reject(getVM(globalObject), reason); +} + +void markPromiseAsHandled(VM&, JSPromise* promise) +{ + promise->markAsHandled(); +} + +// The ONE sanctioned completion-record catch: the spec's "interpreting X as a completion +// record" sites only. Empty return = a VM termination the caller must propagate. +JSValue takeAbruptCompletion(JSGlobalObject*, TopExceptionScope& catchScope) +{ + JSC::Exception* exception = catchScope.exception(); + ASSERT(exception); + JSValue thrown = exception->value(); + if (!catchScope.clearExceptionExceptTermination()) [[unlikely]] + return {}; + return thrown; +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +using namespace JSC; + +// [reaction-convention] _MISC group: the shared no-op fulfillment step that returns undefined. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onReturnUndefined, (JSGlobalObject*, CallFrame*)) +{ + return JSValue::encode(jsUndefined()); +} + +// The per-realm ByteLengthQueuingStrategy `size` function: GetV(chunk, "byteLength"). +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsByteLengthQueuingStrategySize, (JSGlobalObject * globalObject, CallFrame* callFrame)) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + RELEASE_AND_RETURN(scope, JSValue::encode(callFrame->argument(0).get(globalObject, vm.propertyNames->byteLength))); +} + +// The per-realm CountQueuingStrategy `size` function: always 1. +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsCountQueuingStrategySize, (JSGlobalObject*, CallFrame*)) +{ + return JSValue::encode(jsNumber(1)); +} + +} // namespace WebCore diff --git a/src/jsc/bindings/webcore/streams/WritableStreamOperations.cpp b/src/jsc/bindings/webcore/streams/WritableStreamOperations.cpp new file mode 100644 index 000000000000..b98e6af800e0 --- /dev/null +++ b/src/jsc/bindings/webcore/streams/WritableStreamOperations.cpp @@ -0,0 +1,559 @@ +#include "config.h" +#include "WebStreamsInternals.h" + +#include "AbortController.h" +#include "JSAbortController.h" +#include "JSDOMGlobalObject.h" +#include "JSDOMWrapperCache.h" +#include "JSStreamsRuntime.h" +#include "JSWritableStream.h" +#include "JSWritableStreamDefaultController.h" +#include "JSWritableStreamDefaultWriter.h" +#include "StreamQueue.h" +#include "ZigGlobalObject.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Bun { +namespace WebStreams { + +using namespace JSC; + +static void clearPendingAbortRequest(JSWritableStream* stream) +{ + stream->m_pendingAbortRequest.promise.clear(); + stream->m_pendingAbortRequest.reason.clear(); + stream->m_pendingAbortRequest.wasAlreadyErroring = false; +} + +// SetUpWritableStreamDefaultController, minus reacting to the start result. The algorithm +// slots and the size algorithm were already populated on `controller` by the caller. +static void setUpWritableStreamDefaultControllerBeforeStart(JSC::VM& vm, JSGlobalObject* globalObject, JSWritableStream* stream, JSWritableStreamDefaultController* controller, double highWaterMark) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + ASSERT(!stream->m_controller); + controller->m_stream.set(vm, controller, stream); + stream->m_controller.set(vm, stream, controller); + { + WTF::Locker locker { controller->cellLock() }; + controller->m_queue.resetQueue(locker); + } + + auto* domGlobalObject = defaultGlobalObject(globalObject); + JSValue abortController = WebCore::toJSNewlyCreated(globalObject, domGlobalObject, WebCore::AbortController::create(*domGlobalObject->scriptExecutionContext())); + RETURN_IF_EXCEPTION(scope, ); + controller->m_abortController.set(vm, controller, asObject(abortController)); + + controller->m_started = false; + controller->m_strategyHWM = highWaterMark; + + bool backpressure = writableStreamDefaultControllerGetBackpressure(controller); + RELEASE_AND_RETURN(scope, writableStreamUpdateBackpressure(globalObject, stream, backpressure)); +} + +// "Let startPromise be a promise resolved with startResult; upon fulfillment / rejection…". +// A non-thenable primitive needs no promise: the fulfillment handler is queued directly. +static void reactToWritableControllerStart(JSC::VM& vm, JSGlobalObject* globalObject, JSWritableStreamDefaultController* controller, JSValue startResult) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + auto* runtime = JSStreamsRuntime::from(globalObject); + if (startResult.isObject()) { + JSPromise* startPromise = promiseResolvedWith(globalObject, startResult); + RETURN_IF_EXCEPTION(scope, ); + startPromise->performPromiseThenWithContext(vm, globalObject, runtime->onWSControllerStartFulfilled(), runtime->onWSControllerStartRejected(), jsUndefined(), controller); + return; + } + QueuedTask task { nullptr, InternalMicrotask::BunPerformMicrotaskJob, 0, globalObject, runtime->onWSControllerStartFulfilled(), globalObject->m_asyncContextData.get()->getInternalField(0), startResult, controller }; + vm.queueMicrotask(WTF::move(task)); +} + +JSWritableStream* createWritableStream(JSGlobalObject* globalObject, SinkKind kind, JSCell* algorithmContext, JSValue startResult, double highWaterMark, JSObject* sizeAlgorithm) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(highWaterMark >= 0); + + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* stream = JSWritableStream::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + initializeWritableStream(stream); + + auto* controller = JSWritableStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = kind; + if (algorithmContext) + controller->m_algorithms.algorithmContext.set(vm, controller, algorithmContext); + if (sizeAlgorithm) + controller->m_strategySizeAlgorithm.set(vm, controller, sizeAlgorithm); + + setUpWritableStreamDefaultController(globalObject, stream, controller, startResult, highWaterMark); + RETURN_IF_EXCEPTION(scope, nullptr); + return stream; +} + +void initializeWritableStream(JSWritableStream* stream) +{ + stream->m_state = WritableStreamState::Writable; + stream->m_storedError.clear(); + stream->m_writer.clear(); + stream->m_controller.clear(); + stream->m_inFlightWriteRequest.clear(); + stream->m_closeRequest.clear(); + stream->m_inFlightCloseRequest.clear(); + clearPendingAbortRequest(stream); + { + WTF::Locker locker { stream->cellLock() }; + stream->m_writeRequests.clear(); + } + stream->m_backpressure = false; +} + +bool isWritableStreamLocked(JSWritableStream* stream) +{ + return !!stream->m_writer; +} + +JSWritableStreamDefaultWriter* acquireWritableStreamDefaultWriter(JSGlobalObject* globalObject, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* writer = JSWritableStreamDefaultWriter::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + setUpWritableStreamDefaultWriter(globalObject, writer, stream); + RETURN_IF_EXCEPTION(scope, nullptr); + return writer; +} + +void setUpWritableStreamDefaultWriter(JSGlobalObject* globalObject, JSWritableStreamDefaultWriter* writer, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (isWritableStreamLocked(stream)) { + throwException(globalObject, scope, Bun::createError(globalObject, Bun::ErrorCode::ERR_INVALID_STATE_TypeError, "Invalid state: WritableStream is locked"_s)); + return; + } + writer->m_stream.set(vm, writer, stream); + stream->m_writer.set(vm, stream, writer); + + switch (stream->m_state) { + case WritableStreamState::Writable: { + if (!writableStreamCloseQueuedOrInFlight(stream) && stream->m_backpressure) + writer->m_readyPromise.set(vm, writer, JSPromise::create(vm, globalObject->promiseStructure())); + else { + JSPromise* ready = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + writer->m_readyPromise.set(vm, writer, ready); + } + writer->m_closedPromise.set(vm, writer, JSPromise::create(vm, globalObject->promiseStructure())); + return; + } + case WritableStreamState::Erroring: { + JSPromise* ready = promiseRejectedWith(globalObject, stream->m_storedError.get()); + RETURN_IF_EXCEPTION(scope, ); + markPromiseAsHandled(vm, ready); + writer->m_readyPromise.set(vm, writer, ready); + writer->m_closedPromise.set(vm, writer, JSPromise::create(vm, globalObject->promiseStructure())); + return; + } + case WritableStreamState::Closed: { + JSPromise* ready = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + writer->m_readyPromise.set(vm, writer, ready); + JSPromise* closed = promiseFulfilledWith(globalObject, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + writer->m_closedPromise.set(vm, writer, closed); + return; + } + case WritableStreamState::Errored: { + JSValue storedError = stream->m_storedError.get(); + JSPromise* ready = promiseRejectedWith(globalObject, storedError); + RETURN_IF_EXCEPTION(scope, ); + markPromiseAsHandled(vm, ready); + writer->m_readyPromise.set(vm, writer, ready); + JSPromise* closed = promiseRejectedWith(globalObject, storedError); + RETURN_IF_EXCEPTION(scope, ); + markPromiseAsHandled(vm, closed); + writer->m_closedPromise.set(vm, writer, closed); + return; + } + } +} + +JSPromise* writableStreamAbort(JSGlobalObject* globalObject, JSWritableStream* stream, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + if (stream->m_state == WritableStreamState::Closed || stream->m_state == WritableStreamState::Errored) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + + // Signaling abort runs the user's `abort` listeners synchronously. + auto* controller = stream->m_controller.get(); + ASSERT(controller && controller->m_abortController); + uncheckedDowncast(controller->m_abortController.get())->wrapped().abort(*defaultGlobalObject(globalObject), reason); + RETURN_IF_EXCEPTION(scope, nullptr); + + WritableStreamState state = stream->m_state; + if (state == WritableStreamState::Closed || state == WritableStreamState::Errored) + RELEASE_AND_RETURN(scope, promiseFulfilledWith(globalObject, JSC::jsUndefined())); + if (stream->m_pendingAbortRequest.promise) + return stream->m_pendingAbortRequest.promise.get(); + + ASSERT(state == WritableStreamState::Writable || state == WritableStreamState::Erroring); + bool wasAlreadyErroring = false; + if (state == WritableStreamState::Erroring) { + wasAlreadyErroring = true; + reason = jsUndefined(); + } + + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + stream->m_pendingAbortRequest.promise.set(vm, stream, promise); + stream->m_pendingAbortRequest.reason.set(vm, stream, reason); + stream->m_pendingAbortRequest.wasAlreadyErroring = wasAlreadyErroring; + if (!wasAlreadyErroring) { + writableStreamStartErroring(globalObject, stream, reason); + RETURN_IF_EXCEPTION(scope, nullptr); + } + return promise; +} + +JSPromise* writableStreamClose(JSGlobalObject* globalObject, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + WritableStreamState state = stream->m_state; + if (state == WritableStreamState::Closed || state == WritableStreamState::Errored) + RELEASE_AND_RETURN(scope, promiseRejectedWith(globalObject, createTypeError(globalObject, "Cannot close a WritableStream that is closed or errored"_s))); + ASSERT(state == WritableStreamState::Writable || state == WritableStreamState::Erroring); + ASSERT(!writableStreamCloseQueuedOrInFlight(stream)); + + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + stream->m_closeRequest.set(vm, stream, promise); + + auto* writer = stream->m_writer.get(); + if (writer && stream->m_backpressure && state == WritableStreamState::Writable) { + resolvePromise(globalObject, writer->m_readyPromise.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, nullptr); + } + writableStreamDefaultControllerClose(globalObject, stream->m_controller.get()); + RETURN_IF_EXCEPTION(scope, nullptr); + return promise; +} + +// Non-throwing leaf: only allocates the write-request promise cell. +JSPromise* writableStreamAddWriteRequest(JSGlobalObject* globalObject, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + ASSERT(isWritableStreamLocked(stream)); + ASSERT(stream->m_state == WritableStreamState::Writable); + auto* promise = JSPromise::create(vm, globalObject->promiseStructure()); + { + WTF::Locker locker { stream->cellLock() }; + stream->m_writeRequests.append(WriteBarrier(vm, stream, promise)); + } + return promise; +} + +bool writableStreamCloseQueuedOrInFlight(JSWritableStream* stream) +{ + return !!stream->m_closeRequest || !!stream->m_inFlightCloseRequest; +} + +void writableStreamDealWithRejection(JSGlobalObject* globalObject, JSWritableStream* stream, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (stream->m_state == WritableStreamState::Writable) { + writableStreamStartErroring(globalObject, stream, error); + RETURN_IF_EXCEPTION(scope, ); + return; + } + ASSERT(stream->m_state == WritableStreamState::Erroring); + RELEASE_AND_RETURN(scope, writableStreamFinishErroring(globalObject, stream)); +} + +void writableStreamStartErroring(JSGlobalObject* globalObject, JSWritableStream* stream, JSValue reason) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + ASSERT(!stream->m_storedError); + ASSERT(stream->m_state == WritableStreamState::Writable); + auto* controller = stream->m_controller.get(); + ASSERT(controller); + + stream->m_state = WritableStreamState::Erroring; + stream->m_storedError.set(vm, stream, reason); + if (auto* writer = stream->m_writer.get()) { + writableStreamDefaultWriterEnsureReadyPromiseRejected(globalObject, writer, reason); + RETURN_IF_EXCEPTION(scope, ); + } + if (!writableStreamHasOperationMarkedInFlight(stream) && controller->m_started) + RELEASE_AND_RETURN(scope, writableStreamFinishErroring(globalObject, stream)); +} + +void writableStreamFinishErroring(JSGlobalObject* globalObject, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + ASSERT(stream->m_state == WritableStreamState::Erroring); + ASSERT(!writableStreamHasOperationMarkedInFlight(stream)); + stream->m_state = WritableStreamState::Errored; + + auto* controller = stream->m_controller.get(); + controller->errorSteps(); + + JSValue storedError = stream->m_storedError.get(); + // Rejecting runs no user JS, so nothing can mutate the deque under this loop. + for (auto& writeRequest : stream->m_writeRequests) { + rejectPromise(globalObject, writeRequest.get(), storedError); + RETURN_IF_EXCEPTION(scope, ); + } + { + WTF::Locker locker { stream->cellLock() }; + stream->m_writeRequests.clear(); + } + + if (!stream->m_pendingAbortRequest.promise) + RELEASE_AND_RETURN(scope, writableStreamRejectCloseAndClosedPromiseIfNeeded(globalObject, stream)); + + auto* abortPromise = stream->m_pendingAbortRequest.promise.get(); + JSValue abortReason = stream->m_pendingAbortRequest.reason.get(); + bool wasAlreadyErroring = stream->m_pendingAbortRequest.wasAlreadyErroring; + clearPendingAbortRequest(stream); + + if (wasAlreadyErroring) { + rejectPromise(globalObject, abortPromise, storedError); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, writableStreamRejectCloseAndClosedPromiseIfNeeded(globalObject, stream)); + } + + JSPromise* promise = controller->abortSteps(globalObject, abortReason); + RETURN_IF_EXCEPTION(scope, ); + ASSERT(promise); + auto* runtime = JSStreamsRuntime::from(globalObject); + auto* context = InternalFieldTuple::create(vm, globalObject->internalFieldTupleStructure(), abortPromise, stream); + promise->performPromiseThenWithContext(vm, globalObject, runtime->onWSAbortStepsFulfilled(), runtime->onWSAbortStepsRejected(), jsUndefined(), context); +} + +void writableStreamFinishInFlightWrite(JSGlobalObject* globalObject, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_inFlightWriteRequest); + resolvePromise(globalObject, stream->m_inFlightWriteRequest.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + stream->m_inFlightWriteRequest.clear(); +} + +void writableStreamFinishInFlightWriteWithError(JSGlobalObject* globalObject, JSWritableStream* stream, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_inFlightWriteRequest); + rejectPromise(globalObject, stream->m_inFlightWriteRequest.get(), error); + RETURN_IF_EXCEPTION(scope, ); + stream->m_inFlightWriteRequest.clear(); + ASSERT(stream->m_state == WritableStreamState::Writable || stream->m_state == WritableStreamState::Erroring); + RELEASE_AND_RETURN(scope, writableStreamDealWithRejection(globalObject, stream, error)); +} + +void writableStreamFinishInFlightClose(JSGlobalObject* globalObject, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_inFlightCloseRequest); + resolvePromise(globalObject, stream->m_inFlightCloseRequest.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + stream->m_inFlightCloseRequest.clear(); + + WritableStreamState state = stream->m_state; + ASSERT(state == WritableStreamState::Writable || state == WritableStreamState::Erroring); + if (state == WritableStreamState::Erroring) { + stream->m_storedError.clear(); + if (stream->m_pendingAbortRequest.promise) { + resolvePromise(globalObject, stream->m_pendingAbortRequest.promise.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + clearPendingAbortRequest(stream); + } + } + stream->m_state = WritableStreamState::Closed; + if (auto* writer = stream->m_writer.get()) { + resolvePromise(globalObject, writer->m_closedPromise.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + } + ASSERT(!stream->m_pendingAbortRequest.promise); + ASSERT(!stream->m_storedError); +} + +void writableStreamFinishInFlightCloseWithError(JSGlobalObject* globalObject, JSWritableStream* stream, JSValue error) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_inFlightCloseRequest); + rejectPromise(globalObject, stream->m_inFlightCloseRequest.get(), error); + RETURN_IF_EXCEPTION(scope, ); + stream->m_inFlightCloseRequest.clear(); + ASSERT(stream->m_state == WritableStreamState::Writable || stream->m_state == WritableStreamState::Erroring); + if (stream->m_pendingAbortRequest.promise) { + rejectPromise(globalObject, stream->m_pendingAbortRequest.promise.get(), error); + RETURN_IF_EXCEPTION(scope, ); + clearPendingAbortRequest(stream); + } + RELEASE_AND_RETURN(scope, writableStreamDealWithRejection(globalObject, stream, error)); +} + +bool writableStreamHasOperationMarkedInFlight(JSWritableStream* stream) +{ + return !!stream->m_inFlightWriteRequest || !!stream->m_inFlightCloseRequest; +} + +void writableStreamMarkCloseRequestInFlight(VM& vm, JSWritableStream* stream) +{ + ASSERT(!stream->m_inFlightCloseRequest); + ASSERT(stream->m_closeRequest); + stream->m_inFlightCloseRequest.set(vm, stream, stream->m_closeRequest.get()); + stream->m_closeRequest.clear(); +} + +void writableStreamMarkFirstWriteRequestInFlight(VM& vm, JSWritableStream* stream) +{ + ASSERT(!stream->m_inFlightWriteRequest); + ASSERT(!stream->m_writeRequests.isEmpty()); + JSPromise* writeRequest = nullptr; + { + WTF::Locker locker { stream->cellLock() }; + writeRequest = stream->m_writeRequests.takeFirst().get(); + } + stream->m_inFlightWriteRequest.set(vm, stream, writeRequest); +} + +void writableStreamRejectCloseAndClosedPromiseIfNeeded(JSGlobalObject* globalObject, JSWritableStream* stream) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_state == WritableStreamState::Errored); + JSValue storedError = stream->m_storedError.get(); + if (stream->m_closeRequest) { + ASSERT(!stream->m_inFlightCloseRequest); + rejectPromise(globalObject, stream->m_closeRequest.get(), storedError); + RETURN_IF_EXCEPTION(scope, ); + stream->m_closeRequest.clear(); + } + if (auto* writer = stream->m_writer.get()) { + rejectPromise(globalObject, writer->m_closedPromise.get(), storedError); + RETURN_IF_EXCEPTION(scope, ); + markPromiseAsHandled(vm, writer->m_closedPromise.get()); + } +} + +void writableStreamUpdateBackpressure(JSGlobalObject* globalObject, JSWritableStream* stream, bool backpressure) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + ASSERT(stream->m_state == WritableStreamState::Writable); + ASSERT(!writableStreamCloseQueuedOrInFlight(stream)); + auto* writer = stream->m_writer.get(); + if (writer && backpressure != stream->m_backpressure) { + if (backpressure) + writer->m_readyPromise.set(vm, writer, JSPromise::create(vm, globalObject->promiseStructure())); + else { + resolvePromise(globalObject, writer->m_readyPromise.get(), jsUndefined()); + RETURN_IF_EXCEPTION(scope, ); + } + } + stream->m_backpressure = backpressure; +} + +void setUpWritableStreamDefaultController(JSGlobalObject* globalObject, JSWritableStream* stream, JSWritableStreamDefaultController* controller, JSValue startResult, double highWaterMark) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + setUpWritableStreamDefaultControllerBeforeStart(vm, globalObject, stream, controller, highWaterMark); + RETURN_IF_EXCEPTION(scope, ); + RELEASE_AND_RETURN(scope, reactToWritableControllerStart(vm, globalObject, controller, startResult)); +} + +void setUpWritableStreamDefaultControllerFromUnderlyingSink(JSGlobalObject* globalObject, JSWritableStream* stream, JSValue underlyingSink, const UnderlyingSinkDict& underlyingSinkDict, double highWaterMark, JSObject* sizeAlgorithm) +{ + auto& vm = getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + auto* domGlobalObject = defaultGlobalObject(globalObject); + auto* controller = JSWritableStreamDefaultController::create(vm, WebCore::getDOMStructure(vm, *domGlobalObject)); + controller->m_algorithms.kind = SinkKind::JavaScript; + controller->m_algorithms.underlyingObject.set(vm, controller, underlyingSink); + if (underlyingSinkDict.write) + controller->m_algorithms.method1.set(vm, controller, asObject(underlyingSinkDict.write)); + if (underlyingSinkDict.close) + controller->m_algorithms.method2.set(vm, controller, asObject(underlyingSinkDict.close)); + if (underlyingSinkDict.abort) + controller->m_algorithms.method3.set(vm, controller, asObject(underlyingSinkDict.abort)); + if (sizeAlgorithm) + controller->m_strategySizeAlgorithm.set(vm, controller, sizeAlgorithm); + + // The user `start` must observe a fully wired controller, so it runs between the two + // halves of SetUpWritableStreamDefaultController; its exception is rethrown. + setUpWritableStreamDefaultControllerBeforeStart(vm, globalObject, stream, controller, highWaterMark); + RETURN_IF_EXCEPTION(scope, ); + + JSValue startResult = jsUndefined(); + if (underlyingSinkDict.start) { + MarkedArgumentBuffer args; + args.append(controller); + ASSERT(!args.hasOverflowed()); + auto callData = JSC::getCallData(underlyingSinkDict.start); + ASSERT(callData.type != CallData::Type::None); + startResult = JSC::call(globalObject, underlyingSinkDict.start, callData, underlyingSink, args); + RETURN_IF_EXCEPTION(scope, ); + } + RELEASE_AND_RETURN(scope, reactToWritableControllerStart(vm, globalObject, controller, startResult)); +} + +} // namespace WebStreams +} // namespace Bun + +namespace WebCore { + +// Reactions to the promise returned by [[AbortSteps]] (WritableStreamFinishErroring). +// context = InternalFieldTuple{ the pending abort request's promise, the JSWritableStream }: +// the abort request was already detached from the stream when the reaction was registered. + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSAbortStepsFulfilled, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* abortRequestPromise = uncheckedDowncast(context->getInternalField(0)); + auto* stream = uncheckedDowncast(context->getInternalField(1)); + Bun::WebStreams::resolvePromise(globalObject, abortRequestPromise, JSC::jsUndefined()); + RETURN_IF_EXCEPTION(scope, {}); + Bun::WebStreams::writableStreamRejectCloseAndClosedPromiseIfNeeded(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsWebStreamsHandler_onWSAbortStepsRejected, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* context = uncheckedDowncast(callFrame->uncheckedArgument(1)); + auto* abortRequestPromise = uncheckedDowncast(context->getInternalField(0)); + auto* stream = uncheckedDowncast(context->getInternalField(1)); + Bun::WebStreams::rejectPromise(globalObject, abortRequestPromise, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + Bun::WebStreams::writableStreamRejectCloseAndClosedPromiseIfNeeded(globalObject, stream); + RETURN_IF_EXCEPTION(scope, {}); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +} // namespace WebCore diff --git a/src/jsc/event_loop.rs b/src/jsc/event_loop.rs index 9e7cdf41a2ed..46763240a0e1 100644 --- a/src/jsc/event_loop.rs +++ b/src/jsc/event_loop.rs @@ -1401,3 +1401,27 @@ pub(crate) fn __bun_spawn_sync_vm_set_event_loop(vm: *mut (), el: *mut ()) { pub(crate) fn __bun_spawn_sync_vm_swap_suppress_microtask_drain(vm: *mut (), v: bool) -> bool { vm_from_ptr(vm).suppress_microtask_drain.replace(v) } + +/// C++ (webcore/streams) entries for the deferred task queue: register/unregister a task that +/// runs right after the current microtask drain (see DeferredTaskQueue.rs). `ctx` identity is +/// the key; the callee must unregister before `ctx` is freed. +#[unsafe(no_mangle)] +pub extern "C" fn Bun__EventLoop__postDeferredTask( + vm: &VirtualMachine, + ctx: *mut core::ffi::c_void, + task: DeferredRepeatingTask, +) -> bool { + vm.event_loop_ref() + .deferred_tasks + .post_task(core::ptr::NonNull::new(ctx), task) +} + +#[unsafe(no_mangle)] +pub extern "C" fn Bun__EventLoop__unregisterDeferredTask( + vm: &VirtualMachine, + ctx: *mut core::ffi::c_void, +) -> bool { + vm.event_loop_ref() + .deferred_tasks + .unregister_task(core::ptr::NonNull::new(ctx)) +} diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index 98ed0418ee77..b3c27448ce79 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -867,6 +867,24 @@ where debug_assert!(self.flags.has_finalized()); } + // A response body stream suspended inside its `pull()` never settles the promise + // whose reactions consume the sink (`handleResolveStream` / `handleRejectStream`), + // so a client abort in that state reaches deinit with the sink still owned here. + // This is the owner's last exit: release it exactly like the settle paths do. + if let Some(wrapper_ptr) = self.sink.take() { + // SAFETY: deinit runs once, after `detach_response()` removed the uWS callbacks; + // the context is the sink's sole owner (see the `sink` field's doc comment). + let wrapper = unsafe { &mut *wrapper_ptr.as_ptr() }; + wrapper.sink.finalize(); + if let Some(sink_global) = wrapper.sink.global_this { + ResponseStreamJSSink::::detach( + &mut wrapper.sink.signal, + &sink_global, + ); + } + Self::destroy_sink(wrapper_ptr); + } + self.request_body_buf = Vec::new(); self.response_buf_owned = Vec::new(); self.response_weakref.deref(); diff --git a/src/runtime/webcore/Body.rs b/src/runtime/webcore/Body.rs index c3d50deb1af4..f4ddde38f9f3 100644 --- a/src/runtime/webcore/Body.rs +++ b/src/runtime/webcore/Body.rs @@ -1021,8 +1021,11 @@ impl Value { value.ensure_still_alive(); if let Some(readable) = ReadableStream::from_js(value, global_this)? { - if readable.is_disturbed(global_this) { - return Err(global_this.throw(format_args!("ReadableStream has already been used"))); + // fetch spec: a body init stream must be neither disturbed nor locked (TypeError). + if readable.is_disturbed(global_this) || readable.is_locked(global_this) { + return Err(global_this.throw_type_error(format_args!( + "Body object should not be disturbed or locked" + ))); } match readable.ptr { diff --git a/src/runtime/webcore/ReadableStream.rs b/src/runtime/webcore/ReadableStream.rs index 24bf8817d96c..2f0711a4a36d 100644 --- a/src/runtime/webcore/ReadableStream.rs +++ b/src/runtime/webcore/ReadableStream.rs @@ -98,6 +98,7 @@ unsafe extern "C" { possible_readable_stream: &mut JSValue, ptr: &mut *mut c_void, ) -> Tag; + safe fn ReadableStream__is(value: JSValue) -> bool; safe fn ReadableStream__isDisturbed( possible_readable_stream: JSValue, global_object: &JSGlobalObject, @@ -265,6 +266,11 @@ impl ReadableStream { ReadableStream__isLocked(self.value, global_object) } + /// A pure `dynamicDowncast` type test: no tagging, no conversion. + pub fn is_readable_stream(value: JSValue) -> bool { + ReadableStream__is(value) + } + pub fn from_js( value: JSValue, global_this: &JSGlobalObject, diff --git a/src/runtime/webcore/Request.rs b/src/runtime/webcore/Request.rs index f179a2a1ac77..aa2025ef52a7 100644 --- a/src/runtime/webcore/Request.rs +++ b/src/runtime/webcore/Request.rs @@ -1260,6 +1260,19 @@ impl Request { match value.fast_get(global_this, bun_jsc::BuiltinName::Body) { Ok(Some(body_)) => { fields.insert(Fields::Body); + // fetch spec Request(init): `keepalive: true` with a ReadableStream + // body throws before body extraction (Node's message is "keepalive"). + if crate::webcore::ReadableStream::is_readable_stream(body_) { + match value.get(global_this, "keepalive") { + Ok(Some(keepalive)) if keepalive.to_boolean() => { + bail!(Err( + global_this.throw_type_error(format_args!("keepalive")) + )); + } + Ok(_) => {} + Err(e) => bail!(Err(e)), + } + } match BodyValue::from_js(global_this, body_) { Ok(v) => { *req.body_value_mut() = v; diff --git a/test/js/bun/http/async-iterator-stream.test.ts b/test/js/bun/http/async-iterator-stream.test.ts index 071bff47118d..0bb7a4b923eb 100644 --- a/test/js/bun/http/async-iterator-stream.test.ts +++ b/test/js/bun/http/async-iterator-stream.test.ts @@ -27,6 +27,66 @@ describe.concurrent("Streaming body via", () => { expect(chunks).toHaveLength(2); }); + test("a hand-written async iterator without return() completes", async () => { + // https://github.com/oven-sh/bun/pull/33193: the native converter crashed here. + let i = 0; + const text = await new Response({ + [Symbol.asyncIterator]: () => ({ + next: () => Promise.resolve(i++ === 0 ? { value: "a", done: false } : { done: true }), + }), + }).text(); + expect(text).toBe("a"); + }); + + // An erroring async-iterable body also emits an internal unhandled rejection (pre-existing, + // matches the previous implementation), so these two assert in a subprocess. + test("an iterator whose next() rejects and has no throw() rejects the body", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `await new Response({ [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(new Error("nrej")), return: () => Promise.resolve({ done: true }) }) }).text().then(() => console.log("resolved"), e => console.log("rejected", e.constructor.name, e.message)); process.exit(0);`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout.trim()).toBe("rejected Error nrej"); + expect(exitCode).toBe(0); + }); + + test("an iterator returning thenables (non-native promises) streams", async () => { + let n = 0; + const iterator = { + next() { + const i = n++; + return { + then(resolve: (v: any) => void) { + queueMicrotask(() => resolve(i < 3 ? { value: "t" + i, done: false } : { done: true })); + }, + }; + }, + return: () => Promise.resolve({ done: true }), + }; + const text = await new Response({ [Symbol.asyncIterator]: () => iterator }).text(); + expect(text).toBe("t0t1t2"); + }); + + test("a non-object iteration result rejects with a TypeError", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `await new Response({ [Symbol.asyncIterator]: () => ({ next: async () => undefined }) }).text().then(() => console.log("resolved"), e => console.log("rejected", e.constructor.name)); process.exit(0);`, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout.trim()).toBe("rejected TypeError"); + expect(exitCode).toBe(0); + }); + test("async generator function throws an error but continues to send the headers", async () => { const onMessage = mock(async url => { const response = await fetch(url); diff --git a/test/js/bun/http/serve-body-leak.test.ts b/test/js/bun/http/serve-body-leak.test.ts index 2f0f5cee6a54..9bf389b8cd7d 100644 --- a/test/js/bun/http/serve-body-leak.test.ts +++ b/test/js/bun/http/serve-body-leak.test.ts @@ -197,3 +197,65 @@ for (const test_info of [ isDebug ? 60_000 : 40_000, ); } + +// A client disconnecting while a direct response stream is suspended inside pull() must not +// leak the native response sink (nothing else can ever free it once the request context is +// recycled). On ASAN builds LeakSanitizer reports it as a direct leak at exit; the assertion +// compares leaked bytes between a small and a large run so unrelated one-time at-exit +// allocations cannot mask or fake the signal. https://github.com/oven-sh/bun/pull/33193 +it("aborting direct-stream responses parked in pull() does not leak the native sink", async () => { + const runAborts = async (count: number) => { + const script = ` + const parked = []; + const server = Bun.serve({ + port: 0, + idleTimeout: 0, + async fetch() { + return new Response( + new ReadableStream({ + type: "direct", + async pull(c) { + c.write("part1"); + await c.flush(); + await new Promise(resolve => parked.push(resolve)); + }, + }), + { headers: { "Content-Length": "100000" } }, + ); + }, + }); + for (let i = 0; i < ${count}; i++) { + const ac = new AbortController(); + const res = await fetch(server.url, { signal: ac.signal }); + const reader = res.body.getReader(); + await reader.read(); + ac.abort(); + await reader.closed.catch(() => {}); + } + // The aborted requests' pull() calls stay suspended: nothing may rely on them resuming. + server.stop(true); + Bun.gc(true); + await Bun.sleep(20); + Bun.gc(true); + console.log("done"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: { + ...bunEnv, + // On ASAN builds, make the subprocess report leaks at exit (inert elsewhere). + ASAN_OPTIONS: "detect_leaks=1", + LSAN_OPTIONS: `suppressions=${join(import.meta.dirname, "../../../leaksan.supp")}`, + }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stdout.trim()).toBe("done"); + const leaked = /SUMMARY: AddressSanitizer: (\d+) byte\(s\) leaked/.exec(stderr); + return leaked ? Number(leaked[1]) : 0; + }; + const [small, large] = [await runAborts(2), await runAborts(22)]; + // 20 extra aborted requests leaked ~176 bytes each before the fix. + expect(large - small).toBeLessThan(1000); +}); diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index 93fe6b856105..7bfbab7b8012 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -586,9 +586,10 @@ describe("streaming", () => { controller.close(); }, }); - // Lock the stream before handing it to the response. A locked stream - // cannot be piped, so the server must surface ERR_STREAM_CANNOT_PIPE - // instead of silently returning a 200 with an empty body. + // Lock the stream before handing it to the response. Constructing a + // Response from a locked stream throws a TypeError (fetch spec; Node + // agrees), which must reach the error handler instead of silently + // returning a 200 with an empty body. stream.getReader(); return new Response(stream); }, @@ -602,9 +603,9 @@ describe("streaming", () => { expect(await response.text()).toBe("handled"); expect(response.status).toBe(500); expect(captured).toEqual({ - code: "ERR_STREAM_CANNOT_PIPE", - name: "Error", - message: "Stream already used, please create a new one", + code: undefined, + name: "TypeError", + message: "Body object should not be disturbed or locked", }); }); }); diff --git a/test/js/bun/util/readablestreamtoarraybuffer.test.ts b/test/js/bun/util/readablestreamtoarraybuffer.test.ts index 7b15222bc3c3..00bbe238c2e7 100644 --- a/test/js/bun/util/readablestreamtoarraybuffer.test.ts +++ b/test/js/bun/util/readablestreamtoarraybuffer.test.ts @@ -1,29 +1,56 @@ import { expect, test } from "bun:test"; -test("readableStreamToArrayBuffer works", async () => { - // the test calls InternalPromise.then. this test ensures that such function is not user-overridable. - let _then = Promise.prototype.then; +// The consumer's own promise plumbing must never route through user-patched +// Promise.prototype.then. (A thenable returned by the user's own start() is +// adopted through it, matching the spec and Node.) +test("readableStreamToArrayBuffer does not call a patched Promise.prototype.then", async () => { + const originalThen = Promise.prototype.then; let counter = 0; // @ts-ignore - Promise.prototype.then = (...args) => { + Promise.prototype.then = function (...args) { counter++; - return _then.apply(this, args); + return originalThen.apply(this, args); }; try { const result = await Bun.readableStreamToArrayBuffer( new ReadableStream({ - async start(controller) { + start(controller) { controller.enqueue(new TextEncoder().encode("bun is")); controller.enqueue(new TextEncoder().encode(" awesome!")); controller.close(); }, }), ); + expect(new TextDecoder().decode(result)).toBe("bun is awesome!"); expect(counter).toBe(0); + } finally { + Promise.prototype.then = originalThen; + } +}); + +test("an async start() promise is adopted observably, like Node", async () => { + const originalThen = Promise.prototype.then; + let counter = 0; + // @ts-ignore + Promise.prototype.then = function (...args) { + counter++; + return originalThen.apply(this, args); + }; + try { + const result = await Bun.readableStreamToArrayBuffer( + new ReadableStream({ + async start(controller) { + controller.enqueue(new TextEncoder().encode("bun is")); + controller.enqueue(new TextEncoder().encode(" awesome!")); + controller.close(); + }, + }), + ); expect(new TextDecoder().decode(result)).toBe("bun is awesome!"); - } catch (error) { - throw error; + // Web IDL "a promise resolved with startResult" adopts the user's promise: + // one observable then() call, exactly as in Node. + expect(counter).toBe(1); } finally { - Promise.prototype.then = _then; + Promise.prototype.then = originalThen; } }); diff --git a/test/js/node/process/process-stdin.test.ts b/test/js/node/process/process-stdin.test.ts index e783e49bfdd5..35364e53d1f4 100644 --- a/test/js/node/process/process-stdin.test.ts +++ b/test/js/node/process/process-stdin.test.ts @@ -104,9 +104,11 @@ test("stdin with 'data' event handler should NOT receive data when paused", asyn const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]); - expect(await proc.stdout.text()).toMatchInlineSnapshot(`""`); + expect(stdout).toMatchInlineSnapshot(`""`); expect(await proc.stderr.text()).toMatchInlineSnapshot(`""`); - expect(proc.exitCode).toBe(1); + // Reusing the already-consumed stdout stream now rejects (the stream is disturbed). + await expect(proc.stdout.text()).rejects.toThrow("ReadableStream has already been used"); + expect(exitCode).toBe(1); }); // Drains the child; its stderr joins the comparison only when it failed, so a @@ -301,3 +303,32 @@ test("stdin should not allow process to exit when not paused", async () => { expect(await proc.stdout.text()).toMatchInlineSnapshot(`""`); expect(await proc.stderr.text()).toMatchInlineSnapshot(`""`); }); + +test("pause() and resume() churn while data is in flight never destroys stdin", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + let total = 0; + process.stdin.on("data", d => { total += d.length; }); + process.stdin.on("error", err => { console.log("ERROR " + (err?.code || err?.message)); process.exit(1); }); + process.stdin.on("end", () => { console.log("TOTAL " + total); }); + const churn = setInterval(() => { process.stdin.pause(); process.stdin.resume(); }, 5); + churn.unref(); + `, + ], + stdin: "pipe", + stdout: "pipe", + stderr: "pipe", + env: bunEnv, + }); + for (let i = 0; i < 20; i++) { + proc.stdin.write("x".repeat(1024)); + await Bun.sleep(10); + } + await proc.stdin.end(); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + expect(stdout.trim()).toBe(`TOTAL ${20 * 1024}`); + expect(exitCode).toBe(0); +}); diff --git a/test/js/third_party/wpt-h2/run.test.ts b/test/js/third_party/wpt-h2/run.test.ts index a583cc6857e7..d9a91b3e9946 100644 --- a/test/js/third_party/wpt-h2/run.test.ts +++ b/test/js/third_party/wpt-h2/run.test.ts @@ -1,18 +1,43 @@ // Runs the vendored WPT fetch .h2.any.js tests against Bun's fetch() over // the experimental HTTP/2 client path. The .any.js files are byte-identical -// to upstream; this driver supplies the testharness globals, a wptserve -// stand-in, and a fetch() wrapper that forces ALPN h2. +// to upstream; this driver supplies the testharness globals (via the shared +// ../wpt-testharness-shim.ts), a wptserve stand-in, and a fetch() wrapper +// that forces ALPN h2. // // Vendored from web-platform-tests/wpt @ ebf8e3069ec4ac6498826bf9066419e46b0f4ac5 // fetch/api/basic/status.h2.any.js // fetch/api/basic/request-upload.h2.any.js // fetch/api/redirect/redirect-upload.h2.any.js -import { afterAll } from "bun:test"; +import { afterAll, test as bunTest } from "bun:test"; import { readFileSync } from "node:fs"; import { join } from "node:path"; +import { setRegistrar, wptTest } from "../wpt-testharness-shim"; import { startServer } from "./server"; -import { wptTest } from "./testharness-shim"; + +// WPT subtests that do not pass on the current implementation. Tests whose +// names appear here are registered via test.todo so the suite stays green +// while still surfacing the gap. +export const knownFailures = new Set([ + // Bun's Request constructor doesn't read RequestInit.duplex (general fetch + // spec gap, not h2-specific). + "Synchronous feature detect", + // Spec requires TypeError when a streamed chunk is not a BufferSource; Bun + // currently coerces strings and treats null as empty. + "Streaming upload with body containing a String", + "Streaming upload with body containing null", + // Spec requires TypeError on a 401 challenge with a non-replayable body; + // Bun returns the 401 response instead. + "Streaming upload should fail on a 401 response", +]); + +setRegistrar((name, run) => { + if (knownFailures.has(name)) { + bunTest.todo(name); + return; + } + bunTest(name, run); +}); const { origin, close } = await startServer(); afterAll(close); @@ -20,6 +45,7 @@ afterAll(close); const g = globalThis as any; g.RESOURCES_DIR = origin + "/fetch/api/resources/"; g.self = { origin }; +g.token = () => crypto.randomUUID(); const realFetch = globalThis.fetch; const realRequest = globalThis.Request; diff --git a/test/js/third_party/wpt-h2/testharness-shim.ts b/test/js/third_party/wpt-h2/testharness-shim.ts deleted file mode 100644 index 6ef5a29d7e45..000000000000 --- a/test/js/third_party/wpt-h2/testharness-shim.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Minimal WPT testharness.js shim mapped onto bun:test. Only the surface -// the vendored .h2.any.js files touch is implemented. Tests whose names -// appear in `knownFailures` are registered via test.todo so the suite -// stays green while still surfacing the gap. - -import { test as bunTest, expect } from "bun:test"; - -export const knownFailures = new Set([ - // Bun's Request constructor doesn't read RequestInit.duplex (general fetch - // spec gap, not h2-specific). - "Synchronous feature detect", - // Spec requires TypeError when a streamed chunk is not a BufferSource; Bun - // currently coerces strings and treats null as empty. - "Streaming upload with body containing a String", - "Streaming upload with body containing null", - // Spec requires TypeError on a 401 challenge with a non-replayable body; - // Bun returns the 401 response instead. - "Streaming upload should fail on a 401 response", -]); - -function register(name: string, body: () => unknown | Promise) { - if (knownFailures.has(name)) { - bunTest.todo(name); - return; - } - bunTest(name, async () => { - await body(); - }); -} - -const g = globalThis as any; - -g.promise_test = (fn: (t: unknown) => Promise, name: string) => { - register(name, () => fn({})); -}; - -// Exported (not installed on globalThis) because bun:test injects its own -// `test` binding into every module it loads, including dynamic imports, and -// that per-module binding shadows globalThis. run.test.ts feeds this in as -// a Function-constructor parameter instead. -export const wptTest = (fn: (t: unknown) => unknown, name: string) => { - register(name, () => fn({})); -}; - -g.assert_equals = (actual: unknown, expected: unknown, msg?: string) => { - if (!Object.is(actual, expected)) { - throw new Error(`assert_equals: ${msg ?? ""} expected ${String(expected)} got ${String(actual)}`); - } -}; - -g.assert_true = (actual: unknown, msg?: string) => { - if (actual !== true) throw new Error(`assert_true: ${msg ?? ""} got ${String(actual)}`); -}; - -g.promise_rejects_js = async (_t: unknown, ctor: new (...a: any[]) => Error, promise: Promise) => { - try { - await promise; - } catch (e) { - expect(e).toBeInstanceOf(ctor); - return; - } - throw new Error(`promise_rejects_js: expected rejection with ${ctor.name}, but promise fulfilled`); -}; - -g.promise_rejects_exactly = async (_t: unknown, expected: unknown, promise: Promise) => { - try { - await promise; - } catch (e) { - if (e !== expected) { - throw new Error(`promise_rejects_exactly: expected ${String(expected)}, got ${String(e)}`); - } - return; - } - throw new Error(`promise_rejects_exactly: expected rejection, but promise fulfilled`); -}; - -g.token = () => crypto.randomUUID(); diff --git a/test/js/third_party/wpt-streams/RESULTS.md b/test/js/third_party/wpt-streams/RESULTS.md new file mode 100644 index 000000000000..d71a547f262c --- /dev/null +++ b/test/js/third_party/wpt-streams/RESULTS.md @@ -0,0 +1,73 @@ +# WPT streams conformance results (current implementation) + +Vendored from `web-platform-tests/wpt @ 1cfa3004f4ac74aa007591529aba9e9246b1f1bf` +(see `UPSTREAM.md` for the file list and exclusions). 68 `.any.js` files copied +byte-for-byte plus the `streams/resources/*.js` helpers and `common/gc.js`; +`../wpt-testharness-shim.ts` supplies the `promise_test`/`assert_*`/`t.*` surface on +top of `bun:test` and `wpt-streams.test.ts` drives every file, resolving its +`// META: script=` includes. + +Recorded against the **C++ Web Streams implementation** (the rewrite that replaced +the JS-builtin implementation). Every WPT subtest that does not pass is listed in +`expectations.json` and registered as `test.failing` (its body still runs, so a +subtest that starts passing turns the suite red — the graduation signal). Everything +else must pass, so the suite is green in CI and any regression in the passing set is +caught. + +```sh +# run the suite +bun bd test test/js/third_party/wpt-streams/wpt-streams.test.ts + +# re-record the expectations (see the header of wpt-streams.test.ts) +WPT_STREAMS_RECORD=/tmp/wpt-streams-journal.jsonl bun bd test test/js/third_party/wpt-streams/wpt-streams.test.ts +``` + +Statuses: `FAIL` = assertion failed; `TIMEOUT` = the subtest never settled within +the shim's per-subtest budget (`SUBTEST_TIMEOUT_MS`); `CRASH` = the subtest aborts +the whole process and is therefore never executed, in either mode. + +## Totals (debug build, linux-x64, 2026-07-03) + +| | subtests | pass | fail | timeout | crash | pass % | +|---|---|---|---|---|---|---| +| **total** | **1402** | **1402** | 0 | 0 | 0 | **100%** | +| idlharness (WebIDL surface) | 228 | 228 | 0 | 0 | 0 | 100% | +| piping | 229 | 229 | 0 | 0 | 0 | 100% | +| queuing-strategies (top level) | 20 | 20 | 0 | 0 | 0 | 100% | +| readable-byte-streams | 248 | 248 | 0 | 0 | 0 | 100% | +| readable-streams | 348 | 348 | 0 | 0 | 0 | 100% | +| transform-streams | 133 | 133 | 0 | 0 | 0 | 100% | +| writable-streams | 196 | 196 | 0 | 0 | 0 | 100% | + +`expectations.json` is empty: every subtest passes, none are marked expected-fail. + +`idlharness.any.js` (the WebIDL surface-shape harness: interface-object descriptors, +prototype layout, method `length`/`name`, `@@toStringTag`, brand checks) runs with the +vendored `resources/idlharness.js` + `resources/webidl2/lib/webidl2.js` + +`interfaces/{streams,dom}.idl` from the same WPT commit. It is executed through a +registrar with upstream testharness semantics (its member subtests are registered +dynamically from inside its own setup `promise_test`, and its `test()` bodies rely on +running synchronously at registration), and every collected subtest is adjudicated +against `expectations.json` individually. + +For comparison, the pre-rewrite implementation recorded with the same harness on the +same machine one day earlier: **971/1174 (82.7%)**, with 191 assertion failures, 10 +timeouts, and 2 process-aborting crashes (`readable-byte-streams/respond-after-enqueue`, +a JSC assertion). Relative to that baseline the rewrite graduates 202 subtests and +regresses none; the crashes and timeouts are gone. + +## Note on `templated.any.js` "canceling via the reader" (formerly expected-fail) + +For `reader.cancel()` followed by `reader.read(view)`, the WHATWG algorithm +(`ReadableByteStreamControllerPullInto`, closed branch), the reference +implementation, Node, Deno, and Bun all resolve with `{ value: , done: true }`. The WPT subtest asserts +`assert_object_equals(r, { value: undefined, done: true })` and passes in every +browser because upstream `testharness.js`'s `assert_object_equals` recurses into +`actual[p]` whenever it is a non-null object: an empty typed array has no +enumerable own properties, so the comparison against `undefined` is vacuous. +This suite's shim was stricter than upstream (it compared the property with +`assert_equals`), which made Bun the only implementation "failing" the subtest. +The shim now ports upstream's semantics byte-for-byte (a non-empty wrong value +still fails), and the subtest passes here for the same reason it passes +everywhere else. diff --git a/test/js/third_party/wpt-streams/UPSTREAM.md b/test/js/third_party/wpt-streams/UPSTREAM.md new file mode 100644 index 000000000000..77a64b5edb3f --- /dev/null +++ b/test/js/third_party/wpt-streams/UPSTREAM.md @@ -0,0 +1,46 @@ +# Vendored WPT streams suite + +Vendored byte-for-byte from `web-platform-tests/wpt`: + +- **Commit:** `1cfa3004f4ac74aa007591529aba9e9246b1f1bf` +- **Fetched:** 2026-07-01 +- **Source directories:** `streams/`, plus `common/gc.js` + +To re-vendor, pin the same (or a newer, reviewed) commit before copying any files: + +```sh +git -c advice.detachedHead=false clone --depth=1 --filter=blob:none --sparse \ + https://github.com/web-platform-tests/wpt /tmp/wpt +git -C /tmp/wpt sparse-checkout set streams common +git -C /tmp/wpt checkout 1cfa3004f4ac74aa007591529aba9e9246b1f1bf +``` + +## What is vendored + +- `streams/**/*.any.js` (68 files) — every `.any.js` test, preserving the + upstream directory layout (`readable-streams/`, `readable-byte-streams/`, + `writable-streams/`, `transform-streams/`, `piping/`, + `queuing-strategies.any.js`, and the `crashtests/*.any.js`). +- `streams/resources/*.js` — the shared helpers the tests include via + `// META: script=` (`rs-utils.js`, `test-utils.js`, `recording-streams.js`, + `rs-test-templates.js`). +- `common/gc.js` — provides `garbageCollect()`; included by the + garbage-collection tests via `// META: script=/common/gc.js`. +- `resources/idlharness.js`, `resources/webidl2/lib/webidl2.js`, + `interfaces/streams.idl`, `interfaces/dom.idl` — the WebIDL harness, parser, and + IDL definitions `streams/idlharness.any.js` needs. The runner resolves the + `// META: script=/resources/WebIDLParser.js` server alias to the webidl2 bundle + and serves `/interfaces/.idl` fetches from the vendored files + (`fetch_spec` in `wpt-streams.test.ts`). + +Vendored file contents must never be modified. All adaptation lives in +`../wpt-testharness-shim.ts` / `wpt-streams.test.ts`. + +## What is excluded (and why) + +| Path | Reason | +| --- | --- | +| `streams/transferable/**` | Requires `postMessage` stream transfer (windows/workers/service workers); Bun does not support transferable streams — out of scope by design | +| `streams/readable-streams/owning-type*.tentative.any.js` (3 files) | `.tentative` — the `type: 'owning'` proposal is not part of the standard; two also need `MessageChannel` transfer / `VideoFrame` | +| `streams/*/*.window.js`, `streams/**/*.html` | Require a browser `Window`/`Document`/dedicated worker (`queuing-strategies-size-function-per-global.window.js`, `read-task-handling.window.js`, `cross-realm-crash.window.js`, `invalid-realm.tentative.window.js`, the html crashtests, `global.html`) | +| `streams/**/WEB_FEATURES.yml`, `META.yml`, `README.md` | WPT metadata, not tests | diff --git a/test/js/third_party/wpt-streams/common/gc.js b/test/js/third_party/wpt-streams/common/gc.js new file mode 100644 index 000000000000..ac43a4cfaf77 --- /dev/null +++ b/test/js/third_party/wpt-streams/common/gc.js @@ -0,0 +1,52 @@ +/** + * Does a best-effort attempt at invoking garbage collection. Attempts to use + * the standardized `TestUtils.gc()` function, but falls back to other + * environment-specific nonstandard functions, with a final result of just + * creating a lot of garbage (in which case you will get a console warning). + * + * This should generally only be used to attempt to trigger bugs and crashes + * inside tests, i.e. cases where if garbage collection happened, then this + * should not trigger some misbehavior. You cannot rely on garbage collection + * successfully trigger, or that any particular unreachable object will be + * collected. + * + * @returns {Promise} A promise you should await to ensure garbage + * collection has had a chance to complete. + */ +self.garbageCollect = async () => { + // https://testutils.spec.whatwg.org/#the-testutils-namespace + if (self.TestUtils?.gc) { + return TestUtils.gc(); + } + + // Use --expose_gc for V8 (and Node.js) + // to pass this flag at chrome launch use: --js-flags="--expose-gc" + // Exposed in SpiderMonkey shell as well + if (self.gc) { + return self.gc(); + } + + // Present in some WebKit development environments + if (self.GCController) { + return GCController.collect(); + } + + console.warn( + 'Tests are running without the ability to do manual garbage collection. ' + + 'They will still work, but coverage will be suboptimal.'); + + for (var i = 0; i < 1000; i++) { + gcRec(10); + } + + function gcRec(n) { + if (n < 1) { + return {}; + } + + let temp = { i: "ab" + i + i / 100000 }; + temp += "foo"; + + gcRec(n - 1); + } +}; diff --git a/test/js/third_party/wpt-streams/expectations.json b/test/js/third_party/wpt-streams/expectations.json new file mode 100644 index 000000000000..a7784d14c623 --- /dev/null +++ b/test/js/third_party/wpt-streams/expectations.json @@ -0,0 +1,3 @@ +{ + "failures": {} +} diff --git a/test/js/third_party/wpt-streams/interfaces/dom.idl b/test/js/third_party/wpt-streams/interfaces/dom.idl new file mode 100644 index 000000000000..1ddc084b949d --- /dev/null +++ b/test/js/third_party/wpt-streams/interfaces/dom.idl @@ -0,0 +1,663 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: DOM Standard (https://dom.spec.whatwg.org/) + +[Exposed=*] +interface Event { + constructor(DOMString type, optional EventInit eventInitDict = {}); + + readonly attribute DOMString type; + readonly attribute EventTarget? target; + readonly attribute EventTarget? srcElement; // legacy + readonly attribute EventTarget? currentTarget; + sequence composedPath(); + + const unsigned short NONE = 0; + const unsigned short CAPTURING_PHASE = 1; + const unsigned short AT_TARGET = 2; + const unsigned short BUBBLING_PHASE = 3; + readonly attribute unsigned short eventPhase; + + undefined stopPropagation(); + attribute boolean cancelBubble; // legacy alias of .stopPropagation() + undefined stopImmediatePropagation(); + + readonly attribute boolean bubbles; + readonly attribute boolean cancelable; + attribute boolean returnValue; // legacy + undefined preventDefault(); + readonly attribute boolean defaultPrevented; + readonly attribute boolean composed; + + [LegacyUnforgeable] readonly attribute boolean isTrusted; + readonly attribute DOMHighResTimeStamp timeStamp; + + undefined initEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false); // legacy +}; + +dictionary EventInit { + boolean bubbles = false; + boolean cancelable = false; + boolean composed = false; +}; + +partial interface Window { + [Replaceable] readonly attribute (Event or undefined) event; // legacy +}; + +[Exposed=*] +interface CustomEvent : Event { + constructor(DOMString type, optional CustomEventInit eventInitDict = {}); + + readonly attribute any detail; + + undefined initCustomEvent(DOMString type, optional boolean bubbles = false, optional boolean cancelable = false, optional any detail = null); // legacy +}; + +dictionary CustomEventInit : EventInit { + any detail = null; +}; + +[Exposed=*] +interface EventTarget { + constructor(); + + undefined addEventListener(DOMString type, EventListener? callback, optional (AddEventListenerOptions or boolean) options = {}); + undefined removeEventListener(DOMString type, EventListener? callback, optional (EventListenerOptions or boolean) options = {}); + boolean dispatchEvent(Event event); +}; + +callback interface EventListener { + undefined handleEvent(Event event); +}; + +dictionary EventListenerOptions { + boolean capture = false; +}; + +dictionary AddEventListenerOptions : EventListenerOptions { + boolean passive; + boolean once = false; + AbortSignal signal; +}; + +[Exposed=*] +interface AbortController { + constructor(); + + [SameObject] readonly attribute AbortSignal signal; + + undefined abort(optional any reason); +}; + +[Exposed=*] +interface AbortSignal : EventTarget { + [NewObject] static AbortSignal abort(optional any reason); + [Exposed=(Window,Worker), NewObject] static AbortSignal timeout([EnforceRange] unsigned long long milliseconds); + [NewObject] static AbortSignal _any(sequence signals); + + readonly attribute boolean aborted; + readonly attribute any reason; + undefined throwIfAborted(); + + attribute EventHandler onabort; +}; +interface mixin NonElementParentNode { + Element? getElementById(DOMString elementId); +}; +Document includes NonElementParentNode; +DocumentFragment includes NonElementParentNode; + +interface mixin DocumentOrShadowRoot { + readonly attribute CustomElementRegistry? customElementRegistry; +}; +Document includes DocumentOrShadowRoot; +ShadowRoot includes DocumentOrShadowRoot; + +interface mixin ParentNode { + [SameObject] readonly attribute HTMLCollection children; + readonly attribute Element? firstElementChild; + readonly attribute Element? lastElementChild; + readonly attribute unsigned long childElementCount; + + [CEReactions, Unscopable] undefined prepend((Node or DOMString)... nodes); + [CEReactions, Unscopable] undefined append((Node or DOMString)... nodes); + [CEReactions, Unscopable] undefined replaceChildren((Node or DOMString)... nodes); + + [CEReactions] undefined moveBefore(Node node, Node? child); + + Element? querySelector(DOMString selectors); + [NewObject] NodeList querySelectorAll(DOMString selectors); +}; +Document includes ParentNode; +DocumentFragment includes ParentNode; +Element includes ParentNode; + +interface mixin NonDocumentTypeChildNode { + readonly attribute Element? previousElementSibling; + readonly attribute Element? nextElementSibling; +}; +Element includes NonDocumentTypeChildNode; +CharacterData includes NonDocumentTypeChildNode; + +interface mixin ChildNode { + [CEReactions, Unscopable] undefined before((Node or DOMString)... nodes); + [CEReactions, Unscopable] undefined after((Node or DOMString)... nodes); + [CEReactions, Unscopable] undefined replaceWith((Node or DOMString)... nodes); + [CEReactions, Unscopable] undefined remove(); +}; +DocumentType includes ChildNode; +Element includes ChildNode; +CharacterData includes ChildNode; + +interface mixin Slottable { + readonly attribute HTMLSlotElement? assignedSlot; +}; +Element includes Slottable; +Text includes Slottable; + +[Exposed=Window] +interface NodeList { + getter Node? item(unsigned long index); + readonly attribute unsigned long length; + iterable; +}; + +[Exposed=Window, LegacyUnenumerableNamedProperties] +interface HTMLCollection { + readonly attribute unsigned long length; + getter Element? item(unsigned long index); + getter Element? namedItem(DOMString name); +}; + +[Exposed=Window] +interface MutationObserver { + constructor(MutationCallback callback); + + undefined observe(Node target, optional MutationObserverInit options = {}); + undefined disconnect(); + sequence takeRecords(); +}; + +callback MutationCallback = undefined (sequence mutations, MutationObserver observer); + +dictionary MutationObserverInit { + boolean childList = false; + boolean attributes; + boolean characterData; + boolean subtree = false; + boolean attributeOldValue; + boolean characterDataOldValue; + sequence attributeFilter; +}; + +[Exposed=Window] +interface MutationRecord { + readonly attribute DOMString type; + [SameObject] readonly attribute Node target; + [SameObject] readonly attribute NodeList addedNodes; + [SameObject] readonly attribute NodeList removedNodes; + readonly attribute Node? previousSibling; + readonly attribute Node? nextSibling; + readonly attribute DOMString? attributeName; + readonly attribute DOMString? attributeNamespace; + readonly attribute DOMString? oldValue; +}; + +[Exposed=Window] +interface Node : EventTarget { + const unsigned short ELEMENT_NODE = 1; + const unsigned short ATTRIBUTE_NODE = 2; + const unsigned short TEXT_NODE = 3; + const unsigned short CDATA_SECTION_NODE = 4; + const unsigned short ENTITY_REFERENCE_NODE = 5; // legacy + const unsigned short ENTITY_NODE = 6; // legacy + const unsigned short PROCESSING_INSTRUCTION_NODE = 7; + const unsigned short COMMENT_NODE = 8; + const unsigned short DOCUMENT_NODE = 9; + const unsigned short DOCUMENT_TYPE_NODE = 10; + const unsigned short DOCUMENT_FRAGMENT_NODE = 11; + const unsigned short NOTATION_NODE = 12; // legacy + readonly attribute unsigned short nodeType; + readonly attribute DOMString nodeName; + + readonly attribute USVString baseURI; + + readonly attribute boolean isConnected; + readonly attribute Document? ownerDocument; + Node getRootNode(optional GetRootNodeOptions options = {}); + readonly attribute Node? parentNode; + readonly attribute Element? parentElement; + boolean hasChildNodes(); + [SameObject] readonly attribute NodeList childNodes; + readonly attribute Node? firstChild; + readonly attribute Node? lastChild; + readonly attribute Node? previousSibling; + readonly attribute Node? nextSibling; + + [CEReactions] attribute DOMString? nodeValue; + [CEReactions] attribute DOMString? textContent; + [CEReactions] undefined normalize(); + + [CEReactions, NewObject] Node cloneNode(optional boolean subtree = false); + boolean isEqualNode(Node? otherNode); + boolean isSameNode(Node? otherNode); // legacy alias of === + + const unsigned short DOCUMENT_POSITION_DISCONNECTED = 0x01; + const unsigned short DOCUMENT_POSITION_PRECEDING = 0x02; + const unsigned short DOCUMENT_POSITION_FOLLOWING = 0x04; + const unsigned short DOCUMENT_POSITION_CONTAINS = 0x08; + const unsigned short DOCUMENT_POSITION_CONTAINED_BY = 0x10; + const unsigned short DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC = 0x20; + unsigned short compareDocumentPosition(Node other); + boolean contains(Node? other); + + DOMString? lookupPrefix(DOMString? namespace); + DOMString? lookupNamespaceURI(DOMString? prefix); + boolean isDefaultNamespace(DOMString? namespace); + + [CEReactions] Node insertBefore(Node node, Node? child); + [CEReactions] Node appendChild(Node node); + [CEReactions] Node replaceChild(Node node, Node child); + [CEReactions] Node removeChild(Node child); +}; + +dictionary GetRootNodeOptions { + boolean composed = false; +}; + +[Exposed=Window] +interface Document : Node { + constructor(); + + [SameObject] readonly attribute DOMImplementation implementation; + readonly attribute USVString URL; + readonly attribute USVString documentURI; + readonly attribute DOMString compatMode; + readonly attribute DOMString characterSet; + readonly attribute DOMString charset; // legacy alias of .characterSet + readonly attribute DOMString inputEncoding; // legacy alias of .characterSet + readonly attribute DOMString contentType; + + readonly attribute DocumentType? doctype; + readonly attribute Element? documentElement; + HTMLCollection getElementsByTagName(DOMString qualifiedName); + HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName); + HTMLCollection getElementsByClassName(DOMString classNames); + + [CEReactions, NewObject] Element createElement(DOMString localName, optional (DOMString or ElementCreationOptions) options = {}); + [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional (DOMString or ElementCreationOptions) options = {}); + [NewObject] DocumentFragment createDocumentFragment(); + [NewObject] Text createTextNode(DOMString data); + [NewObject] CDATASection createCDATASection(DOMString data); + [NewObject] Comment createComment(DOMString data); + [NewObject] ProcessingInstruction createProcessingInstruction(DOMString target, DOMString data); + + [CEReactions, NewObject] Node importNode(Node node, optional (boolean or ImportNodeOptions) options = false); + [CEReactions] Node adoptNode(Node node); + + [NewObject] Attr createAttribute(DOMString localName); + [NewObject] Attr createAttributeNS(DOMString? namespace, DOMString qualifiedName); + + [NewObject] Event createEvent(DOMString interface); // legacy + + [NewObject] Range createRange(); + + // NodeFilter.SHOW_ALL = 0xFFFFFFFF + [NewObject] NodeIterator createNodeIterator(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null); + [NewObject] TreeWalker createTreeWalker(Node root, optional unsigned long whatToShow = 0xFFFFFFFF, optional NodeFilter? filter = null); +}; + +[Exposed=Window] +interface XMLDocument : Document {}; + +dictionary ElementCreationOptions { + CustomElementRegistry? customElementRegistry; + DOMString is; +}; + +dictionary ImportNodeOptions { + CustomElementRegistry customElementRegistry; + boolean selfOnly = false; +}; + +[Exposed=Window] +interface DOMImplementation { + [NewObject] DocumentType createDocumentType(DOMString name, DOMString publicId, DOMString systemId); + [NewObject] XMLDocument createDocument(DOMString? namespace, [LegacyNullToEmptyString] DOMString qualifiedName, optional DocumentType? doctype = null); + [NewObject] Document createHTMLDocument(optional DOMString title); + + boolean hasFeature(); // useless; always returns true +}; + +[Exposed=Window] +interface DocumentType : Node { + readonly attribute DOMString name; + readonly attribute DOMString publicId; + readonly attribute DOMString systemId; +}; + +[Exposed=Window] +interface DocumentFragment : Node { + constructor(); +}; + +[Exposed=Window] +interface ShadowRoot : DocumentFragment { + readonly attribute ShadowRootMode mode; + readonly attribute boolean delegatesFocus; + readonly attribute SlotAssignmentMode slotAssignment; + readonly attribute boolean clonable; + readonly attribute boolean serializable; + readonly attribute Element host; + + attribute EventHandler onslotchange; +}; + +enum ShadowRootMode { "open", "closed" }; +enum SlotAssignmentMode { "manual", "named" }; + +[Exposed=Window] +interface Element : Node { + readonly attribute DOMString? namespaceURI; + readonly attribute DOMString? prefix; + readonly attribute DOMString localName; + readonly attribute DOMString tagName; + + [CEReactions] attribute DOMString id; + [CEReactions] attribute DOMString className; + [SameObject, PutForwards=value] readonly attribute DOMTokenList classList; + [CEReactions, Unscopable] attribute DOMString slot; + + boolean hasAttributes(); + [SameObject] readonly attribute NamedNodeMap attributes; + sequence getAttributeNames(); + DOMString? getAttribute(DOMString qualifiedName); + DOMString? getAttributeNS(DOMString? namespace, DOMString localName); + [CEReactions] undefined setAttribute(DOMString qualifiedName, (TrustedType or DOMString) value); + [CEReactions] undefined setAttributeNS(DOMString? namespace, DOMString qualifiedName, (TrustedType or DOMString) value); + [CEReactions] undefined removeAttribute(DOMString qualifiedName); + [CEReactions] undefined removeAttributeNS(DOMString? namespace, DOMString localName); + [CEReactions] boolean toggleAttribute(DOMString qualifiedName, optional boolean force); + boolean hasAttribute(DOMString qualifiedName); + boolean hasAttributeNS(DOMString? namespace, DOMString localName); + + Attr? getAttributeNode(DOMString qualifiedName); + Attr? getAttributeNodeNS(DOMString? namespace, DOMString localName); + [CEReactions] Attr? setAttributeNode(Attr attr); + [CEReactions] Attr? setAttributeNodeNS(Attr attr); + [CEReactions] Attr removeAttributeNode(Attr attr); + + ShadowRoot attachShadow(ShadowRootInit init); + readonly attribute ShadowRoot? shadowRoot; + + readonly attribute CustomElementRegistry? customElementRegistry; + + Element? closest(DOMString selectors); + boolean matches(DOMString selectors); + boolean webkitMatchesSelector(DOMString selectors); // legacy alias of .matches + + HTMLCollection getElementsByTagName(DOMString qualifiedName); + HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName); + HTMLCollection getElementsByClassName(DOMString classNames); + + [CEReactions] Element? insertAdjacentElement(DOMString where, Element element); // legacy + undefined insertAdjacentText(DOMString where, DOMString data); // legacy +}; + +dictionary ShadowRootInit { + required ShadowRootMode mode; + boolean delegatesFocus = false; + SlotAssignmentMode slotAssignment = "named"; + boolean clonable = false; + boolean serializable = false; + CustomElementRegistry? customElementRegistry; +}; + +[Exposed=Window, + LegacyUnenumerableNamedProperties] +interface NamedNodeMap { + readonly attribute unsigned long length; + getter Attr? item(unsigned long index); + getter Attr? getNamedItem(DOMString qualifiedName); + Attr? getNamedItemNS(DOMString? namespace, DOMString localName); + [CEReactions] Attr? setNamedItem(Attr attr); + [CEReactions] Attr? setNamedItemNS(Attr attr); + [CEReactions] Attr removeNamedItem(DOMString qualifiedName); + [CEReactions] Attr removeNamedItemNS(DOMString? namespace, DOMString localName); +}; + +[Exposed=Window] +interface Attr : Node { + readonly attribute DOMString? namespaceURI; + readonly attribute DOMString? prefix; + readonly attribute DOMString localName; + readonly attribute DOMString name; + [CEReactions] attribute DOMString value; + + readonly attribute Element? ownerElement; + + readonly attribute boolean specified; // useless; always returns true +}; +[Exposed=Window] +interface CharacterData : Node { + attribute [LegacyNullToEmptyString] DOMString data; + readonly attribute unsigned long length; + DOMString substringData(unsigned long offset, unsigned long count); + undefined appendData(DOMString data); + undefined insertData(unsigned long offset, DOMString data); + undefined deleteData(unsigned long offset, unsigned long count); + undefined replaceData(unsigned long offset, unsigned long count, DOMString data); +}; + +[Exposed=Window] +interface Text : CharacterData { + constructor(optional DOMString data = ""); + + [NewObject] Text splitText(unsigned long offset); + readonly attribute DOMString wholeText; +}; + +[Exposed=Window] +interface CDATASection : Text { +}; +[Exposed=Window] +interface ProcessingInstruction : CharacterData { + readonly attribute DOMString target; +}; +[Exposed=Window] +interface Comment : CharacterData { + constructor(optional DOMString data = ""); +}; + +[Exposed=Window] +interface AbstractRange { + readonly attribute Node startContainer; + readonly attribute unsigned long startOffset; + readonly attribute Node endContainer; + readonly attribute unsigned long endOffset; + readonly attribute boolean collapsed; +}; + +dictionary StaticRangeInit { + required Node startContainer; + required unsigned long startOffset; + required Node endContainer; + required unsigned long endOffset; +}; + +[Exposed=Window] +interface StaticRange : AbstractRange { + constructor(StaticRangeInit init); +}; + +[Exposed=Window] +interface Range : AbstractRange { + constructor(); + + readonly attribute Node commonAncestorContainer; + + undefined setStart(Node node, unsigned long offset); + undefined setEnd(Node node, unsigned long offset); + undefined setStartBefore(Node node); + undefined setStartAfter(Node node); + undefined setEndBefore(Node node); + undefined setEndAfter(Node node); + undefined collapse(optional boolean toStart = false); + undefined selectNode(Node node); + undefined selectNodeContents(Node node); + + const unsigned short START_TO_START = 0; + const unsigned short START_TO_END = 1; + const unsigned short END_TO_END = 2; + const unsigned short END_TO_START = 3; + short compareBoundaryPoints(unsigned short how, Range sourceRange); + + [CEReactions] undefined deleteContents(); + [CEReactions, NewObject] DocumentFragment extractContents(); + [CEReactions, NewObject] DocumentFragment cloneContents(); + [CEReactions] undefined insertNode(Node node); + [CEReactions] undefined surroundContents(Node newParent); + + [NewObject] Range cloneRange(); + undefined detach(); + + boolean isPointInRange(Node node, unsigned long offset); + short comparePoint(Node node, unsigned long offset); + + boolean intersectsNode(Node node); + + stringifier; +}; + +[Exposed=Window] +interface NodeIterator { + [SameObject] readonly attribute Node root; + readonly attribute Node referenceNode; + readonly attribute boolean pointerBeforeReferenceNode; + readonly attribute unsigned long whatToShow; + readonly attribute NodeFilter? filter; + + Node? nextNode(); + Node? previousNode(); + + undefined detach(); +}; + +[Exposed=Window] +interface TreeWalker { + [SameObject] readonly attribute Node root; + readonly attribute unsigned long whatToShow; + readonly attribute NodeFilter? filter; + attribute Node currentNode; + + Node? parentNode(); + Node? firstChild(); + Node? lastChild(); + Node? previousSibling(); + Node? nextSibling(); + Node? previousNode(); + Node? nextNode(); +}; +[Exposed=Window] +callback interface NodeFilter { + // Constants for acceptNode() + const unsigned short FILTER_ACCEPT = 1; + const unsigned short FILTER_REJECT = 2; + const unsigned short FILTER_SKIP = 3; + + // Constants for whatToShow + const unsigned long SHOW_ALL = 0xFFFFFFFF; + const unsigned long SHOW_ELEMENT = 0x1; + const unsigned long SHOW_ATTRIBUTE = 0x2; + const unsigned long SHOW_TEXT = 0x4; + const unsigned long SHOW_CDATA_SECTION = 0x8; + const unsigned long SHOW_ENTITY_REFERENCE = 0x10; // legacy + const unsigned long SHOW_ENTITY = 0x20; // legacy + const unsigned long SHOW_PROCESSING_INSTRUCTION = 0x40; + const unsigned long SHOW_COMMENT = 0x80; + const unsigned long SHOW_DOCUMENT = 0x100; + const unsigned long SHOW_DOCUMENT_TYPE = 0x200; + const unsigned long SHOW_DOCUMENT_FRAGMENT = 0x400; + const unsigned long SHOW_NOTATION = 0x800; // legacy + + unsigned short acceptNode(Node node); +}; + +[Exposed=Window] +interface DOMTokenList { + readonly attribute unsigned long length; + getter DOMString? item(unsigned long index); + boolean contains(DOMString token); + [CEReactions] undefined add(DOMString... tokens); + [CEReactions] undefined remove(DOMString... tokens); + [CEReactions] boolean toggle(DOMString token, optional boolean force); + [CEReactions] boolean replace(DOMString token, DOMString newToken); + boolean supports(DOMString token); + [CEReactions] stringifier attribute DOMString value; + iterable; +}; + +[Exposed=Window] +interface XPathResult { + const unsigned short ANY_TYPE = 0; + const unsigned short NUMBER_TYPE = 1; + const unsigned short STRING_TYPE = 2; + const unsigned short BOOLEAN_TYPE = 3; + const unsigned short UNORDERED_NODE_ITERATOR_TYPE = 4; + const unsigned short ORDERED_NODE_ITERATOR_TYPE = 5; + const unsigned short UNORDERED_NODE_SNAPSHOT_TYPE = 6; + const unsigned short ORDERED_NODE_SNAPSHOT_TYPE = 7; + const unsigned short ANY_UNORDERED_NODE_TYPE = 8; + const unsigned short FIRST_ORDERED_NODE_TYPE = 9; + + readonly attribute unsigned short resultType; + readonly attribute unrestricted double numberValue; + readonly attribute DOMString stringValue; + readonly attribute boolean booleanValue; + readonly attribute Node? singleNodeValue; + readonly attribute boolean invalidIteratorState; + readonly attribute unsigned long snapshotLength; + + Node? iterateNext(); + Node? snapshotItem(unsigned long index); +}; + +[Exposed=Window] +interface XPathExpression { + // XPathResult.ANY_TYPE = 0 + XPathResult evaluate(Node contextNode, optional unsigned short type = 0, optional XPathResult? result = null); +}; + +callback interface XPathNSResolver { + DOMString? lookupNamespaceURI(DOMString? prefix); +}; + +interface mixin XPathEvaluatorBase { + [NewObject] XPathExpression createExpression(DOMString expression, optional XPathNSResolver? resolver = null); + Node createNSResolver(Node nodeResolver); // legacy + // XPathResult.ANY_TYPE = 0 + XPathResult evaluate(DOMString expression, Node contextNode, optional XPathNSResolver? resolver = null, optional unsigned short type = 0, optional XPathResult? result = null); +}; +Document includes XPathEvaluatorBase; + +[Exposed=Window] +interface XPathEvaluator { + constructor(); +}; + +XPathEvaluator includes XPathEvaluatorBase; + +[Exposed=Window] +interface XSLTProcessor { + constructor(); + undefined importStylesheet(Node style); + [CEReactions] DocumentFragment transformToFragment(Node source, Document output); + [CEReactions] Document transformToDocument(Node source); + undefined setParameter([LegacyNullToEmptyString] DOMString namespaceURI, DOMString localName, any value); + any getParameter([LegacyNullToEmptyString] DOMString namespaceURI, DOMString localName); + undefined removeParameter([LegacyNullToEmptyString] DOMString namespaceURI, DOMString localName); + undefined clearParameters(); + undefined reset(); +}; diff --git a/test/js/third_party/wpt-streams/interfaces/streams.idl b/test/js/third_party/wpt-streams/interfaces/streams.idl new file mode 100644 index 000000000000..7f7ea73a5740 --- /dev/null +++ b/test/js/third_party/wpt-streams/interfaces/streams.idl @@ -0,0 +1,230 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Streams Standard (https://streams.spec.whatwg.org/) + +[Exposed=*, Transferable] +interface ReadableStream { + constructor(optional object underlyingSource, optional QueuingStrategy strategy = {}); + + static ReadableStream from(any asyncIterable); + + readonly attribute boolean locked; + + Promise cancel(optional any reason); + ReadableStreamReader getReader(optional ReadableStreamGetReaderOptions options = {}); + ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {}); + Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); + sequence tee(); + + async_iterable(optional ReadableStreamIteratorOptions options = {}); +}; + +typedef (ReadableStreamDefaultReader or ReadableStreamBYOBReader) ReadableStreamReader; + +enum ReadableStreamReaderMode { "byob" }; + +dictionary ReadableStreamGetReaderOptions { + ReadableStreamReaderMode mode; +}; + +dictionary ReadableStreamIteratorOptions { + boolean preventCancel = false; +}; + +dictionary ReadableWritablePair { + required ReadableStream readable; + required WritableStream writable; +}; + +dictionary StreamPipeOptions { + boolean preventClose = false; + boolean preventAbort = false; + boolean preventCancel = false; + AbortSignal signal; +}; + +dictionary UnderlyingSource { + UnderlyingSourceStartCallback start; + UnderlyingSourcePullCallback pull; + UnderlyingSourceCancelCallback cancel; + ReadableStreamType type; + [EnforceRange] unsigned long long autoAllocateChunkSize; +}; + +typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; + +callback UnderlyingSourceStartCallback = any (ReadableStreamController controller); +callback UnderlyingSourcePullCallback = Promise (ReadableStreamController controller); +callback UnderlyingSourceCancelCallback = Promise (optional any reason); + +enum ReadableStreamType { "bytes" }; + +interface mixin ReadableStreamGenericReader { + readonly attribute Promise closed; + + Promise cancel(optional any reason); +}; + +[Exposed=*] +interface ReadableStreamDefaultReader { + constructor(ReadableStream stream); + + Promise read(); + undefined releaseLock(); +}; +ReadableStreamDefaultReader includes ReadableStreamGenericReader; + +dictionary ReadableStreamReadResult { + any value; + boolean done; +}; + +[Exposed=*] +interface ReadableStreamBYOBReader { + constructor(ReadableStream stream); + + Promise read(ArrayBufferView view, optional ReadableStreamBYOBReaderReadOptions options = {}); + undefined releaseLock(); +}; +ReadableStreamBYOBReader includes ReadableStreamGenericReader; + +dictionary ReadableStreamBYOBReaderReadOptions { + [EnforceRange] unsigned long long min = 1; +}; + +[Exposed=*] +interface ReadableStreamDefaultController { + readonly attribute unrestricted double? desiredSize; + + undefined close(); + undefined enqueue(optional any chunk); + undefined error(optional any e); +}; + +[Exposed=*] +interface ReadableByteStreamController { + readonly attribute ReadableStreamBYOBRequest? byobRequest; + readonly attribute unrestricted double? desiredSize; + + undefined close(); + undefined enqueue(ArrayBufferView chunk); + undefined error(optional any e); +}; + +[Exposed=*] +interface ReadableStreamBYOBRequest { + readonly attribute Uint8Array? view; + + undefined respond([EnforceRange] unsigned long long bytesWritten); + undefined respondWithNewView(ArrayBufferView view); +}; + +[Exposed=*, Transferable] +interface WritableStream { + constructor(optional object underlyingSink, optional QueuingStrategy strategy = {}); + + readonly attribute boolean locked; + + Promise abort(optional any reason); + Promise close(); + WritableStreamDefaultWriter getWriter(); +}; + +dictionary UnderlyingSink { + UnderlyingSinkStartCallback start; + UnderlyingSinkWriteCallback write; + UnderlyingSinkCloseCallback close; + UnderlyingSinkAbortCallback abort; + any type; +}; + +callback UnderlyingSinkStartCallback = any (WritableStreamDefaultController controller); +callback UnderlyingSinkWriteCallback = Promise (any chunk, WritableStreamDefaultController controller); +callback UnderlyingSinkCloseCallback = Promise (); +callback UnderlyingSinkAbortCallback = Promise (optional any reason); + +[Exposed=*] +interface WritableStreamDefaultWriter { + constructor(WritableStream stream); + + readonly attribute Promise closed; + readonly attribute unrestricted double? desiredSize; + readonly attribute Promise ready; + + Promise abort(optional any reason); + Promise close(); + undefined releaseLock(); + Promise write(optional any chunk); +}; + +[Exposed=*] +interface WritableStreamDefaultController { + readonly attribute AbortSignal signal; + undefined error(optional any e); +}; + +[Exposed=*, Transferable] +interface TransformStream { + constructor(optional object transformer, + optional QueuingStrategy writableStrategy = {}, + optional QueuingStrategy readableStrategy = {}); + + readonly attribute ReadableStream readable; + readonly attribute WritableStream writable; +}; + +dictionary Transformer { + TransformerStartCallback start; + TransformerTransformCallback transform; + TransformerFlushCallback flush; + TransformerCancelCallback cancel; + any readableType; + any writableType; +}; + +callback TransformerStartCallback = any (TransformStreamDefaultController controller); +callback TransformerFlushCallback = Promise (TransformStreamDefaultController controller); +callback TransformerTransformCallback = Promise (any chunk, TransformStreamDefaultController controller); +callback TransformerCancelCallback = Promise (any reason); + +[Exposed=*] +interface TransformStreamDefaultController { + readonly attribute unrestricted double? desiredSize; + + undefined enqueue(optional any chunk); + undefined error(optional any reason); + undefined terminate(); +}; + +dictionary QueuingStrategy { + unrestricted double highWaterMark; + QueuingStrategySize size; +}; + +callback QueuingStrategySize = unrestricted double (any chunk); + +dictionary QueuingStrategyInit { + required unrestricted double highWaterMark; +}; + +[Exposed=*] +interface ByteLengthQueuingStrategy { + constructor(QueuingStrategyInit init); + + readonly attribute unrestricted double highWaterMark; + readonly attribute Function size; +}; + +[Exposed=*] +interface CountQueuingStrategy { + constructor(QueuingStrategyInit init); + + readonly attribute unrestricted double highWaterMark; + readonly attribute Function size; +}; + +interface mixin GenericTransformStream { + readonly attribute ReadableStream readable; + readonly attribute WritableStream writable; +}; diff --git a/test/js/third_party/wpt-streams/resources/idlharness.js b/test/js/third_party/wpt-streams/resources/idlharness.js new file mode 100644 index 000000000000..57cefedc22a1 --- /dev/null +++ b/test/js/third_party/wpt-streams/resources/idlharness.js @@ -0,0 +1,3573 @@ +/* For user documentation see docs/_writing-tests/idlharness.md */ + +/** + * Notes for people who want to edit this file (not just use it as a library): + * + * Most of the interesting stuff happens in the derived classes of IdlObject, + * especially IdlInterface. The entry point for all IdlObjects is .test(), + * which is called by IdlArray.test(). An IdlObject is conceptually just + * "thing we want to run tests on", and an IdlArray is an array of IdlObjects + * with some additional data thrown in. + * + * The object model is based on what WebIDLParser.js produces, which is in turn + * based on its pegjs grammar. If you want to figure out what properties an + * object will have from WebIDLParser.js, the best way is to look at the + * grammar: + * + * https://github.com/darobin/webidl.js/blob/master/lib/grammar.peg + * + * So for instance: + * + * // interface definition + * interface + * = extAttrs:extendedAttributeList? S? "interface" S name:identifier w herit:ifInheritance? w "{" w mem:ifMember* w "}" w ";" w + * { return { type: "interface", name: name, inheritance: herit, members: mem, extAttrs: extAttrs }; } + * + * This means that an "interface" object will have a .type property equal to + * the string "interface", a .name property equal to the identifier that the + * parser found, an .inheritance property equal to either null or the result of + * the "ifInheritance" production found elsewhere in the grammar, and so on. + * After each grammatical production is a JavaScript function in curly braces + * that gets called with suitable arguments and returns some JavaScript value. + * + * (Note that the version of WebIDLParser.js we use might sometimes be + * out-of-date or forked.) + * + * The members and methods of the classes defined by this file are all at least + * briefly documented, hopefully. + */ +(function(){ +"use strict"; +// Support subsetTestByKey from /common/subset-tests-by-key.js, but make it optional +if (!('subsetTestByKey' in self)) { + self.subsetTestByKey = function(key, callback, ...args) { + return callback(...args); + } + self.shouldRunSubTest = () => true; +} +/// Helpers /// +function constValue (cnt) +{ + if (cnt.type === "null") return null; + if (cnt.type === "NaN") return NaN; + if (cnt.type === "Infinity") return cnt.negative ? -Infinity : Infinity; + if (cnt.type === "number") return +cnt.value; + return cnt.value; +} + +function minOverloadLength(overloads) +{ + // "The value of the Function object’s “length” property is + // a Number determined as follows: + // ". . . + // "Return the length of the shortest argument list of the + // entries in S." + if (!overloads.length) { + return 0; + } + + return overloads.map(function(attr) { + return attr.arguments ? attr.arguments.filter(function(arg) { + return !arg.optional && !arg.variadic; + }).length : 0; + }) + .reduce(function(m, n) { return Math.min(m, n); }); +} + +// A helper to get the global of a Function object. This is needed to determine +// which global exceptions the function throws will come from. +function globalOf(func) +{ + try { + // Use the fact that .constructor for a Function object is normally the + // Function constructor, which can be used to mint a new function in the + // right global. + return func.constructor("return this;")(); + } catch (e) { + } + // If the above fails, because someone gave us a non-function, or a function + // with a weird proto chain or weird .constructor property, just fall back + // to 'self'. + return self; +} + +// https://esdiscuss.org/topic/isconstructor#content-11 +function isConstructor(o) { + try { + new (new Proxy(o, {construct: () => ({})})); + return true; + } catch(e) { + return false; + } +} + +function throwOrReject(a_test, operation, fn, obj, args, message, cb) +{ + if (operation.idlType.generic !== "Promise") { + assert_throws_js(globalOf(fn).TypeError, function() { + fn.apply(obj, args); + }, message); + cb(); + } else { + try { + promise_rejects_js(a_test, TypeError, fn.apply(obj, args), message).then(cb, cb); + } catch (e){ + a_test.step(function() { + assert_unreached("Throws \"" + e + "\" instead of rejecting promise"); + cb(); + }); + } + } +} + +function awaitNCallbacks(n, cb, ctx) +{ + var counter = 0; + return function() { + counter++; + if (counter >= n) { + cb(); + } + }; +} + +/// IdlHarnessError /// +// Entry point +self.IdlHarnessError = function(message) +{ + /** + * Message to be printed as the error's toString invocation. + */ + this.message = message; +}; + +IdlHarnessError.prototype = Object.create(Error.prototype); + +IdlHarnessError.prototype.toString = function() +{ + return this.message; +}; + + +/// IdlArray /// +// Entry point +self.IdlArray = function() +{ + /** + * A map from strings to the corresponding named IdlObject, such as + * IdlInterface or IdlException. These are the things that test() will run + * tests on. + */ + this.members = {}; + + /** + * A map from strings to arrays of strings. The keys are interface or + * exception names, and are expected to also exist as keys in this.members + * (otherwise they'll be ignored). This is populated by add_objects() -- + * see documentation at the start of the file. The actual tests will be + * run by calling this.members[name].test_object(obj) for each obj in + * this.objects[name]. obj is a string that will be eval'd to produce a + * JavaScript value, which is supposed to be an object implementing the + * given IdlObject (interface, exception, etc.). + */ + this.objects = {}; + + /** + * When adding multiple collections of IDLs one at a time, an earlier one + * might contain a partial interface or includes statement that depends + * on a later one. Save these up and handle them right before we run + * tests. + * + * Both this.partials and this.includes will be the objects as parsed by + * WebIDLParser.js, not wrapped in IdlInterface or similar. + */ + this.partials = []; + this.includes = []; + + /** + * Record of skipped IDL items, in case we later realize that they are a + * dependency (to retroactively process them). + */ + this.skipped = new Map(); +}; + +IdlArray.prototype.add_idls = function(raw_idls, options) +{ + /** Entry point. See documentation at beginning of file. */ + this.internal_add_idls(WebIDL2.parse(raw_idls), options); +}; + +IdlArray.prototype.add_untested_idls = function(raw_idls, options) +{ + /** Entry point. See documentation at beginning of file. */ + var parsed_idls = WebIDL2.parse(raw_idls); + this.mark_as_untested(parsed_idls); + this.internal_add_idls(parsed_idls, options); +}; + +IdlArray.prototype.mark_as_untested = function (parsed_idls) +{ + for (var i = 0; i < parsed_idls.length; i++) { + parsed_idls[i].untested = true; + if ("members" in parsed_idls[i]) { + for (var j = 0; j < parsed_idls[i].members.length; j++) { + parsed_idls[i].members[j].untested = true; + } + } + } +}; + +IdlArray.prototype.is_excluded_by_options = function (name, options) +{ + return options && + (options.except && options.except.includes(name) + || options.only && !options.only.includes(name)); +}; + +IdlArray.prototype.add_dependency_idls = function(raw_idls, options) +{ + return this.internal_add_dependency_idls(WebIDL2.parse(raw_idls), options); +}; + +IdlArray.prototype.internal_add_dependency_idls = function(parsed_idls, options) +{ + const new_options = { only: [] } + + const all_deps = new Set(); + Object.values(this.members).forEach(v => { + if (v.base) { + all_deps.add(v.base); + } + }); + // Add both 'A' and 'B' for each 'A includes B' entry. + this.includes.forEach(i => { + all_deps.add(i.target); + all_deps.add(i.includes); + }); + this.partials.forEach(p => all_deps.add(p.name)); + // Add 'TypeOfType' for each "typedef TypeOfType MyType;" entry. + Object.entries(this.members).forEach(([k, v]) => { + if (v instanceof IdlTypedef) { + let defs = v.idlType.union + ? v.idlType.idlType.map(t => t.idlType) + : [v.idlType.idlType]; + defs.forEach(d => all_deps.add(d)); + } + }); + + // Add the attribute idlTypes of all the nested members of idls. + const attrDeps = parsedIdls => { + return parsedIdls.reduce((deps, parsed) => { + if (parsed.members) { + for (const attr of Object.values(parsed.members).filter(m => m.type === 'attribute')) { + let attrType = attr.idlType; + // Check for generic members (e.g. FrozenArray) + if (attrType.generic) { + deps.add(attrType.generic); + attrType = attrType.idlType; + } + deps.add(attrType.idlType); + } + } + if (parsed.base in this.members) { + attrDeps([this.members[parsed.base]]).forEach(dep => deps.add(dep)); + } + return deps; + }, new Set()); + }; + + const testedMembers = Object.values(this.members).filter(m => !m.untested && m.members); + attrDeps(testedMembers).forEach(dep => all_deps.add(dep)); + + const testedPartials = this.partials.filter(m => !m.untested && m.members); + attrDeps(testedPartials).forEach(dep => all_deps.add(dep)); + + + if (options && options.except && options.only) { + throw new IdlHarnessError("The only and except options can't be used together."); + } + + const defined_or_untested = name => { + // NOTE: Deps are untested, so we're lenient, and skip re-encountered definitions. + // e.g. for 'idl' containing A:B, B:C, C:D + // array.add_idls(idl, {only: ['A','B']}). + // array.add_dependency_idls(idl); + // B would be encountered as tested, and encountered as a dep, so we ignore. + return name in this.members + || this.is_excluded_by_options(name, options); + } + // Maps name -> [parsed_idl, ...] + const process = function(parsed) { + var deps = []; + if (parsed.name) { + deps.push(parsed.name); + } else if (parsed.type === "includes") { + deps.push(parsed.target); + deps.push(parsed.includes); + } + + deps = deps.filter(function(name) { + if (!name + || name === parsed.name && defined_or_untested(name) + || !all_deps.has(name)) { + // Flag as skipped, if it's not already processed, so we can + // come back to it later if we retrospectively call it a dep. + if (name && !(name in this.members)) { + this.skipped.has(name) + ? this.skipped.get(name).push(parsed) + : this.skipped.set(name, [parsed]); + } + return false; + } + return true; + }.bind(this)); + + deps.forEach(function(name) { + if (!new_options.only.includes(name)) { + new_options.only.push(name); + } + + const follow_up = new Set(); + for (const dep_type of ["inheritance", "includes"]) { + if (parsed[dep_type]) { + const inheriting = parsed[dep_type]; + const inheritor = parsed.name || parsed.target; + const deps = [inheriting]; + // For A includes B, we can ignore A, unless B (or some of its + // members) is being tested. + if (dep_type !== "includes" + || inheriting in this.members && !this.members[inheriting].untested + || this.partials.some(function(p) { + return p.name === inheriting; + })) { + deps.push(inheritor); + } + for (const dep of deps) { + if (!new_options.only.includes(dep)) { + new_options.only.push(dep); + } + all_deps.add(dep); + follow_up.add(dep); + } + } + } + + for (const deferred of follow_up) { + if (this.skipped.has(deferred)) { + const next = this.skipped.get(deferred); + this.skipped.delete(deferred); + next.forEach(process); + } + } + }.bind(this)); + }.bind(this); + + for (let parsed of parsed_idls) { + process(parsed); + } + + this.mark_as_untested(parsed_idls); + + if (new_options.only.length) { + this.internal_add_idls(parsed_idls, new_options); + } +} + +IdlArray.prototype.internal_add_idls = function(parsed_idls, options) +{ + /** + * Internal helper called by add_idls() and add_untested_idls(). + * + * parsed_idls is an array of objects that come from WebIDLParser.js's + * "definitions" production. The add_untested_idls() entry point + * additionally sets an .untested property on each object (and its + * .members) so that they'll be skipped by test() -- they'll only be + * used for base interfaces of tested interfaces, return types, etc. + * + * options is a dictionary that can have an only or except member which are + * arrays. If only is given then only members, partials and interface + * targets listed will be added, and if except is given only those that + * aren't listed will be added. Only one of only and except can be used. + */ + + if (options && options.only && options.except) + { + throw new IdlHarnessError("The only and except options can't be used together."); + } + + var should_skip = name => { + return this.is_excluded_by_options(name, options); + } + + parsed_idls.forEach(function(parsed_idl) + { + var partial_types = [ + "interface", + "interface mixin", + "dictionary", + "namespace", + ]; + if (parsed_idl.partial && partial_types.includes(parsed_idl.type)) + { + if (should_skip(parsed_idl.name)) + { + return; + } + this.partials.push(parsed_idl); + return; + } + + if (parsed_idl.type == "includes") + { + if (should_skip(parsed_idl.target)) + { + return; + } + this.includes.push(parsed_idl); + return; + } + + parsed_idl.array = this; + if (should_skip(parsed_idl.name)) + { + return; + } + if (parsed_idl.name in this.members) + { + throw new IdlHarnessError("Duplicate identifier " + parsed_idl.name); + } + + switch(parsed_idl.type) + { + case "interface": + this.members[parsed_idl.name] = + new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ false); + break; + + case "interface mixin": + this.members[parsed_idl.name] = + new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ true); + break; + + case "dictionary": + // Nothing to test, but we need the dictionary info around for type + // checks + this.members[parsed_idl.name] = new IdlDictionary(parsed_idl); + break; + + case "typedef": + this.members[parsed_idl.name] = new IdlTypedef(parsed_idl); + break; + + case "callback": + this.members[parsed_idl.name] = new IdlCallback(parsed_idl); + break; + + case "enum": + this.members[parsed_idl.name] = new IdlEnum(parsed_idl); + break; + + case "callback interface": + this.members[parsed_idl.name] = + new IdlInterface(parsed_idl, /* is_callback = */ true, /* is_mixin = */ false); + break; + + case "namespace": + this.members[parsed_idl.name] = new IdlNamespace(parsed_idl); + break; + + default: + throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported"; + } + }.bind(this)); +}; + +IdlArray.prototype.add_objects = function(dict) +{ + /** Entry point. See documentation at beginning of file. */ + for (var k in dict) + { + if (k in this.objects) + { + this.objects[k] = this.objects[k].concat(dict[k]); + } + else + { + this.objects[k] = dict[k]; + } + } +}; + +IdlArray.prototype.prevent_multiple_testing = function(name) +{ + /** Entry point. See documentation at beginning of file. */ + this.members[name].prevent_multiple_testing = true; +}; + +IdlArray.prototype.is_json_type = function(type) +{ + /** + * Checks whether type is a JSON type as per + * https://webidl.spec.whatwg.org/#dfn-json-types + */ + + var idlType = type.idlType; + + if (type.generic == "Promise") { return false; } + + // nullable and annotated types don't need to be handled separately, + // as webidl2 doesn't represent them wrapped-up (as they're described + // in WebIDL). + + // union and record types + if (type.union || type.generic == "record") { + return idlType.every(this.is_json_type, this); + } + + // sequence types + if (type.generic == "sequence" || type.generic == "FrozenArray") { + return this.is_json_type(idlType[0]); + } + + if (typeof idlType != "string") { throw new Error("Unexpected type " + JSON.stringify(idlType)); } + + switch (idlType) + { + // Numeric types + case "byte": + case "octet": + case "short": + case "unsigned short": + case "long": + case "unsigned long": + case "long long": + case "unsigned long long": + case "float": + case "double": + case "unrestricted float": + case "unrestricted double": + // boolean + case "boolean": + // string types + case "DOMString": + case "ByteString": + case "USVString": + // object type + case "object": + return true; + case "Error": + case "DOMException": + case "Int8Array": + case "Int16Array": + case "Int32Array": + case "Uint8Array": + case "Uint16Array": + case "Uint32Array": + case "Uint8ClampedArray": + case "BigInt64Array": + case "BigUint64Array": + case "Float16Array": + case "Float32Array": + case "Float64Array": + case "ArrayBuffer": + case "DataView": + case "any": + return false; + default: + var thing = this.members[idlType]; + if (!thing) { throw new Error("Type " + idlType + " not found"); } + if (thing instanceof IdlEnum) { return true; } + + if (thing instanceof IdlTypedef) { + return this.is_json_type(thing.idlType); + } + + // dictionaries where all of their members are JSON types + if (thing instanceof IdlDictionary) { + const map = new Map(); + for (const dict of thing.get_reverse_inheritance_stack()) { + for (const m of dict.members) { + map.set(m.name, m.idlType); + } + } + return Array.from(map.values()).every(this.is_json_type, this); + } + + // interface types that have a toJSON operation declared on themselves or + // one of their inherited interfaces. + if (thing instanceof IdlInterface) { + var base; + while (thing) + { + if (thing.has_to_json_regular_operation()) { return true; } + var mixins = this.includes[thing.name]; + if (mixins) { + mixins = mixins.map(function(id) { + var mixin = this.members[id]; + if (!mixin) { + throw new Error("Interface " + id + " not found (implemented by " + thing.name + ")"); + } + return mixin; + }, this); + if (mixins.some(function(m) { return m.has_to_json_regular_operation() } )) { return true; } + } + if (!thing.base) { return false; } + base = this.members[thing.base]; + if (!base) { + throw new Error("Interface " + thing.base + " not found (inherited by " + thing.name + ")"); + } + thing = base; + } + return false; + } + return false; + } +}; + +function exposure_set(object, default_set) { + var exposed = object.extAttrs && object.extAttrs.filter(a => a.name === "Exposed"); + if (exposed && exposed.length > 1) { + throw new IdlHarnessError( + `Multiple 'Exposed' extended attributes on ${object.name}`); + } + + let result = default_set || ["Window"]; + if (result && !(result instanceof Set)) { + result = new Set(result); + } + if (exposed && exposed.length) { + const { rhs } = exposed[0]; + // Could be a list or a string. + const set = + rhs.type === "*" ? + [ "*" ] : + rhs.type === "identifier-list" ? + rhs.value.map(id => id.value) : + [ rhs.value ]; + result = new Set(set); + } + if (result && result.has("*")) { + return "*"; + } + if (result && result.has("Worker")) { + result.delete("Worker"); + result.add("DedicatedWorker"); + result.add("ServiceWorker"); + result.add("SharedWorker"); + } + return result; +} + +function exposed_in(globals) { + if (globals === "*") { + return true; + } + if ('Window' in self) { + return globals.has("Window"); + } + if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + return globals.has("DedicatedWorker"); + } + if ('SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + return globals.has("SharedWorker"); + } + if ('ServiceWorkerGlobalScope' in self && + self instanceof ServiceWorkerGlobalScope) { + return globals.has("ServiceWorker"); + } + if (Object.getPrototypeOf(self) === Object.prototype) { + // ShadowRealm - only exposed with `"*"`. + return false; + } + throw new IdlHarnessError("Unexpected global object"); +} + +/** + * Asserts that the given error message is thrown for the given function. + * @param {string|IdlHarnessError} error Expected Error message. + * @param {Function} idlArrayFunc Function operating on an IdlArray that should throw. + */ +IdlArray.prototype.assert_throws = function(error, idlArrayFunc) +{ + try { + idlArrayFunc.call(this, this); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + // Assertions for behaviour of the idlharness.js engine. + if (error instanceof IdlHarnessError) { + error = error.message; + } + if (e.message !== error) { + throw new IdlHarnessError(`${idlArrayFunc} threw "${e}", not the expected IdlHarnessError "${error}"`); + } + return; + } + throw new IdlHarnessError(`${idlArrayFunc} did not throw the expected IdlHarnessError`); +} + +IdlArray.prototype.test = function() +{ + /** Entry point. See documentation at beginning of file. */ + + // First merge in all partial definitions and interface mixins. + this.merge_partials(); + this.merge_mixins(); + + // Assert B defined for A : B + for (const member of Object.values(this.members).filter(m => m.base)) { + const lhs = member.name; + const rhs = member.base; + if (!(rhs in this.members)) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is undefined.`); + const lhs_is_interface = this.members[lhs] instanceof IdlInterface; + const rhs_is_interface = this.members[rhs] instanceof IdlInterface; + if (rhs_is_interface != lhs_is_interface) { + if (!lhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${lhs} is not an interface.`); + if (!rhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is not an interface.`); + } + // Check for circular dependencies. + member.get_reverse_inheritance_stack(); + } + + Object.getOwnPropertyNames(this.members).forEach(function(memberName) { + var member = this.members[memberName]; + if (!(member instanceof IdlInterface || member instanceof IdlNamespace)) { + return; + } + + var globals = exposure_set(member); + member.exposed = exposed_in(globals); + member.exposureSet = globals; + }.bind(this)); + + // Now run test() on every member, and test_object() for every object. + for (var name in this.members) + { + this.members[name].test(); + if (name in this.objects) + { + const objects = this.objects[name]; + if (!objects || !Array.isArray(objects)) { + throw new IdlHarnessError(`Invalid or empty objects for member ${name}`); + } + objects.forEach(function(str) + { + if (!this.members[name] || !(this.members[name] instanceof IdlInterface)) { + throw new IdlHarnessError(`Invalid object member name ${name}`); + } + this.members[name].test_object(str); + }.bind(this)); + } + } +}; + +IdlArray.prototype.merge_partials = function() +{ + const testedPartials = new Map(); + this.partials.forEach(function(parsed_idl) + { + const originalExists = parsed_idl.name in this.members + && (this.members[parsed_idl.name] instanceof IdlInterface + || this.members[parsed_idl.name] instanceof IdlDictionary + || this.members[parsed_idl.name] instanceof IdlNamespace); + + // Ensure unique test name in case of multiple partials. + let partialTestName = parsed_idl.name; + let partialTestCount = 1; + if (testedPartials.has(parsed_idl.name)) { + partialTestCount += testedPartials.get(parsed_idl.name); + partialTestName = `${partialTestName}[${partialTestCount}]`; + } + testedPartials.set(parsed_idl.name, partialTestCount); + + if (!self.shouldRunSubTest(partialTestName)) { + return; + } + + if (!parsed_idl.untested) { + test(function () { + assert_true(originalExists, `Original ${parsed_idl.type} should be defined`); + + var expected; + switch (parsed_idl.type) { + case 'dictionary': expected = IdlDictionary; break; + case 'namespace': expected = IdlNamespace; break; + case 'interface': + case 'interface mixin': + default: + expected = IdlInterface; break; + } + assert_true( + expected.prototype.isPrototypeOf(this.members[parsed_idl.name]), + `Original ${parsed_idl.name} definition should have type ${parsed_idl.type}`); + }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: original ${parsed_idl.type} defined`); + } + if (!originalExists) { + // Not good.. but keep calm and carry on. + return; + } + + if (parsed_idl.extAttrs) + { + // Special-case "Exposed". Must be a subset of original interface's exposure. + // Exposed on a partial is the equivalent of having the same Exposed on all nested members. + // See https://github.com/heycam/webidl/issues/154 for discrepency between Exposed and + // other extended attributes on partial interfaces. + const exposureAttr = parsed_idl.extAttrs.find(a => a.name === "Exposed"); + if (exposureAttr) { + if (!parsed_idl.untested) { + test(function () { + const partialExposure = exposure_set(parsed_idl); + const memberExposure = exposure_set(this.members[parsed_idl.name]); + if (memberExposure === "*") { + return; + } + if (partialExposure === "*") { + throw new IdlHarnessError( + `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed everywhere, the original ${parsed_idl.type} is not.`); + } + partialExposure.forEach(name => { + if (!memberExposure || !memberExposure.has(name)) { + throw new IdlHarnessError( + `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed to '${name}', the original ${parsed_idl.type} is not.`); + } + }); + }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: valid exposure set`); + } + parsed_idl.members.forEach(function (member) { + member.extAttrs.push(exposureAttr); + }.bind(this)); + } + + parsed_idl.extAttrs.forEach(function(extAttr) + { + // "Exposed" already handled above. + if (extAttr.name === "Exposed") { + return; + } + this.members[parsed_idl.name].extAttrs.push(extAttr); + }.bind(this)); + } + if (parsed_idl.members.length) { + test(function () { + var clash = parsed_idl.members.find(function(member) { + return this.members[parsed_idl.name].members.find(function(m) { + return this.are_duplicate_members(m, member); + }.bind(this)); + }.bind(this)); + parsed_idl.members.forEach(function(member) + { + this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member)); + }.bind(this)); + assert_true(!clash, "member " + (clash && clash.name) + " is unique"); + }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: member names are unique`); + } + }.bind(this)); + this.partials = []; +} + +IdlArray.prototype.merge_mixins = function() +{ + for (const parsed_idl of this.includes) + { + const lhs = parsed_idl.target; + const rhs = parsed_idl.includes; + const testName = lhs + " includes " + rhs + ": member names are unique"; + + var errStr = lhs + " includes " + rhs + ", but "; + if (!(lhs in this.members)) throw errStr + lhs + " is undefined."; + if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface."; + if (!(rhs in this.members)) throw errStr + rhs + " is undefined."; + if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface."; + + if (this.members[rhs].members.length && self.shouldRunSubTest(testName)) { + test(function () { + var clash = this.members[rhs].members.find(function(member) { + return this.members[lhs].members.find(function(m) { + return this.are_duplicate_members(m, member); + }.bind(this)); + }.bind(this)); + this.members[rhs].members.forEach(function(member) { + assert_true( + this.members[lhs].members.every(m => !this.are_duplicate_members(m, member)), + "member " + member.name + " is unique"); + this.members[lhs].members.push(new IdlInterfaceMember(member)); + }.bind(this)); + assert_true(!clash, "member " + (clash && clash.name) + " is unique"); + }.bind(this), testName); + } + } + this.includes = []; +} + +IdlArray.prototype.are_duplicate_members = function(m1, m2) { + if (m1.name !== m2.name) { + return false; + } + if (m1.type === 'operation' && m2.type === 'operation' + && m1.arguments.length !== m2.arguments.length) { + // Method overload. TODO: Deep comparison of arguments. + return false; + } + return true; +} + +IdlArray.prototype.assert_type_is = function(value, type) +{ + if (type.idlType in this.members + && this.members[type.idlType] instanceof IdlTypedef) { + this.assert_type_is(value, this.members[type.idlType].idlType); + return; + } + + if (type.nullable && value === null) + { + // This is fine + return; + } + + if (type.union) { + for (var i = 0; i < type.idlType.length; i++) { + try { + this.assert_type_is(value, type.idlType[i]); + // No AssertionError, so we match one type in the union + return; + } catch(e) { + if (e instanceof AssertionError) { + // We didn't match this type, let's try some others + continue; + } + throw e; + } + } + // TODO: Is there a nice way to list the union's types in the message? + assert_true(false, "Attribute has value " + format_value(value) + + " which doesn't match any of the types in the union"); + + } + + /** + * Helper function that tests that value is an instance of type according + * to the rules of WebIDL. value is any JavaScript value, and type is an + * object produced by WebIDLParser.js' "type" production. That production + * is fairly elaborate due to the complexity of WebIDL's types, so it's + * best to look at the grammar to figure out what properties it might have. + */ + if (type.idlType == "any") + { + // No assertions to make + return; + } + + if (type.array) + { + // TODO: not supported yet + return; + } + + if (type.generic === "sequence" || type.generic == "ObservableArray") + { + assert_true(Array.isArray(value), "should be an Array"); + if (!value.length) + { + // Nothing we can do. + return; + } + this.assert_type_is(value[0], type.idlType[0]); + return; + } + + if (type.generic === "Promise") { + assert_true("then" in value, "Attribute with a Promise type should have a then property"); + // TODO: Ideally, we would check on project fulfillment + // that we get the right type + // but that would require making the type check async + return; + } + + if (type.generic === "FrozenArray") { + assert_true(Array.isArray(value), "Value should be array"); + assert_true(Object.isFrozen(value), "Value should be frozen"); + if (!value.length) + { + // Nothing we can do. + return; + } + this.assert_type_is(value[0], type.idlType[0]); + return; + } + + type = Array.isArray(type.idlType) ? type.idlType[0] : type.idlType; + + switch(type) + { + case "undefined": + assert_equals(value, undefined); + return; + + case "boolean": + assert_equals(typeof value, "boolean"); + return; + + case "byte": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(-128 <= value && value <= 127, "byte " + value + " should be in range [-128, 127]"); + return; + + case "octet": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(0 <= value && value <= 255, "octet " + value + " should be in range [0, 255]"); + return; + + case "short": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(-32768 <= value && value <= 32767, "short " + value + " should be in range [-32768, 32767]"); + return; + + case "unsigned short": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(0 <= value && value <= 65535, "unsigned short " + value + " should be in range [0, 65535]"); + return; + + case "long": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(-2147483648 <= value && value <= 2147483647, "long " + value + " should be in range [-2147483648, 2147483647]"); + return; + + case "unsigned long": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(0 <= value && value <= 4294967295, "unsigned long " + value + " should be in range [0, 4294967295]"); + return; + + case "long long": + assert_equals(typeof value, "number"); + return; + + case "unsigned long long": + case "DOMTimeStamp": + assert_equals(typeof value, "number"); + assert_true(0 <= value, "unsigned long long should be positive"); + return; + + case "float": + assert_equals(typeof value, "number"); + assert_equals(value, Math.fround(value), "float rounded to 32-bit float should be itself"); + assert_not_equals(value, Infinity); + assert_not_equals(value, -Infinity); + assert_not_equals(value, NaN); + return; + + case "DOMHighResTimeStamp": + case "double": + assert_equals(typeof value, "number"); + assert_not_equals(value, Infinity); + assert_not_equals(value, -Infinity); + assert_not_equals(value, NaN); + return; + + case "unrestricted float": + assert_equals(typeof value, "number"); + assert_equals(value, Math.fround(value), "unrestricted float rounded to 32-bit float should be itself"); + return; + + case "unrestricted double": + assert_equals(typeof value, "number"); + return; + + case "DOMString": + assert_equals(typeof value, "string"); + return; + + case "ByteString": + assert_equals(typeof value, "string"); + assert_regexp_match(value, /^[\x00-\x7F]*$/); + return; + + case "USVString": + assert_equals(typeof value, "string"); + assert_regexp_match(value, /^([\x00-\ud7ff\ue000-\uffff]|[\ud800-\udbff][\udc00-\udfff])*$/); + return; + + case "ArrayBufferView": + assert_true(ArrayBuffer.isView(value)); + return; + + case "object": + assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); + return; + } + + // This is a catch-all for any IDL type name which follows JS class + // semantics. This includes some non-interface IDL types (e.g. Int8Array, + // Function, ...), as well as any interface types that are not in the IDL + // that is fed to the harness. If an IDL type does not follow JS class + // semantics then it should go in the switch statement above. If an IDL + // type needs full checking, then the test should include it in the IDL it + // feeds to the harness. + if (!(type in this.members)) + { + assert_true(value instanceof self[type], "wrong type: not a " + type); + return; + } + + if (this.members[type] instanceof IdlInterface) + { + // We don't want to run the full + // IdlInterface.prototype.test_instance_of, because that could result + // in an infinite loop. TODO: This means we don't have tests for + // LegacyNoInterfaceObject interfaces, and we also can't test objects + // that come from another self. + assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); + if (value instanceof Object + && !this.members[type].has_extended_attribute("LegacyNoInterfaceObject") + && type in self) + { + assert_true(value instanceof self[type], "instanceof " + type); + } + } + else if (this.members[type] instanceof IdlEnum) + { + assert_equals(typeof value, "string"); + } + else if (this.members[type] instanceof IdlDictionary) + { + // TODO: Test when we actually have something to test this on + } + else if (this.members[type] instanceof IdlCallback) + { + assert_equals(typeof value, "function"); + } + else + { + throw new IdlHarnessError("Type " + type + " isn't an interface, callback or dictionary"); + } +}; + +/// IdlObject /// +function IdlObject() {} +IdlObject.prototype.test = function() +{ + /** + * By default, this does nothing, so no actual tests are run for IdlObjects + * that don't define any (e.g., IdlDictionary at the time of this writing). + */ +}; + +IdlObject.prototype.has_extended_attribute = function(name) +{ + /** + * This is only meaningful for things that support extended attributes, + * such as interfaces, exceptions, and members. + */ + return this.extAttrs.some(function(o) + { + return o.name == name; + }); +}; + + +/// IdlDictionary /// +// Used for IdlArray.prototype.assert_type_is +function IdlDictionary(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "dictionary" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** A back-reference to our IdlArray. */ + this.array = obj.array; + + /** An array of objects produced by the "dictionaryMember" production. */ + this.members = obj.members; + + /** + * The name (as a string) of the dictionary type we inherit from, or null + * if there is none. + */ + this.base = obj.inheritance; +} + +IdlDictionary.prototype = Object.create(IdlObject.prototype); + +IdlDictionary.prototype.get_reverse_inheritance_stack = function() { + return IdlInterface.prototype.get_reverse_inheritance_stack.call(this); +}; + +/// IdlInterface /// +function IdlInterface(obj, is_callback, is_mixin) +{ + /** + * obj is an object produced by the WebIDLParser.js "interface" production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** A back-reference to our IdlArray. */ + this.array = obj.array; + + /** + * An indicator of whether we should run tests on the interface object and + * interface prototype object. Tests on members are controlled by .untested + * on each member, not this. + */ + this.untested = obj.untested; + + /** An array of objects produced by the "ExtAttr" production. */ + this.extAttrs = obj.extAttrs; + + /** An array of IdlInterfaceMembers. */ + this.members = obj.members.map(function(m){return new IdlInterfaceMember(m); }); + if (this.has_extended_attribute("LegacyUnforgeable")) { + this.members + .filter(function(m) { return m.special !== "static" && (m.type == "attribute" || m.type == "operation"); }) + .forEach(function(m) { return m.isUnforgeable = true; }); + } + + /** + * The name (as a string) of the type we inherit from, or null if there is + * none. + */ + this.base = obj.inheritance; + + this._is_callback = is_callback; + this._is_mixin = is_mixin; +} +IdlInterface.prototype = Object.create(IdlObject.prototype); +IdlInterface.prototype.is_callback = function() +{ + return this._is_callback; +}; + +IdlInterface.prototype.is_mixin = function() +{ + return this._is_mixin; +}; + +IdlInterface.prototype.has_constants = function() +{ + return this.members.some(function(member) { + return member.type === "const"; + }); +}; + +IdlInterface.prototype.get_unscopables = function() +{ + return this.members.filter(function(member) { + return member.isUnscopable; + }); +}; + +IdlInterface.prototype.is_global = function() +{ + return this.extAttrs.some(function(attribute) { + return attribute.name === "Global"; + }); +}; + +/** + * Value of the LegacyNamespace extended attribute, if any. + * + * https://webidl.spec.whatwg.org/#LegacyNamespace + */ +IdlInterface.prototype.get_legacy_namespace = function() +{ + var legacyNamespace = this.extAttrs.find(function(attribute) { + return attribute.name === "LegacyNamespace"; + }); + return legacyNamespace ? legacyNamespace.rhs.value : undefined; +}; + +IdlInterface.prototype.get_interface_object_owner = function() +{ + var legacyNamespace = this.get_legacy_namespace(); + return legacyNamespace ? self[legacyNamespace] : self; +}; + +IdlInterface.prototype.should_have_interface_object = function() +{ + // "For every interface that is exposed in a given ECMAScript global + // environment and: + // * is a callback interface that has constants declared on it, or + // * is a non-callback interface that is not declared with the + // [LegacyNoInterfaceObject] extended attribute, + // a corresponding property MUST exist on the ECMAScript global object. + + return this.is_callback() ? this.has_constants() : !this.has_extended_attribute("LegacyNoInterfaceObject"); +}; + +IdlInterface.prototype.assert_interface_object_exists = function() +{ + var owner = this.get_legacy_namespace() || "self"; + assert_own_property(self[owner], this.name, owner + " does not have own property " + format_value(this.name)); +}; + +IdlInterface.prototype.get_interface_object = function() { + if (!this.should_have_interface_object()) { + var reason = this.is_callback() ? "lack of declared constants" : "declared [LegacyNoInterfaceObject] attribute"; + throw new IdlHarnessError(this.name + " has no interface object due to " + reason); + } + + return this.get_interface_object_owner()[this.name]; +}; + +IdlInterface.prototype.get_qualified_name = function() { + // https://webidl.spec.whatwg.org/#qualified-name + var legacyNamespace = this.get_legacy_namespace(); + if (legacyNamespace) { + return legacyNamespace + "." + this.name; + } + return this.name; +}; + +IdlInterface.prototype.has_to_json_regular_operation = function() { + return this.members.some(function(m) { + return m.is_to_json_regular_operation(); + }); +}; + +IdlInterface.prototype.has_default_to_json_regular_operation = function() { + return this.members.some(function(m) { + return m.is_to_json_regular_operation() && m.has_extended_attribute("Default"); + }); +}; + +/** + * Implementation of https://webidl.spec.whatwg.org/#create-an-inheritance-stack + * with the order reversed. + * + * The order is reversed so that the base class comes first in the list, because + * this is what all call sites need. + * + * So given: + * + * A : B {}; + * B : C {}; + * C {}; + * + * then A.get_reverse_inheritance_stack() returns [C, B, A], + * and B.get_reverse_inheritance_stack() returns [C, B]. + * + * Note: as dictionary inheritance is expressed identically by the AST, + * this works just as well for getting a stack of inherited dictionaries. + */ +IdlInterface.prototype.get_reverse_inheritance_stack = function() { + const stack = [this]; + let idl_interface = this; + while (idl_interface.base) { + const base = this.array.members[idl_interface.base]; + if (!base) { + throw new Error(idl_interface.type + " " + idl_interface.base + " not found (inherited by " + idl_interface.name + ")"); + } else if (stack.indexOf(base) > -1) { + stack.unshift(base); + const dep_chain = stack.map(i => i.name).join(','); + throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`); + } + idl_interface = base; + stack.unshift(idl_interface); + } + return stack; +}; + +/** + * Implementation of + * https://webidl.spec.whatwg.org/#default-tojson-operation + * for testing purposes. + * + * Collects the IDL types of the attributes that meet the criteria + * for inclusion in the default toJSON operation for easy + * comparison with actual value + */ +IdlInterface.prototype.default_to_json_operation = function() { + const map = new Map() + let isDefault = false; + for (const I of this.get_reverse_inheritance_stack()) { + if (I.has_default_to_json_regular_operation()) { + isDefault = true; + for (const m of I.members) { + if (!m.untested && m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) { + map.set(m.name, m.idlType); + } + } + } else if (I.has_to_json_regular_operation()) { + isDefault = false; + } + } + return isDefault ? map : null; +}; + +IdlInterface.prototype.test = function() +{ + if (this.has_extended_attribute("LegacyNoInterfaceObject") || this.is_mixin()) + { + // No tests to do without an instance. TODO: We should still be able + // to run tests on the prototype object, if we obtain one through some + // other means. + return; + } + + // If the interface object is not exposed, only test that. Members can't be + // tested either, but objects could still be tested in |test_object|. + if (!this.exposed) + { + if (!this.untested) + { + subsetTestByKey(this.name, test, function() { + assert_false(this.name in self, this.name + " interface should not exist"); + }.bind(this), this.name + " interface: existence and properties of interface object"); + } + return; + } + + if (!this.untested) + { + // First test things to do with the exception/interface object and + // exception/interface prototype object. + this.test_self(); + } + // Then test things to do with its members (constants, fields, attributes, + // operations, . . .). These are run even if .untested is true, because + // members might themselves be marked as .untested. This might happen to + // interfaces if the interface itself is untested but a partial interface + // that extends it is tested -- then the interface itself and its initial + // members will be marked as untested, but the members added by the partial + // interface are still tested. + this.test_members(); +}; + +IdlInterface.prototype.constructors = function() +{ + return this.members + .filter(function(m) { return m.type == "constructor"; }); +} + +IdlInterface.prototype.test_self = function() +{ + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + // The name of the property is the identifier of the interface, and its + // value is an object called the interface object. + // The property has the attributes { [[Writable]]: true, + // [[Enumerable]]: false, [[Configurable]]: true }." + // TODO: Should we test here that the property is actually writable + // etc., or trust getOwnPropertyDescriptor? + this.assert_interface_object_exists(); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object_owner(), this.name); + assert_false("get" in desc, "self's property " + format_value(this.name) + " should not have a getter"); + assert_false("set" in desc, "self's property " + format_value(this.name) + " should not have a setter"); + assert_true(desc.writable, "self's property " + format_value(this.name) + " should be writable"); + assert_false(desc.enumerable, "self's property " + format_value(this.name) + " should not be enumerable"); + assert_true(desc.configurable, "self's property " + format_value(this.name) + " should be configurable"); + + if (this.is_callback()) { + // "The internal [[Prototype]] property of an interface object for + // a callback interface must be the Function.prototype object." + assert_equals(Object.getPrototypeOf(this.get_interface_object()), Function.prototype, + "prototype of self's property " + format_value(this.name) + " is not Object.prototype"); + + return; + } + + // "The interface object for a given non-callback interface is a + // function object." + // "If an object is defined to be a function object, then it has + // characteristics as follows:" + + // Its [[Prototype]] internal property is otherwise specified (see + // below). + + // "* Its [[Get]] internal property is set as described in ECMA-262 + // section 9.1.8." + // Not much to test for this. + + // "* Its [[Construct]] internal property is set as described in + // ECMA-262 section 19.2.2.3." + + // "* Its @@hasInstance property is set as described in ECMA-262 + // section 19.2.3.8, unless otherwise specified." + // TODO + + // ES6 (rev 30) 19.1.3.6: + // "Else, if O has a [[Call]] internal method, then let builtinTag be + // "Function"." + assert_class_string(this.get_interface_object(), "Function", "class string of " + this.name); + + // "The [[Prototype]] internal property of an interface object for a + // non-callback interface is determined as follows:" + var prototype = Object.getPrototypeOf(this.get_interface_object()); + if (this.base) { + // "* If the interface inherits from some other interface, the + // value of [[Prototype]] is the interface object for that other + // interface." + var inherited_interface = this.array.members[this.base]; + if (!inherited_interface.has_extended_attribute("LegacyNoInterfaceObject")) { + inherited_interface.assert_interface_object_exists(); + assert_equals(prototype, inherited_interface.get_interface_object(), + 'prototype of ' + this.name + ' is not ' + + this.base); + } + } else { + // "If the interface doesn't inherit from any other interface, the + // value of [[Prototype]] is %FunctionPrototype% ([ECMA-262], + // section 6.1.7.4)." + assert_equals(prototype, Function.prototype, + "prototype of self's property " + format_value(this.name) + " is not Function.prototype"); + } + + // Always test for [[Construct]]: + // https://github.com/heycam/webidl/issues/698 + assert_true(isConstructor(this.get_interface_object()), "interface object must pass IsConstructor check"); + + var interface_object = this.get_interface_object(); + assert_throws_js(globalOf(interface_object).TypeError, function() { + interface_object(); + }, "interface object didn't throw TypeError when called as a function"); + + if (!this.constructors().length) { + assert_throws_js(globalOf(interface_object).TypeError, function() { + new interface_object(); + }, "interface object didn't throw TypeError when called as a constructor"); + } + }.bind(this), this.name + " interface: existence and properties of interface object"); + + if (this.should_have_interface_object() && !this.is_callback()) { + subsetTestByKey(this.name, test, function() { + // This function tests WebIDL as of 2014-10-25. + // https://webidl.spec.whatwg.org/#es-interface-call + + this.assert_interface_object_exists(); + + // "Interface objects for non-callback interfaces MUST have a + // property named “length” with attributes { [[Writable]]: false, + // [[Enumerable]]: false, [[Configurable]]: true } whose value is + // a Number." + assert_own_property(this.get_interface_object(), "length"); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "length"); + assert_false("get" in desc, this.name + ".length should not have a getter"); + assert_false("set" in desc, this.name + ".length should not have a setter"); + assert_false(desc.writable, this.name + ".length should not be writable"); + assert_false(desc.enumerable, this.name + ".length should not be enumerable"); + assert_true(desc.configurable, this.name + ".length should be configurable"); + + var constructors = this.constructors(); + var expected_length = minOverloadLength(constructors); + assert_equals(this.get_interface_object().length, expected_length, "wrong value for " + this.name + ".length"); + }.bind(this), this.name + " interface object length"); + } + + if (this.should_have_interface_object()) { + subsetTestByKey(this.name, test, function() { + // This function tests WebIDL as of 2015-11-17. + // https://webidl.spec.whatwg.org/#interface-object + + this.assert_interface_object_exists(); + + // "All interface objects must have a property named “name” with + // attributes { [[Writable]]: false, [[Enumerable]]: false, + // [[Configurable]]: true } whose value is the identifier of the + // corresponding interface." + + assert_own_property(this.get_interface_object(), "name"); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "name"); + assert_false("get" in desc, this.name + ".name should not have a getter"); + assert_false("set" in desc, this.name + ".name should not have a setter"); + assert_false(desc.writable, this.name + ".name should not be writable"); + assert_false(desc.enumerable, this.name + ".name should not be enumerable"); + assert_true(desc.configurable, this.name + ".name should be configurable"); + assert_equals(this.get_interface_object().name, this.name, "wrong value for " + this.name + ".name"); + }.bind(this), this.name + " interface object name"); + } + + + if (this.has_extended_attribute("LegacyWindowAlias")) { + subsetTestByKey(this.name, test, function() + { + var aliasAttrs = this.extAttrs.filter(function(o) { return o.name === "LegacyWindowAlias"; }); + if (aliasAttrs.length > 1) { + throw new IdlHarnessError("Invalid IDL: multiple LegacyWindowAlias extended attributes on " + this.name); + } + if (this.is_callback()) { + throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on non-interface " + this.name); + } + if (!(this.exposureSet === "*" || this.exposureSet.has("Window"))) { + throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " which is not exposed in Window"); + } + // TODO: when testing of [LegacyNoInterfaceObject] interfaces is supported, + // check that it's not specified together with LegacyWindowAlias. + + // TODO: maybe check that [LegacyWindowAlias] is not specified on a partial interface. + + var rhs = aliasAttrs[0].rhs; + if (!rhs) { + throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " without identifier"); + } + var aliases; + if (rhs.type === "identifier-list") { + aliases = rhs.value.map(id => id.value); + } else { // rhs.type === identifier + aliases = [ rhs.value ]; + } + + // OK now actually check the aliases... + var alias; + if (exposed_in(exposure_set(this, this.exposureSet)) && 'document' in self) { + for (alias of aliases) { + assert_true(alias in self, alias + " should exist"); + assert_equals(self[alias], this.get_interface_object(), "self." + alias + " should be the same value as self." + this.get_qualified_name()); + var desc = Object.getOwnPropertyDescriptor(self, alias); + assert_equals(desc.value, this.get_interface_object(), "wrong value in " + alias + " property descriptor"); + assert_true(desc.writable, alias + " should be writable"); + assert_false(desc.enumerable, alias + " should not be enumerable"); + assert_true(desc.configurable, alias + " should be configurable"); + assert_false('get' in desc, alias + " should not have a getter"); + assert_false('set' in desc, alias + " should not have a setter"); + } + } else { + for (alias of aliases) { + assert_false(alias in self, alias + " should not exist"); + } + } + + }.bind(this), this.name + " interface: legacy window alias"); + } + + if (this.has_extended_attribute("LegacyFactoryFunction")) { + var constructors = this.extAttrs + .filter(function(attr) { return attr.name == "LegacyFactoryFunction"; }); + if (constructors.length !== 1) { + throw new IdlHarnessError("Internal error: missing support for multiple LegacyFactoryFunction extended attributes"); + } + var constructor = constructors[0]; + var min_length = minOverloadLength([constructor]); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "for every [LegacyFactoryFunction] extended attribute on an exposed + // interface, a corresponding property must exist on the ECMAScript + // global object. The name of the property is the + // [LegacyFactoryFunction]'s identifier, and its value is an object + // called a named constructor, ... . The property has the attributes + // { [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }." + var name = constructor.rhs.value; + assert_own_property(self, name); + var desc = Object.getOwnPropertyDescriptor(self, name); + assert_equals(desc.value, self[name], "wrong value in " + name + " property descriptor"); + assert_true(desc.writable, name + " should be writable"); + assert_false(desc.enumerable, name + " should not be enumerable"); + assert_true(desc.configurable, name + " should be configurable"); + assert_false("get" in desc, name + " should not have a getter"); + assert_false("set" in desc, name + " should not have a setter"); + }.bind(this), this.name + " interface: named constructor"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "2. Let F be ! CreateBuiltinFunction(realm, steps, + // realm.[[Intrinsics]].[[%FunctionPrototype%]])." + var name = constructor.rhs.value; + var value = self[name]; + assert_equals(typeof value, "function", "type of value in " + name + " property descriptor"); + assert_not_equals(value, this.get_interface_object(), "wrong value in " + name + " property descriptor"); + assert_equals(Object.getPrototypeOf(value), Function.prototype, "wrong value for " + name + "'s prototype"); + }.bind(this), this.name + " interface: named constructor object"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "7. Let proto be the interface prototype object of interface I + // in realm. + // "8. Perform ! DefinePropertyOrThrow(F, "prototype", + // PropertyDescriptor{ + // [[Value]]: proto, [[Writable]]: false, + // [[Enumerable]]: false, [[Configurable]]: false + // })." + var name = constructor.rhs.value; + var expected = this.get_interface_object().prototype; + var desc = Object.getOwnPropertyDescriptor(self[name], "prototype"); + assert_equals(desc.value, expected, "wrong value for " + name + ".prototype"); + assert_false(desc.writable, "prototype should not be writable"); + assert_false(desc.enumerable, "prototype should not be enumerable"); + assert_false(desc.configurable, "prototype should not be configurable"); + assert_false("get" in desc, "prototype should not have a getter"); + assert_false("set" in desc, "prototype should not have a setter"); + }.bind(this), this.name + " interface: named constructor prototype property"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "3. Perform ! SetFunctionName(F, id)." + var name = constructor.rhs.value; + var desc = Object.getOwnPropertyDescriptor(self[name], "name"); + assert_equals(desc.value, name, "wrong value for " + name + ".name"); + assert_false(desc.writable, "name should not be writable"); + assert_false(desc.enumerable, "name should not be enumerable"); + assert_true(desc.configurable, "name should be configurable"); + assert_false("get" in desc, "name should not have a getter"); + assert_false("set" in desc, "name should not have a setter"); + }.bind(this), this.name + " interface: named constructor name"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "4. Initialize S to the effective overload set for constructors + // with identifier id on interface I and with argument count 0. + // "5. Let length be the length of the shortest argument list of + // the entries in S. + // "6. Perform ! SetFunctionLength(F, length)." + var name = constructor.rhs.value; + var desc = Object.getOwnPropertyDescriptor(self[name], "length"); + assert_equals(desc.value, min_length, "wrong value for " + name + ".length"); + assert_false(desc.writable, "length should not be writable"); + assert_false(desc.enumerable, "length should not be enumerable"); + assert_true(desc.configurable, "length should be configurable"); + assert_false("get" in desc, "length should not have a getter"); + assert_false("set" in desc, "length should not have a setter"); + }.bind(this), this.name + " interface: named constructor length"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "1. Let steps be the following steps: + // " 1. If NewTarget is undefined, then throw a TypeError." + var name = constructor.rhs.value; + var args = constructor.arguments.map(function(arg) { + return create_suitable_object(arg.idlType); + }); + assert_throws_js(globalOf(self[name]).TypeError, function() { + self[name](...args); + }.bind(this)); + }.bind(this), this.name + " interface: named constructor without 'new'"); + } + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2015-01-21. + // https://webidl.spec.whatwg.org/#interface-object + + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + // "An interface object for a non-callback interface must have a + // property named “prototype” with attributes { [[Writable]]: false, + // [[Enumerable]]: false, [[Configurable]]: false } whose value is an + // object called the interface prototype object. This object has + // properties that correspond to the regular attributes and regular + // operations defined on the interface, and is described in more detail + // in section 4.5.4 below." + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "prototype"); + assert_false("get" in desc, this.name + ".prototype should not have a getter"); + assert_false("set" in desc, this.name + ".prototype should not have a setter"); + assert_false(desc.writable, this.name + ".prototype should not be writable"); + assert_false(desc.enumerable, this.name + ".prototype should not be enumerable"); + assert_false(desc.configurable, this.name + ".prototype should not be configurable"); + + // Next, test that the [[Prototype]] of the interface prototype object + // is correct. (This is made somewhat difficult by the existence of + // [LegacyNoInterfaceObject].) + // TODO: Aryeh thinks there's at least other place in this file where + // we try to figure out if an interface prototype object is + // correct. Consolidate that code. + + // "The interface prototype object for a given interface A must have an + // internal [[Prototype]] property whose value is returned from the + // following steps: + // "If A is declared with the [Global] extended + // attribute, and A supports named properties, then return the named + // properties object for A, as defined in §3.6.4 Named properties + // object. + // "Otherwise, if A is declared to inherit from another interface, then + // return the interface prototype object for the inherited interface. + // "Otherwise, return %ObjectPrototype%. + // + // "In the ECMAScript binding, the DOMException type has some additional + // requirements: + // + // "Unlike normal interface types, the interface prototype object + // for DOMException must have as its [[Prototype]] the intrinsic + // object %ErrorPrototype%." + // + if (this.name === "Window") { + assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype), + 'WindowProperties', + 'Class name for prototype of Window' + + '.prototype is not "WindowProperties"'); + } else { + var inherit_interface, inherit_interface_interface_object; + if (this.base) { + inherit_interface = this.base; + var parent = this.array.members[inherit_interface]; + if (!parent.has_extended_attribute("LegacyNoInterfaceObject")) { + parent.assert_interface_object_exists(); + inherit_interface_interface_object = parent.get_interface_object(); + } + } else if (this.name === "DOMException") { + inherit_interface = 'Error'; + inherit_interface_interface_object = self.Error; + } else { + inherit_interface = 'Object'; + inherit_interface_interface_object = self.Object; + } + if (inherit_interface_interface_object) { + assert_not_equals(inherit_interface_interface_object, undefined, + 'should inherit from ' + inherit_interface + ', but there is no such property'); + assert_own_property(inherit_interface_interface_object, 'prototype', + 'should inherit from ' + inherit_interface + ', but that object has no "prototype" property'); + assert_equals(Object.getPrototypeOf(this.get_interface_object().prototype), + inherit_interface_interface_object.prototype, + 'prototype of ' + this.name + '.prototype is not ' + inherit_interface + '.prototype'); + } else { + // We can't test that we get the correct object, because this is the + // only way to get our hands on it. We only test that its class + // string, at least, is correct. + assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype), + inherit_interface + 'Prototype', + 'Class name for prototype of ' + this.name + + '.prototype is not "' + inherit_interface + 'Prototype"'); + } + } + + // "The class string of an interface prototype object is the + // concatenation of the interface’s qualified identifier and the string + // “Prototype”." + + // Skip these tests for now due to a specification issue about + // prototype name. + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28244 + + // assert_class_string(this.get_interface_object().prototype, this.get_qualified_name() + "Prototype", + // "class string of " + this.name + ".prototype"); + + // String() should end up calling {}.toString if nothing defines a + // stringifier. + if (!this.has_stringifier()) { + // assert_equals(String(this.get_interface_object().prototype), "[object " + this.get_qualified_name() + "Prototype]", + // "String(" + this.name + ".prototype)"); + } + }.bind(this), this.name + " interface: existence and properties of interface prototype object"); + + // "If the interface is declared with the [Global] + // extended attribute, or the interface is in the set of inherited + // interfaces for any other interface that is declared with one of these + // attributes, then the interface prototype object must be an immutable + // prototype exotic object." + // https://webidl.spec.whatwg.org/#interface-prototype-object + if (this.is_global()) { + this.test_immutable_prototype("interface prototype object", this.get_interface_object().prototype); + } + + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // "If the [LegacyNoInterfaceObject] extended attribute was not specified + // on the interface, then the interface prototype object must also have a + // property named “constructor” with attributes { [[Writable]]: true, + // [[Enumerable]]: false, [[Configurable]]: true } whose value is a + // reference to the interface object for the interface." + assert_own_property(this.get_interface_object().prototype, "constructor", + this.name + '.prototype does not have own property "constructor"'); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, "constructor"); + assert_false("get" in desc, this.name + ".prototype.constructor should not have a getter"); + assert_false("set" in desc, this.name + ".prototype.constructor should not have a setter"); + assert_true(desc.writable, this.name + ".prototype.constructor should be writable"); + assert_false(desc.enumerable, this.name + ".prototype.constructor should not be enumerable"); + assert_true(desc.configurable, this.name + ".prototype.constructor should be configurable"); + assert_equals(this.get_interface_object().prototype.constructor, this.get_interface_object(), + this.name + '.prototype.constructor is not the same object as ' + this.name); + }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s "constructor" property'); + + + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // If the interface has any member declared with the [Unscopable] extended + // attribute, then there must be a property on the interface prototype object + // whose name is the @@unscopables symbol, which has the attributes + // { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }, + // and whose value is an object created as follows... + var unscopables = this.get_unscopables().map(m => m.name); + var proto = this.get_interface_object().prototype; + if (unscopables.length != 0) { + assert_own_property( + proto, Symbol.unscopables, + this.name + '.prototype should have an @@unscopables property'); + var desc = Object.getOwnPropertyDescriptor(proto, Symbol.unscopables); + assert_false("get" in desc, + this.name + ".prototype[Symbol.unscopables] should not have a getter"); + assert_false("set" in desc, this.name + ".prototype[Symbol.unscopables] should not have a setter"); + assert_false(desc.writable, this.name + ".prototype[Symbol.unscopables] should not be writable"); + assert_false(desc.enumerable, this.name + ".prototype[Symbol.unscopables] should not be enumerable"); + assert_true(desc.configurable, this.name + ".prototype[Symbol.unscopables] should be configurable"); + assert_equals(desc.value, proto[Symbol.unscopables], + this.name + '.prototype[Symbol.unscopables] should be in the descriptor'); + assert_equals(typeof desc.value, "object", + this.name + '.prototype[Symbol.unscopables] should be an object'); + assert_equals(Object.getPrototypeOf(desc.value), null, + this.name + '.prototype[Symbol.unscopables] should have a null prototype'); + assert_equals(Object.getOwnPropertySymbols(desc.value).length, + 0, + this.name + '.prototype[Symbol.unscopables] should have the right number of symbol-named properties'); + + // Check that we do not have _extra_ unscopables. Checking that we + // have all the ones we should will happen in the per-member tests. + var observed = Object.getOwnPropertyNames(desc.value); + for (var prop of observed) { + assert_not_equals(unscopables.indexOf(prop), + -1, + this.name + '.prototype[Symbol.unscopables] has unexpected property "' + prop + '"'); + } + } else { + assert_equals(Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, Symbol.unscopables), + undefined, + this.name + '.prototype should not have @@unscopables'); + } + }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s @@unscopables property'); +}; + +IdlInterface.prototype.test_immutable_prototype = function(type, obj) +{ + if (typeof Object.setPrototypeOf !== "function") { + return; + } + + subsetTestByKey(this.name, test, function(t) { + var originalValue = Object.getPrototypeOf(obj); + var newValue = Object.create(null); + + t.add_cleanup(function() { + try { + Object.setPrototypeOf(obj, originalValue); + } catch (err) {} + }); + + assert_throws_js(TypeError, function() { + Object.setPrototypeOf(obj, newValue); + }); + + assert_equals( + Object.getPrototypeOf(obj), + originalValue, + "original value not modified" + ); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to a new value via Object.setPrototypeOf " + + "should throw a TypeError"); + + subsetTestByKey(this.name, test, function(t) { + var originalValue = Object.getPrototypeOf(obj); + var newValue = Object.create(null); + + t.add_cleanup(function() { + let setter = Object.getOwnPropertyDescriptor( + Object.prototype, '__proto__' + ).set; + + try { + setter.call(obj, originalValue); + } catch (err) {} + }); + + // We need to find the actual setter for the '__proto__' property, so we + // can determine the right global for it. Walk up the prototype chain + // looking for that property until we find it. + let setter; + { + let cur = obj; + while (cur) { + const desc = Object.getOwnPropertyDescriptor(cur, "__proto__"); + if (desc) { + setter = desc.set; + break; + } + cur = Object.getPrototypeOf(cur); + } + } + assert_throws_js(globalOf(setter).TypeError, function() { + obj.__proto__ = newValue; + }); + + assert_equals( + Object.getPrototypeOf(obj), + originalValue, + "original value not modified" + ); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to a new value via __proto__ " + + "should throw a TypeError"); + + subsetTestByKey(this.name, test, function(t) { + var originalValue = Object.getPrototypeOf(obj); + var newValue = Object.create(null); + + t.add_cleanup(function() { + try { + Reflect.setPrototypeOf(obj, originalValue); + } catch (err) {} + }); + + assert_false(Reflect.setPrototypeOf(obj, newValue)); + + assert_equals( + Object.getPrototypeOf(obj), + originalValue, + "original value not modified" + ); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to a new value via Reflect.setPrototypeOf " + + "should return false"); + + subsetTestByKey(this.name, test, function() { + var originalValue = Object.getPrototypeOf(obj); + + Object.setPrototypeOf(obj, originalValue); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to its original value via Object.setPrototypeOf " + + "should not throw"); + + subsetTestByKey(this.name, test, function() { + var originalValue = Object.getPrototypeOf(obj); + + obj.__proto__ = originalValue; + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to its original value via __proto__ " + + "should not throw"); + + subsetTestByKey(this.name, test, function() { + var originalValue = Object.getPrototypeOf(obj); + + assert_true(Reflect.setPrototypeOf(obj, originalValue)); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to its original value via Reflect.setPrototypeOf " + + "should return true"); +}; + +IdlInterface.prototype.test_member_const = function(member) +{ + if (!this.has_constants()) { + throw new IdlHarnessError("Internal error: test_member_const called without any constants"); + } + + subsetTestByKey(this.name, test, function() + { + this.assert_interface_object_exists(); + + // "For each constant defined on an interface A, there must be + // a corresponding property on the interface object, if it + // exists." + assert_own_property(this.get_interface_object(), member.name); + // "The value of the property is that which is obtained by + // converting the constant’s IDL value to an ECMAScript + // value." + assert_equals(this.get_interface_object()[member.name], constValue(member.value), + "property has wrong value"); + // "The property has attributes { [[Writable]]: false, + // [[Enumerable]]: true, [[Configurable]]: false }." + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name); + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_false(desc.writable, "property should not be writable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_false(desc.configurable, "property should not be configurable"); + }.bind(this), this.name + " interface: constant " + member.name + " on interface object"); + + // "In addition, a property with the same characteristics must + // exist on the interface prototype object." + subsetTestByKey(this.name, test, function() + { + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + assert_own_property(this.get_interface_object().prototype, member.name); + assert_equals(this.get_interface_object().prototype[member.name], constValue(member.value), + "property has wrong value"); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name); + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_false(desc.writable, "property should not be writable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_false(desc.configurable, "property should not be configurable"); + }.bind(this), this.name + " interface: constant " + member.name + " on interface prototype object"); +}; + + +IdlInterface.prototype.test_member_attribute = function(member) + { + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: attribute " + member.name); + a_test.step(function() + { + if (!this.should_have_interface_object()) { + a_test.done(); + return; + } + + this.assert_interface_object_exists(); + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + if (member.special === "static") { + assert_own_property(this.get_interface_object(), member.name, + "The interface object must have a property " + + format_value(member.name)); + a_test.done(); + return; + } + + this.do_member_unscopable_asserts(member); + + if (this.is_global()) { + assert_own_property(self, member.name, + "The global object must have a property " + + format_value(member.name)); + assert_false(member.name in this.get_interface_object().prototype, + "The prototype object should not have a property " + + format_value(member.name)); + + var getter = Object.getOwnPropertyDescriptor(self, member.name).get; + assert_equals(typeof(getter), "function", + format_value(member.name) + " must have a getter"); + + // Try/catch around the get here, since it can legitimately throw. + // If it does, we obviously can't check for equality with direct + // invocation of the getter. + var gotValue; + var propVal; + try { + propVal = self[member.name]; + gotValue = true; + } catch (e) { + gotValue = false; + } + if (gotValue) { + assert_equals(propVal, getter.call(undefined), + "Gets on a global should not require an explicit this"); + } + + // do_interface_attribute_asserts must be the last thing we do, + // since it will call done() on a_test. + this.do_interface_attribute_asserts(self, member, a_test); + } else { + assert_true(member.name in this.get_interface_object().prototype, + "The prototype object must have a property " + + format_value(member.name)); + + if (!member.has_extended_attribute("LegacyLenientThis")) { + if (member.idlType.generic !== "Promise") { + // this.get_interface_object() returns a thing in our global + assert_throws_js(TypeError, function() { + this.get_interface_object().prototype[member.name]; + }.bind(this), "getting property on prototype object must throw TypeError"); + // do_interface_attribute_asserts must be the last thing we + // do, since it will call done() on a_test. + this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test); + } else { + promise_rejects_js(a_test, TypeError, + this.get_interface_object().prototype[member.name]) + .then(a_test.step_func(function() { + // do_interface_attribute_asserts must be the last + // thing we do, since it will call done() on a_test. + this.do_interface_attribute_asserts(this.get_interface_object().prototype, + member, a_test); + }.bind(this))); + } + } else { + assert_equals(this.get_interface_object().prototype[member.name], undefined, + "getting property on prototype object must return undefined"); + // do_interface_attribute_asserts must be the last thing we do, + // since it will call done() on a_test. + this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test); + } + } + }.bind(this)); +}; + +IdlInterface.prototype.test_member_operation = function(member) +{ + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: operation " + member); + a_test.step(function() + { + // This function tests WebIDL as of 2015-12-29. + // https://webidl.spec.whatwg.org/#es-operations + + if (!this.should_have_interface_object()) { + a_test.done(); + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + a_test.done(); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // "For each unique identifier of an exposed operation defined on the + // interface, there must exist a corresponding property, unless the + // effective overload set for that identifier and operation and with an + // argument count of 0 has no entries." + + // TODO: Consider [Exposed]. + + // "The location of the property is determined as follows:" + var memberHolderObject; + // "* If the operation is static, then the property exists on the + // interface object." + if (member.special === "static") { + assert_own_property(this.get_interface_object(), member.name, + "interface object missing static operation"); + memberHolderObject = this.get_interface_object(); + // "* Otherwise, [...] if the interface was declared with the [Global] + // extended attribute, then the property exists + // on every object that implements the interface." + } else if (this.is_global()) { + assert_own_property(self, member.name, + "global object missing non-static operation"); + memberHolderObject = self; + // "* Otherwise, the property exists solely on the interface’s + // interface prototype object." + } else { + assert_own_property(this.get_interface_object().prototype, member.name, + "interface prototype object missing non-static operation"); + memberHolderObject = this.get_interface_object().prototype; + } + this.do_member_unscopable_asserts(member); + this.do_member_operation_asserts(memberHolderObject, member, a_test); + }.bind(this)); +}; + +IdlInterface.prototype.do_member_unscopable_asserts = function(member) +{ + // Check that if the member is unscopable then it's in the + // @@unscopables object properly. + if (!member.isUnscopable) { + return; + } + + var unscopables = this.get_interface_object().prototype[Symbol.unscopables]; + var prop = member.name; + var propDesc = Object.getOwnPropertyDescriptor(unscopables, prop); + assert_equals(typeof propDesc, "object", + this.name + '.prototype[Symbol.unscopables].' + prop + ' must exist') + assert_false("get" in propDesc, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no getter'); + assert_false("set" in propDesc, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no setter'); + assert_true(propDesc.writable, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must be writable'); + assert_true(propDesc.enumerable, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must be enumerable'); + assert_true(propDesc.configurable, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must be configurable'); + assert_equals(propDesc.value, true, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must have the value `true`'); +}; + +IdlInterface.prototype.do_member_operation_asserts = function(memberHolderObject, member, a_test) +{ + var done = a_test.done.bind(a_test); + var operationUnforgeable = member.isUnforgeable; + var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name); + // "The property has attributes { [[Writable]]: B, + // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the + // operation is unforgeable on the interface, and true otherwise". + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_equals(desc.writable, !operationUnforgeable, + "property should be writable if and only if not unforgeable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_equals(desc.configurable, !operationUnforgeable, + "property should be configurable if and only if not unforgeable"); + // "The value of the property is a Function object whose + // behavior is as follows . . ." + assert_equals(typeof memberHolderObject[member.name], "function", + "property must be a function"); + + const operationOverloads = this.members.filter(function(m) { + return m.type == "operation" && m.name == member.name && + (m.special === "static") === (member.special === "static"); + }); + assert_equals( + memberHolderObject[member.name].length, + minOverloadLength(operationOverloads), + "property has wrong .length"); + assert_equals( + memberHolderObject[member.name].name, + member.name, + "property has wrong .name"); + + // Make some suitable arguments + var args = member.arguments.map(function(arg) { + return create_suitable_object(arg.idlType); + }); + + // "Let O be a value determined as follows: + // ". . . + // "Otherwise, throw a TypeError." + // This should be hit if the operation is not static, there is + // no [ImplicitThis] attribute, and the this value is null. + // + // TODO: We currently ignore the [ImplicitThis] case. Except we manually + // check for globals, since otherwise we'll invoke window.close(). And we + // have to skip this test for anything that on the proto chain of "self", + // since that does in fact have implicit-this behavior. + if (member.special !== "static") { + var cb; + if (!this.is_global() && + memberHolderObject[member.name] != self[member.name]) + { + cb = awaitNCallbacks(2, done); + throwOrReject(a_test, member, memberHolderObject[member.name], null, args, + "calling operation with this = null didn't throw TypeError", cb); + } else { + cb = awaitNCallbacks(1, done); + } + + // ". . . If O is not null and is also not a platform object + // that implements interface I, throw a TypeError." + // + // TODO: Test a platform object that implements some other + // interface. (Have to be sure to get inheritance right.) + throwOrReject(a_test, member, memberHolderObject[member.name], {}, args, + "calling operation with this = {} didn't throw TypeError", cb); + } else { + done(); + } +} + +IdlInterface.prototype.test_to_json_operation = function(desc, memberHolderObject, member) { + var instanceName = memberHolderObject && memberHolderObject.constructor.name + || member.name + " object"; + if (member.has_extended_attribute("Default")) { + subsetTestByKey(this.name, test, function() { + var map = this.default_to_json_operation(); + var json = memberHolderObject.toJSON(); + map.forEach(function(type, k) { + assert_true(k in json, "property " + JSON.stringify(k) + " should be present in the output of " + this.name + ".prototype.toJSON()"); + var descriptor = Object.getOwnPropertyDescriptor(json, k); + assert_true(descriptor.writable, "property " + k + " should be writable"); + assert_true(descriptor.configurable, "property " + k + " should be configurable"); + assert_true(descriptor.enumerable, "property " + k + " should be enumerable"); + this.array.assert_type_is(json[k], type); + delete json[k]; + }, this); + }.bind(this), this.name + " interface: default toJSON operation on " + desc); + } else { + subsetTestByKey(this.name, test, function() { + assert_true(this.array.is_json_type(member.idlType), JSON.stringify(member.idlType) + " is not an appropriate return value for the toJSON operation of " + instanceName); + this.array.assert_type_is(memberHolderObject.toJSON(), member.idlType); + }.bind(this), this.name + " interface: toJSON operation on " + desc); + } +}; + +IdlInterface.prototype.test_member_maplike = function(member) { + subsetTestByKey(this.name, test, () => { + const proto = this.get_interface_object().prototype; + + const methods = [ + ["entries", 0], + ["keys", 0], + ["values", 0], + ["forEach", 1], + ["get", 1], + ["has", 1] + ]; + if (!member.readonly) { + methods.push( + ["set", 2], + ["delete", 1], + ["clear", 0] + ); + } + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator); + assert_equals(iteratorDesc.value, proto.entries, `@@iterator should equal entries`); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size"); + assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`); + assert_equals(sizeDesc.set, undefined, `size should not have a setter`); + assert_equals(sizeDesc.enumerable, true, `size enumerable`); + assert_equals(sizeDesc.configurable, true, `size configurable`); + assert_equals(sizeDesc.get.length, 0, `size getter length`); + assert_equals(sizeDesc.get.name, "get size", `size getter name`); + }, `${this.name} interface: maplike<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_setlike = function(member) { + subsetTestByKey(this.name, test, () => { + const proto = this.get_interface_object().prototype; + + const methods = [ + ["entries", 0], + ["keys", 0], + ["values", 0], + ["forEach", 1], + ["has", 1] + ]; + if (!member.readonly) { + methods.push( + ["add", 1], + ["delete", 1], + ["clear", 0] + ); + } + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator); + assert_equals(iteratorDesc.value, proto.values, `@@iterator should equal values`); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size"); + assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`); + assert_equals(sizeDesc.set, undefined, `size should not have a setter`); + assert_equals(sizeDesc.enumerable, true, `size enumerable`); + assert_equals(sizeDesc.configurable, true, `size configurable`); + assert_equals(sizeDesc.get.length, 0, `size getter length`); + assert_equals(sizeDesc.get.name, "get size", `size getter name`); + }, `${this.name} interface: setlike<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_iterable = function(member) { + subsetTestByKey(this.name, test, () => { + const isPairIterator = member.idlType.length === 2; + const proto = this.get_interface_object().prototype; + + const methods = [ + ["entries", 0], + ["keys", 0], + ["values", 0], + ["forEach", 1] + ]; + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + + if (!isPairIterator) { + assert_equals(desc.value, Array.prototype[name], `${name} equality with Array.prototype version`); + } + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + if (isPairIterator) { + assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`); + } else { + assert_equals(iteratorDesc.value, Array.prototype[Symbol.iterator], `@@iterator equality with Array.prototype version`); + } + }, `${this.name} interface: iterable<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_async_iterable = function(member) { + subsetTestByKey(this.name, test, () => { + const isPairIterator = member.idlType.length === 2; + const proto = this.get_interface_object().prototype; + + // Note that although the spec allows arguments, which will be passed to the @@asyncIterator + // method (which is either values or entries), those arguments must always be optional. So + // length of 0 is still correct for values and entries. + const methods = [ + ["values", 0], + ]; + + if (isPairIterator) { + methods.push( + ["entries", 0], + ["keys", 0] + ); + } + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + if (isPairIterator) { + assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`); + } else { + assert_equals(iteratorDesc.value, proto.values, `@@iterator equality with values`); + } + }, `${this.name} interface: async iterable<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_stringifier = function(member) +{ + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // ". . . the property exists on the interface prototype object." + var interfacePrototypeObject = this.get_interface_object().prototype; + assert_own_property(interfacePrototypeObject, "toString", + "interface prototype object missing non-static operation"); + + var stringifierUnforgeable = member.isUnforgeable; + var desc = Object.getOwnPropertyDescriptor(interfacePrototypeObject, "toString"); + // "The property has attributes { [[Writable]]: B, + // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the + // stringifier is unforgeable on the interface, and true otherwise." + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_equals(desc.writable, !stringifierUnforgeable, + "property should be writable if and only if not unforgeable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_equals(desc.configurable, !stringifierUnforgeable, + "property should be configurable if and only if not unforgeable"); + // "The value of the property is a Function object, which behaves as + // follows . . ." + assert_equals(typeof interfacePrototypeObject.toString, "function", + "property must be a function"); + // "The value of the Function object’s “length” property is the Number + // value 0." + assert_equals(interfacePrototypeObject.toString.length, 0, + "property has wrong .length"); + + // "Let O be the result of calling ToObject on the this value." + assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() { + interfacePrototypeObject.toString.apply(null, []); + }, "calling stringifier with this = null didn't throw TypeError"); + + // "If O is not an object that implements the interface on which the + // stringifier was declared, then throw a TypeError." + // + // TODO: Test a platform object that implements some other + // interface. (Have to be sure to get inheritance right.) + assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() { + interfacePrototypeObject.toString.apply({}, []); + }, "calling stringifier with this = {} didn't throw TypeError"); + }.bind(this), this.name + " interface: stringifier"); +}; + +IdlInterface.prototype.test_members = function() +{ + var unexposed_members = new Set(); + for (var i = 0; i < this.members.length; i++) + { + var member = this.members[i]; + if (member.untested) { + continue; + } + + if (!exposed_in(exposure_set(member, this.exposureSet))) { + if (!unexposed_members.has(member.name)) { + unexposed_members.add(member.name); + subsetTestByKey(this.name, test, function() { + // It's not exposed, so we shouldn't find it anywhere. + assert_false(member.name in this.get_interface_object(), + "The interface object must not have a property " + + format_value(member.name)); + assert_false(member.name in this.get_interface_object().prototype, + "The prototype object must not have a property " + + format_value(member.name)); + }.bind(this), this.name + " interface: member " + member.name); + } + continue; + } + + switch (member.type) { + case "const": + this.test_member_const(member); + break; + + case "attribute": + // For unforgeable attributes, we do the checks in + // test_interface_of instead. + if (!member.isUnforgeable) + { + this.test_member_attribute(member); + } + if (member.special === "stringifier") { + this.test_member_stringifier(member); + } + break; + + case "operation": + // TODO: Need to correctly handle multiple operations with the same + // identifier. + // For unforgeable operations, we do the checks in + // test_interface_of instead. + if (member.name) { + if (!member.isUnforgeable) + { + this.test_member_operation(member); + } + } else if (member.special === "stringifier") { + this.test_member_stringifier(member); + } + break; + + case "iterable": + if (member.async) { + this.test_member_async_iterable(member); + } else { + this.test_member_iterable(member); + } + break; + case "maplike": + this.test_member_maplike(member); + break; + case "setlike": + this.test_member_setlike(member); + break; + default: + // TODO: check more member types. + break; + } + } +}; + +IdlInterface.prototype.test_object = function(desc) +{ + var obj, exception = null; + try + { + obj = eval(desc); + } + catch(e) + { + exception = e; + } + + var expected_typeof; + if (this.name == "HTMLAllCollection") + { + // Result of [[IsHTMLDDA]] slot + expected_typeof = "undefined"; + } + else + { + expected_typeof = "object"; + } + + if (this.is_callback()) { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + } else { + this.test_primary_interface_of(desc, obj, exception, expected_typeof); + + var current_interface = this; + while (current_interface) + { + if (!(current_interface.name in this.array.members)) + { + throw new IdlHarnessError("Interface " + current_interface.name + " not found (inherited by " + this.name + ")"); + } + if (current_interface.prevent_multiple_testing && current_interface.already_tested) + { + return; + } + current_interface.test_interface_of(desc, obj, exception, expected_typeof); + current_interface = this.array.members[current_interface.base]; + } + } +}; + +IdlInterface.prototype.test_primary_interface_of = function(desc, obj, exception, expected_typeof) +{ + // Only the object itself, not its members, are tested here, so if the + // interface is untested, there is nothing to do. + if (this.untested) + { + return; + } + + // "The internal [[SetPrototypeOf]] method of every platform object that + // implements an interface with the [Global] extended + // attribute must execute the same algorithm as is defined for the + // [[SetPrototypeOf]] internal method of an immutable prototype exotic + // object." + // https://webidl.spec.whatwg.org/#platform-object-setprototypeof + if (this.is_global()) + { + this.test_immutable_prototype("global platform object", obj); + } + + + // We can't easily test that its prototype is correct if there's no + // interface object, or the object is from a different global environment + // (not instanceof Object). TODO: test in this case that its prototype at + // least looks correct, even if we can't test that it's actually correct. + if (this.should_have_interface_object() + && (typeof obj != expected_typeof || obj instanceof Object)) + { + subsetTestByKey(this.name, test, function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + this.assert_interface_object_exists(); + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // "The value of the internal [[Prototype]] property of the + // platform object is the interface prototype object of the primary + // interface from the platform object’s associated global + // environment." + assert_equals(Object.getPrototypeOf(obj), + this.get_interface_object().prototype, + desc + "'s prototype is not " + this.name + ".prototype"); + }.bind(this), this.name + " must be primary interface of " + desc); + } + + // "The class string of a platform object that implements one or more + // interfaces must be the qualified name of the primary interface of the + // platform object." + subsetTestByKey(this.name, test, function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + assert_class_string(obj, this.get_qualified_name(), "class string of " + desc); + if (!this.has_stringifier()) + { + assert_equals(String(obj), "[object " + this.get_qualified_name() + "]", "String(" + desc + ")"); + } + }.bind(this), "Stringification of " + desc); +}; + +IdlInterface.prototype.test_interface_of = function(desc, obj, exception, expected_typeof) +{ + // TODO: Indexed and named properties, more checks on interface members + this.already_tested = true; + if (!shouldRunSubTest(this.name)) { + return; + } + + var unexposed_properties = new Set(); + for (var i = 0; i < this.members.length; i++) + { + var member = this.members[i]; + if (member.untested) { + continue; + } + if (!exposed_in(exposure_set(member, this.exposureSet))) + { + if (!unexposed_properties.has(member.name)) + { + unexposed_properties.add(member.name); + subsetTestByKey(this.name, test, function() { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_false(member.name in obj); + }.bind(this), this.name + " interface: " + desc + ' must not have property "' + member.name + '"'); + } + continue; + } + if (member.type == "attribute" && member.isUnforgeable) + { + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"'); + a_test.step(function() { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + // Call do_interface_attribute_asserts last, since it will call a_test.done() + this.do_interface_attribute_asserts(obj, member, a_test); + }.bind(this)); + } + else if (member.type == "operation" && + member.name && + member.isUnforgeable) + { + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"'); + a_test.step(function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + assert_own_property(obj, member.name, + "Doesn't have the unforgeable operation property"); + this.do_member_operation_asserts(obj, member, a_test); + }.bind(this)); + } + else if ((member.type == "const" + || member.type == "attribute" + || member.type == "operation") + && member.name) + { + subsetTestByKey(this.name, test, function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + if (member.special !== "static") { + if (!this.is_global()) { + assert_inherits(obj, member.name); + } else { + assert_own_property(obj, member.name); + } + + if (member.type == "const") + { + assert_equals(obj[member.name], constValue(member.value)); + } + if (member.type == "attribute") + { + // Attributes are accessor properties, so they might + // legitimately throw an exception rather than returning + // anything. + var property, thrown = false; + try + { + property = obj[member.name]; + } + catch (e) + { + thrown = true; + } + if (!thrown) + { + if (this.name == "Document" && member.name == "all") + { + // Result of [[IsHTMLDDA]] slot + assert_equals(typeof property, "undefined"); + } + else + { + this.array.assert_type_is(property, member.idlType); + } + } + } + if (member.type == "operation") + { + assert_equals(typeof obj[member.name], "function"); + } + } + }.bind(this), this.name + " interface: " + desc + ' must inherit property "' + member + '" with the proper type'); + } + // TODO: This is wrong if there are multiple operations with the same + // identifier. + // TODO: Test passing arguments of the wrong type. + if (member.type == "operation" && member.name && member.arguments.length) + { + var description = + this.name + " interface: calling " + member + " on " + desc + + " with too few arguments must throw TypeError"; + var a_test = subsetTestByKey(this.name, async_test, description); + a_test.step(function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + var fn; + if (member.special !== "static") { + if (!this.is_global() && !member.isUnforgeable) { + assert_inherits(obj, member.name); + } else { + assert_own_property(obj, member.name); + } + fn = obj[member.name]; + } + else + { + assert_own_property(obj.constructor, member.name, "interface object must have static operation as own property"); + fn = obj.constructor[member.name]; + } + + var minLength = minOverloadLength(this.members.filter(function(m) { + return m.type == "operation" && m.name == member.name; + })); + var args = []; + var cb = awaitNCallbacks(minLength, a_test.done.bind(a_test)); + for (var i = 0; i < minLength; i++) { + throwOrReject(a_test, member, fn, obj, args, "Called with " + i + " arguments", cb); + + args.push(create_suitable_object(member.arguments[i].idlType)); + } + if (minLength === 0) { + cb(); + } + }.bind(this)); + } + + if (member.is_to_json_regular_operation()) { + this.test_to_json_operation(desc, obj, member); + } + } +}; + +IdlInterface.prototype.has_stringifier = function() +{ + if (this.name === "DOMException") { + // toString is inherited from Error, so don't assume we have the + // default stringifer + return true; + } + if (this.members.some(function(member) { return member.special === "stringifier"; })) { + return true; + } + if (this.base && + this.array.members[this.base].has_stringifier()) { + return true; + } + return false; +}; + +IdlInterface.prototype.do_interface_attribute_asserts = function(obj, member, a_test) +{ + // This function tests WebIDL as of 2015-01-27. + // TODO: Consider [Exposed]. + + // This is called by test_member_attribute() with the prototype as obj if + // it is not a global, and the global otherwise, and by test_interface_of() + // with the object as obj. + + var pendingPromises = []; + + // "The name of the property is the identifier of the attribute." + assert_own_property(obj, member.name); + + // "The property has attributes { [[Get]]: G, [[Set]]: S, [[Enumerable]]: + // true, [[Configurable]]: configurable }, where: + // "configurable is false if the attribute was declared with the + // [LegacyUnforgeable] extended attribute and true otherwise; + // "G is the attribute getter, defined below; and + // "S is the attribute setter, also defined below." + var desc = Object.getOwnPropertyDescriptor(obj, member.name); + assert_false("value" in desc, 'property descriptor should not have a "value" field'); + assert_false("writable" in desc, 'property descriptor should not have a "writable" field'); + assert_true(desc.enumerable, "property should be enumerable"); + if (member.isUnforgeable) + { + assert_false(desc.configurable, "[LegacyUnforgeable] property must not be configurable"); + } + else + { + assert_true(desc.configurable, "property must be configurable"); + } + + + // "The attribute getter is a Function object whose behavior when invoked + // is as follows:" + assert_equals(typeof desc.get, "function", "getter must be Function"); + + // "If the attribute is a regular attribute, then:" + if (member.special !== "static") { + // "If O is not a platform object that implements I, then: + // "If the attribute was specified with the [LegacyLenientThis] extended + // attribute, then return undefined. + // "Otherwise, throw a TypeError." + if (!member.has_extended_attribute("LegacyLenientThis")) { + if (member.idlType.generic !== "Promise") { + assert_throws_js(globalOf(desc.get).TypeError, function() { + desc.get.call({}); + }.bind(this), "calling getter on wrong object type must throw TypeError"); + } else { + pendingPromises.push( + promise_rejects_js(a_test, TypeError, desc.get.call({}), + "calling getter on wrong object type must reject the return promise with TypeError")); + } + } else { + assert_equals(desc.get.call({}), undefined, + "calling getter on wrong object type must return undefined"); + } + } + + // "The value of the Function object’s “length” property is the Number + // value 0." + assert_equals(desc.get.length, 0, "getter length must be 0"); + + // "Let name be the string "get " prepended to attribute’s identifier." + // "Perform ! SetFunctionName(F, name)." + assert_equals(desc.get.name, "get " + member.name, + "getter must have the name 'get " + member.name + "'"); + + + // TODO: Test calling setter on the interface prototype (should throw + // TypeError in most cases). + if (member.readonly + && !member.has_extended_attribute("LegacyLenientSetter") + && !member.has_extended_attribute("PutForwards") + && !member.has_extended_attribute("Replaceable")) + { + // "The attribute setter is undefined if the attribute is declared + // readonly and has neither a [PutForwards] nor a [Replaceable] + // extended attribute declared on it." + assert_equals(desc.set, undefined, "setter must be undefined for readonly attributes"); + } + else + { + // "Otherwise, it is a Function object whose behavior when + // invoked is as follows:" + assert_equals(typeof desc.set, "function", "setter must be function for PutForwards, Replaceable, or non-readonly attributes"); + + // "If the attribute is a regular attribute, then:" + if (member.special !== "static") { + // "If /validThis/ is false and the attribute was not specified + // with the [LegacyLenientThis] extended attribute, then throw a + // TypeError." + // "If the attribute is declared with a [Replaceable] extended + // attribute, then: ..." + // "If validThis is false, then return." + if (!member.has_extended_attribute("LegacyLenientThis")) { + assert_throws_js(globalOf(desc.set).TypeError, function() { + desc.set.call({}); + }.bind(this), "calling setter on wrong object type must throw TypeError"); + } else { + assert_equals(desc.set.call({}), undefined, + "calling setter on wrong object type must return undefined"); + } + } + + // "The value of the Function object’s “length” property is the Number + // value 1." + assert_equals(desc.set.length, 1, "setter length must be 1"); + + // "Let name be the string "set " prepended to id." + // "Perform ! SetFunctionName(F, name)." + assert_equals(desc.set.name, "set " + member.name, + "The attribute setter must have the name 'set " + member.name + "'"); + } + + Promise.all(pendingPromises).then(a_test.done.bind(a_test)); +} + +/// IdlInterfaceMember /// +function IdlInterfaceMember(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "ifMember" production. + * We just forward all properties to this object without modification, + * except for special extAttrs handling. + */ + for (var k in obj.toJSON()) + { + this[k] = obj[k]; + } + if (!("extAttrs" in this)) + { + this.extAttrs = []; + } + + this.isUnforgeable = this.has_extended_attribute("LegacyUnforgeable"); + this.isUnscopable = this.has_extended_attribute("Unscopable"); +} + +IdlInterfaceMember.prototype = Object.create(IdlObject.prototype); + +IdlInterfaceMember.prototype.toJSON = function() { + return this; +}; + +IdlInterfaceMember.prototype.is_to_json_regular_operation = function() { + return this.type == "operation" && this.special !== "static" && this.name == "toJSON"; +}; + +IdlInterfaceMember.prototype.toString = function() { + function formatType(type) { + var result; + if (type.generic) { + result = type.generic + "<" + type.idlType.map(formatType).join(", ") + ">"; + } else if (type.union) { + result = "(" + type.subtype.map(formatType).join(" or ") + ")"; + } else { + result = type.idlType; + } + if (type.nullable) { + result += "?" + } + return result; + } + + if (this.type === "operation") { + var args = this.arguments.map(function(m) { + return [ + m.optional ? "optional " : "", + formatType(m.idlType), + m.variadic ? "..." : "", + ].join(""); + }).join(", "); + return this.name + "(" + args + ")"; + } + + return this.name; +} + +/// Internal helper functions /// +function create_suitable_object(type) +{ + /** + * type is an object produced by the WebIDLParser.js "type" production. We + * return a JavaScript value that matches the type, if we can figure out + * how. + */ + if (type.nullable) + { + return null; + } + switch (type.idlType) + { + case "any": + case "boolean": + return true; + + case "byte": case "octet": case "short": case "unsigned short": + case "long": case "unsigned long": case "long long": + case "unsigned long long": case "float": case "double": + case "unrestricted float": case "unrestricted double": + return 7; + + case "DOMString": + case "ByteString": + case "USVString": + return "foo"; + + case "object": + return {a: "b"}; + + case "Node": + return document.createTextNode("abc"); + } + return null; +} + +/// IdlEnum /// +// Used for IdlArray.prototype.assert_type_is +function IdlEnum(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "dictionary" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** An array of values produced by the "enum" production. */ + this.values = obj.values; + +} + +IdlEnum.prototype = Object.create(IdlObject.prototype); + +/// IdlCallback /// +// Used for IdlArray.prototype.assert_type_is +function IdlCallback(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "callback" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** Arguments for the callback. */ + this.arguments = obj.arguments; +} + +IdlCallback.prototype = Object.create(IdlObject.prototype); + +/// IdlTypedef /// +// Used for IdlArray.prototype.assert_type_is +function IdlTypedef(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "typedef" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** The idlType that we are supposed to be typedeffing to. */ + this.idlType = obj.idlType; + +} + +IdlTypedef.prototype = Object.create(IdlObject.prototype); + +/// IdlNamespace /// +function IdlNamespace(obj) +{ + this.name = obj.name; + this.extAttrs = obj.extAttrs; + this.untested = obj.untested; + /** A back-reference to our IdlArray. */ + this.array = obj.array; + + /** An array of IdlInterfaceMembers. */ + this.members = obj.members.map(m => new IdlInterfaceMember(m)); +} + +IdlNamespace.prototype = Object.create(IdlObject.prototype); + +IdlNamespace.prototype.do_member_operation_asserts = function (memberHolderObject, member, a_test) +{ + var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name); + + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_equals( + desc.writable, + !member.isUnforgeable, + "property should be writable if and only if not unforgeable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_equals( + desc.configurable, + !member.isUnforgeable, + "property should be configurable if and only if not unforgeable"); + + assert_equals( + typeof memberHolderObject[member.name], + "function", + "property must be a function"); + + assert_equals( + memberHolderObject[member.name].length, + minOverloadLength(this.members.filter(function(m) { + return m.type == "operation" && m.name == member.name; + })), + "operation has wrong .length"); + a_test.done(); +} + +IdlNamespace.prototype.test_member_operation = function(member) +{ + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey( + this.name, + async_test, + this.name + ' namespace: operation ' + member); + a_test.step(function() { + assert_own_property( + self[this.name], + member.name, + 'namespace object missing operation ' + format_value(member.name)); + + this.do_member_operation_asserts(self[this.name], member, a_test); + }.bind(this)); +}; + +IdlNamespace.prototype.test_member_attribute = function (member) +{ + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey( + this.name, + async_test, + this.name + ' namespace: attribute ' + member.name); + a_test.step(function() + { + assert_own_property( + self[this.name], + member.name, + this.name + ' does not have property ' + format_value(member.name)); + + var desc = Object.getOwnPropertyDescriptor(self[this.name], member.name); + assert_equals(desc.set, undefined, "setter must be undefined for namespace members"); + a_test.done(); + }.bind(this)); +}; + +IdlNamespace.prototype.test_self = function () +{ + /** + * TODO(lukebjerring): Assert: + * - "Note that unlike interfaces or dictionaries, namespaces do not create types." + */ + + subsetTestByKey(this.name, test, () => { + assert_true(this.extAttrs.every(o => o.name === "Exposed" || o.name === "SecureContext"), + "Only the [Exposed] and [SecureContext] extended attributes are applicable to namespaces"); + assert_true(this.has_extended_attribute("Exposed"), + "Namespaces must be annotated with the [Exposed] extended attribute"); + }, `${this.name} namespace: extended attributes`); + + const namespaceObject = self[this.name]; + + subsetTestByKey(this.name, test, () => { + const desc = Object.getOwnPropertyDescriptor(self, this.name); + assert_equals(desc.value, namespaceObject, `wrong value for ${this.name} namespace object`); + assert_true(desc.writable, "namespace object should be writable"); + assert_false(desc.enumerable, "namespace object should not be enumerable"); + assert_true(desc.configurable, "namespace object should be configurable"); + assert_false("get" in desc, "namespace object should not have a getter"); + assert_false("set" in desc, "namespace object should not have a setter"); + }, `${this.name} namespace: property descriptor`); + + subsetTestByKey(this.name, test, () => { + assert_true(Object.isExtensible(namespaceObject)); + }, `${this.name} namespace: [[Extensible]] is true`); + + subsetTestByKey(this.name, test, () => { + assert_true(namespaceObject instanceof Object); + + if (this.name === "console") { + // https://console.spec.whatwg.org/#console-namespace + const namespacePrototype = Object.getPrototypeOf(namespaceObject); + assert_equals(Reflect.ownKeys(namespacePrototype).length, 0); + assert_equals(Object.getPrototypeOf(namespacePrototype), Object.prototype); + } else { + assert_equals(Object.getPrototypeOf(namespaceObject), Object.prototype); + } + }, `${this.name} namespace: [[Prototype]] is Object.prototype`); + + subsetTestByKey(this.name, test, () => { + assert_equals(typeof namespaceObject, "object"); + }, `${this.name} namespace: typeof is "object"`); + + subsetTestByKey(this.name, test, () => { + assert_equals( + Object.getOwnPropertyDescriptor(namespaceObject, "length"), + undefined, + "length property must be undefined" + ); + }, `${this.name} namespace: has no length property`); + + subsetTestByKey(this.name, test, () => { + assert_equals( + Object.getOwnPropertyDescriptor(namespaceObject, "name"), + undefined, + "name property must be undefined" + ); + }, `${this.name} namespace: has no name property`); +}; + +IdlNamespace.prototype.test = function () +{ + // If the namespace object is not exposed, only test that. Members can't be + // tested either + if (!this.exposed) { + if (!this.untested) { + subsetTestByKey(this.name, test, function() { + assert_false(this.name in self, this.name + " namespace should not exist"); + }.bind(this), this.name + " namespace: existence and properties of namespace object"); + } + return; + } + + if (!this.untested) { + this.test_self(); + } + + for (const v of Object.values(this.members)) { + switch (v.type) { + + case 'operation': + this.test_member_operation(v); + break; + + case 'attribute': + this.test_member_attribute(v); + break; + + default: + throw 'Invalid namespace member ' + v.name + ': ' + v.type + ' not supported'; + } + }; +}; + +}()); + +/** + * idl_test is a promise_test wrapper that handles the fetching of the IDL, + * avoiding repetitive boilerplate. + * + * @param {String[]} srcs Spec name(s) for source idl files (fetched from + * /interfaces/{name}.idl). + * @param {String[]} deps Spec name(s) for dependency idl files (fetched + * from /interfaces/{name}.idl). Order is important - dependencies from + * each source will only be included if they're already know to be a + * dependency (i.e. have already been seen). + * @param {Function} setup_func Function for extra setup of the idl_array, such + * as adding objects. Do not call idl_array.test() in the setup; it is + * called by this function (idl_test). + */ +function idl_test(srcs, deps, idl_setup_func) { + return promise_test(function (t) { + var idl_array = new IdlArray(); + var setup_error = null; + const validationIgnored = [ + "constructor-member", + "dict-arg-default", + "require-exposed" + ]; + return Promise.all( + srcs.concat(deps).map(globalThis.fetch_spec)) + .then(function(results) { + const astArray = results.map(result => + WebIDL2.parse(result.idl, { sourceName: result.spec }) + ); + test(() => { + const validations = WebIDL2.validate(astArray) + .filter(v => !validationIgnored.includes(v.ruleName)); + if (validations.length) { + const message = validations.map(v => v.message).join("\n\n"); + throw new Error(message); + } + }, "idl_test validation"); + for (var i = 0; i < srcs.length; i++) { + idl_array.internal_add_idls(astArray[i]); + } + for (var i = srcs.length; i < srcs.length + deps.length; i++) { + idl_array.internal_add_dependency_idls(astArray[i]); + } + }) + .then(function() { + if (idl_setup_func) { + return idl_setup_func(idl_array, t); + } + }) + .catch(function(e) { setup_error = e || 'IDL setup failed.'; }) + .then(function () { + var error = setup_error; + try { + idl_array.test(); // Test what we can. + } catch (e) { + // If testing fails hard here, the original setup error + // is more likely to be the real cause. + error = error || e; + } + if (error) { + throw error; + } + }); + }, 'idl_test setup'); +} +globalThis.idl_test = idl_test; + +/** + * fetch_spec is a shorthand for a Promise that fetches the spec's content. + * Note: ShadowRealm-specific implementation in testharness-shadowrealm-inner.js + */ +function fetch_spec(spec) { + var url = '/interfaces/' + spec + '.idl'; + return fetch(url).then(function (r) { + if (!r.ok) { + throw new IdlHarnessError("Error fetching " + url + "."); + } + return r.text(); + }).then(idl => ({ spec, idl })); +} +// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker: diff --git a/test/js/third_party/wpt-streams/resources/webidl2/lib/webidl2.js b/test/js/third_party/wpt-streams/resources/webidl2/lib/webidl2.js new file mode 100644 index 000000000000..bae0b2047595 --- /dev/null +++ b/test/js/third_party/wpt-streams/resources/webidl2/lib/webidl2.js @@ -0,0 +1,4002 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else if(typeof exports === 'object') + exports["WebIDL2"] = factory(); + else + root["WebIDL2"] = factory(); +})(globalThis, () => { +return /******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ([ +/* 0 */, +/* 1 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ parse: () => (/* binding */ parse) +/* harmony export */ }); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); +/* harmony import */ var _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(15); +/* harmony import */ var _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16); +/* harmony import */ var _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8); +/* harmony import */ var _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); +/* harmony import */ var _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(18); +/* harmony import */ var _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(19); +/* harmony import */ var _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(25); +/* harmony import */ var _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(26); +/* harmony import */ var _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(28); +/* harmony import */ var _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(29); +/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(4); +/* harmony import */ var _productions_token_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(10); + + + + + + + + + + + + + + +/** @typedef {'callbackInterface'|'dictionary'|'interface'|'mixin'|'namespace'} ExtendableInterfaces */ +/** @typedef {{ extMembers?: import("./productions/container.js").AllowedMember[]}} Extension */ +/** @typedef {Partial>} Extensions */ + +/** + * Parser options. + * @typedef {Object} ParserOptions + * @property {string} [sourceName] + * @property {boolean} [concrete] + * @property {Function[]} [productions] + * @property {Extensions} [extensions] + */ + +/** + * @param {Tokeniser} tokeniser + * @param {ParserOptions} options + */ +function parseByTokens(tokeniser, options) { + const source = tokeniser.source; + + function error(str) { + tokeniser.error(str); + } + + function consume(...candidates) { + return tokeniser.consume(...candidates); + } + + function callback() { + const callback = consume("callback"); + if (!callback) return; + if (tokeniser.probe("interface")) { + return _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__.CallbackInterface.parse(tokeniser, callback, { + ...options?.extensions?.callbackInterface, + }); + } + return _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__.CallbackFunction.parse(tokeniser, callback); + } + + function interface_(opts) { + const base = consume("interface"); + if (!base) return; + return ( + _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__.Mixin.parse(tokeniser, base, { + ...opts, + ...options?.extensions?.mixin, + }) || + _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__.Interface.parse(tokeniser, base, { + ...opts, + ...options?.extensions?.interface, + }) || + error("Interface has no proper body") + ); + } + + function partial() { + const partial = consume("partial"); + if (!partial) return; + return ( + _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, { + partial, + ...options?.extensions?.dictionary, + }) || + interface_({ partial }) || + _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, { + partial, + ...options?.extensions?.namespace, + }) || + error("Partial doesn't apply to anything") + ); + } + + function definition() { + if (options.productions) { + for (const production of options.productions) { + const result = production(tokeniser); + if (result) { + return result; + } + } + } + + return ( + callback() || + interface_() || + partial() || + _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, options?.extensions?.dictionary) || + _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__.Enum.parse(tokeniser) || + _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__.Typedef.parse(tokeniser) || + _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__.Includes.parse(tokeniser) || + _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, options?.extensions?.namespace) + ); + } + + function definitions() { + if (!source.length) return []; + const defs = []; + while (true) { + const ea = _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__.ExtendedAttributes.parse(tokeniser); + const def = definition(); + if (!def) { + if (ea.length) error("Stray extended attributes"); + break; + } + (0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__.autoParenter)(def).extAttrs = ea; + defs.push(def); + } + const eof = _productions_token_js__WEBPACK_IMPORTED_MODULE_12__.Eof.parse(tokeniser); + if (options.concrete) { + defs.push(eof); + } + return defs; + } + + const res = definitions(); + if (tokeniser.position < source.length) error("Unrecognised tokens"); + return res; +} + +/** + * @param {string} str + * @param {ParserOptions} [options] + */ +function parse(str, options = {}) { + const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__.Tokeniser(str); + if (typeof options.sourceName !== "undefined") { + // @ts-ignore (See Tokeniser.source in supplement.d.ts) + tokeniser.source.name = options.sourceName; + } + return parseByTokens(tokeniser, options); +} + + +/***/ }), +/* 2 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Tokeniser: () => (/* binding */ Tokeniser), +/* harmony export */ WebIDLParseError: () => (/* binding */ WebIDLParseError), +/* harmony export */ argumentNameKeywords: () => (/* binding */ argumentNameKeywords), +/* harmony export */ stringTypes: () => (/* binding */ stringTypes), +/* harmony export */ typeNameKeywords: () => (/* binding */ typeNameKeywords) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); +/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +// These regular expressions use the sticky flag so they will only match at +// the current location (ie. the offset of lastIndex). +const tokenRe = { + // This expression uses a lookahead assertion to catch false matches + // against integers early. + decimal: + /-?(?=[0-9]*\.|[0-9]+[eE])(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/y, + integer: /-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/y, + identifier: /[_-]?[A-Za-z][0-9A-Z_a-z-]*/y, + string: /"[^"]*"/y, + whitespace: /[\t\n\r ]+/y, + comment: /\/\/.*|\/\*[\s\S]*?\*\//y, + other: /[^\t\n\r 0-9A-Za-z]/y, +}; + +const typeNameKeywords = [ + "ArrayBuffer", + "SharedArrayBuffer", + "DataView", + "Int8Array", + "Int16Array", + "Int32Array", + "Uint8Array", + "Uint16Array", + "Uint32Array", + "Uint8ClampedArray", + "BigInt64Array", + "BigUint64Array", + "Float16Array", + "Float32Array", + "Float64Array", + "any", + "object", + "symbol", +]; + +const stringTypes = ["ByteString", "DOMString", "USVString"]; + +const argumentNameKeywords = [ + "async", + "attribute", + "callback", + "const", + "constructor", + "deleter", + "dictionary", + "enum", + "getter", + "includes", + "inherit", + "interface", + "iterable", + "maplike", + "namespace", + "partial", + "required", + "setlike", + "setter", + "static", + "stringifier", + "typedef", + "unrestricted", +]; + +const nonRegexTerminals = [ + "-Infinity", + "FrozenArray", + "Infinity", + "NaN", + "ObservableArray", + "Promise", + "async_iterable", + "async_sequence", + "bigint", + "boolean", + "byte", + "double", + "false", + "float", + "long", + "mixin", + "null", + "octet", + "optional", + "or", + "readonly", + "record", + "sequence", + "short", + "true", + "undefined", + "unsigned", + "void", +].concat(argumentNameKeywords, stringTypes, typeNameKeywords); + +const punctuations = [ + "(", + ")", + ",", + "...", + ":", + ";", + "<", + "=", + ">", + "?", + "*", + "[", + "]", + "{", + "}", +]; + +const reserved = [ + // "constructor" is now a keyword + "_constructor", + "toString", + "_toString", +]; + +/** + * @typedef {ArrayItemType>} Token + * @param {string} str + */ +function tokenise(str) { + const tokens = []; + let lastCharIndex = 0; + let trivia = ""; + let line = 1; + let index = 0; + while (lastCharIndex < str.length) { + const nextChar = str.charAt(lastCharIndex); + let result = -1; + + if (/[\t\n\r ]/.test(nextChar)) { + result = attemptTokenMatch("whitespace", { noFlushTrivia: true }); + } else if (nextChar === "/") { + result = attemptTokenMatch("comment", { noFlushTrivia: true }); + } + + if (result !== -1) { + const currentTrivia = tokens.pop().value; + line += (currentTrivia.match(/\n/g) || []).length; + trivia += currentTrivia; + index -= 1; + } else if (/[-0-9.A-Z_a-z]/.test(nextChar)) { + result = attemptTokenMatch("decimal"); + if (result === -1) { + result = attemptTokenMatch("integer"); + } + if (result === -1) { + result = attemptTokenMatch("identifier"); + const lastIndex = tokens.length - 1; + const token = tokens[lastIndex]; + if (result !== -1) { + if (reserved.includes(token.value)) { + const message = `${(0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)( + token.value, + )} is a reserved identifier and must not be used.`; + throw new WebIDLParseError( + (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(tokens, lastIndex, null, message), + ); + } else if (nonRegexTerminals.includes(token.value)) { + token.type = "inline"; + } + } + } + } else if (nextChar === '"') { + result = attemptTokenMatch("string"); + } + + for (const punctuation of punctuations) { + if (str.startsWith(punctuation, lastCharIndex)) { + tokens.push({ + type: "inline", + value: punctuation, + trivia, + line, + index, + }); + trivia = ""; + lastCharIndex += punctuation.length; + result = lastCharIndex; + break; + } + } + + // other as the last try + if (result === -1) { + result = attemptTokenMatch("other"); + } + if (result === -1) { + throw new Error("Token stream not progressing"); + } + lastCharIndex = result; + index += 1; + } + + // remaining trivia as eof + tokens.push({ + type: "eof", + value: "", + trivia, + line, + index, + }); + + return tokens; + + /** + * @param {keyof typeof tokenRe} type + * @param {object} options + * @param {boolean} [options.noFlushTrivia] + */ + function attemptTokenMatch(type, { noFlushTrivia } = {}) { + const re = tokenRe[type]; + re.lastIndex = lastCharIndex; + const result = re.exec(str); + if (result) { + tokens.push({ type, value: result[0], trivia, line, index }); + if (!noFlushTrivia) { + trivia = ""; + } + return re.lastIndex; + } + return -1; + } +} + +class Tokeniser { + /** + * @param {string} idl + */ + constructor(idl) { + this.source = tokenise(idl); + this.position = 0; + } + + /** + * @param {string} message + * @return {never} + */ + error(message) { + throw new WebIDLParseError( + (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(this.source, this.position, this.current, message), + ); + } + + /** + * @param {string} type + */ + probeKind(type) { + return ( + this.source.length > this.position && + this.source[this.position].type === type + ); + } + + /** + * @param {string} value + */ + probe(value) { + return ( + this.probeKind("inline") && this.source[this.position].value === value + ); + } + + /** + * @param {...string} candidates + */ + consumeKind(...candidates) { + for (const type of candidates) { + if (!this.probeKind(type)) continue; + const token = this.source[this.position]; + this.position++; + return token; + } + } + + /** + * @param {...string} candidates + */ + consume(...candidates) { + if (!this.probeKind("inline")) return; + const token = this.source[this.position]; + for (const value of candidates) { + if (token.value !== value) continue; + this.position++; + return token; + } + } + + /** + * @param {string} value + */ + consumeIdentifier(value) { + if (!this.probeKind("identifier")) { + return; + } + if (this.source[this.position].value !== value) { + return; + } + return this.consumeKind("identifier"); + } + + /** + * @param {number} position + */ + unconsume(position) { + this.position = position; + } +} + +class WebIDLParseError extends Error { + /** + * @param {object} options + * @param {string} options.message + * @param {string} options.bareMessage + * @param {string} options.context + * @param {number} options.line + * @param {*} options.sourceName + * @param {string} options.input + * @param {*[]} options.tokens + */ + constructor({ + message, + bareMessage, + context, + line, + sourceName, + input, + tokens, + }) { + super(message); + + this.name = "WebIDLParseError"; // not to be mangled + this.bareMessage = bareMessage; + this.context = context; + this.line = line; + this.sourceName = sourceName; + this.input = input; + this.tokens = tokens; + } +} + + +/***/ }), +/* 3 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ syntaxError: () => (/* binding */ syntaxError), +/* harmony export */ validationError: () => (/* binding */ validationError) +/* harmony export */ }); +/** + * @param {string} text + */ +function lastLine(text) { + const splitted = text.split("\n"); + return splitted[splitted.length - 1]; +} + +function appendIfExist(base, target) { + let result = base; + if (target) { + result += ` ${target}`; + } + return result; +} + +function contextAsText(node) { + const hierarchy = [node]; + while (node && node.parent) { + const { parent } = node; + hierarchy.unshift(parent); + node = parent; + } + return hierarchy.map((n) => appendIfExist(n.type, n.name)).join(" -> "); +} + +/** + * @typedef {object} WebIDL2ErrorOptions + * @property {"error" | "warning"} [level] + * @property {Function} [autofix] + * @property {string} [ruleName] + * + * @typedef {ReturnType} WebIDLErrorData + * + * @param {string} message error message + * @param {*} position + * @param {*} current + * @param {*} message + * @param {"Syntax" | "Validation"} kind error type + * @param {WebIDL2ErrorOptions=} options + */ +function error( + source, + position, + current, + message, + kind, + { level = "error", autofix, ruleName } = {}, +) { + /** + * @param {number} count + */ + function sliceTokens(count) { + return count > 0 + ? source.slice(position, position + count) + : source.slice(Math.max(position + count, 0), position); + } + + /** + * @param {import("./tokeniser.js").Token[]} inputs + * @param {object} [options] + * @param {boolean} [options.precedes] + * @returns + */ + function tokensToText(inputs, { precedes } = {}) { + const text = inputs.map((t) => t.trivia + t.value).join(""); + const nextToken = source[position]; + if (nextToken.type === "eof") { + return text; + } + if (precedes) { + return text + nextToken.trivia; + } + return text.slice(nextToken.trivia.length); + } + + const maxTokens = 5; // arbitrary but works well enough + const line = + source[position].type !== "eof" + ? source[position].line + : source.length > 1 + ? source[position - 1].line + : 1; + + const precedingLastLine = lastLine( + tokensToText(sliceTokens(-maxTokens), { precedes: true }), + ); + + const subsequentTokens = sliceTokens(maxTokens); + const subsequentText = tokensToText(subsequentTokens); + const subsequentFirstLine = subsequentText.split("\n")[0]; + + const spaced = " ".repeat(precedingLastLine.length) + "^"; + const sourceContext = precedingLastLine + subsequentFirstLine + "\n" + spaced; + + const contextType = kind === "Syntax" ? "since" : "inside"; + const inSourceName = source.name ? ` in ${source.name}` : ""; + const grammaticalContext = + current && current.name + ? `, ${contextType} \`${current.partial ? "partial " : ""}${contextAsText( + current, + )}\`` + : ""; + const context = `${kind} error at line ${line}${inSourceName}${grammaticalContext}:\n${sourceContext}`; + return { + message: `${context} ${message}`, + bareMessage: message, + context, + line, + sourceName: source.name, + level, + ruleName, + autofix, + input: subsequentText, + tokens: subsequentTokens, + }; +} + +/** + * @param {string} message error message + */ +function syntaxError(source, position, current, message) { + return error(source, position, current, message, "Syntax"); +} + +/** + * @param {string} message error message + * @param {WebIDL2ErrorOptions} [options] + */ +function validationError( + token, + current, + ruleName, + message, + options = {}, +) { + options.ruleName = ruleName; + return error( + current.source, + token.index, + current, + message, + "Validation", + options, + ); +} + + +/***/ }), +/* 4 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ argument_list: () => (/* binding */ argument_list), +/* harmony export */ autoParenter: () => (/* binding */ autoParenter), +/* harmony export */ autofixAddExposedWindow: () => (/* binding */ autofixAddExposedWindow), +/* harmony export */ const_data: () => (/* binding */ const_data), +/* harmony export */ const_value: () => (/* binding */ const_value), +/* harmony export */ findLastIndex: () => (/* binding */ findLastIndex), +/* harmony export */ getFirstToken: () => (/* binding */ getFirstToken), +/* harmony export */ getLastIndentation: () => (/* binding */ getLastIndentation), +/* harmony export */ getMemberIndentation: () => (/* binding */ getMemberIndentation), +/* harmony export */ list: () => (/* binding */ list), +/* harmony export */ primitive_type: () => (/* binding */ primitive_type), +/* harmony export */ return_type: () => (/* binding */ return_type), +/* harmony export */ stringifier: () => (/* binding */ stringifier), +/* harmony export */ type_with_extended_attributes: () => (/* binding */ type_with_extended_attributes), +/* harmony export */ unescape: () => (/* binding */ unescape) +/* harmony export */ }); +/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5); +/* harmony import */ var _argument_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(14); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(2); + + + + + + + +/** + * @param {string} identifier + */ +function unescape(identifier) { + return identifier.startsWith("_") ? identifier.slice(1) : identifier; +} + +/** + * Parses comma-separated list + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} args + * @param {Function} args.parser parser function for each item + * @param {boolean} [args.allowDangler] whether to allow dangling comma + * @param {string} [args.listName] the name to be shown on error messages + */ +function list(tokeniser, { parser, allowDangler, listName = "list" }) { + const first = parser(tokeniser); + if (!first) { + return []; + } + first.tokens.separator = tokeniser.consume(","); + const items = [first]; + while (first.tokens.separator) { + const item = parser(tokeniser); + if (!item) { + if (!allowDangler) { + tokeniser.error(`Trailing comma in ${listName}`); + } + break; + } + item.tokens.separator = tokeniser.consume(","); + items.push(item); + if (!item.tokens.separator) break; + } + return items; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function const_value(tokeniser) { + return ( + tokeniser.consumeKind("decimal", "integer") || + tokeniser.consume("true", "false", "Infinity", "-Infinity", "NaN") + ); +} + +/** + * @param {object} token + * @param {string} token.type + * @param {string} token.value + */ +function const_data({ type, value }) { + switch (type) { + case "decimal": + case "integer": + return { type: "number", value }; + case "string": + return { type: "string", value: value.slice(1, -1) }; + } + + switch (value) { + case "true": + case "false": + return { type: "boolean", value: value === "true" }; + case "Infinity": + case "-Infinity": + return { type: "Infinity", negative: value.startsWith("-") }; + case "[": + return { type: "sequence", value: [] }; + case "{": + return { type: "dictionary" }; + default: + return { type: value }; + } +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function primitive_type(tokeniser) { + function integer_type() { + const prefix = tokeniser.consume("unsigned"); + const base = tokeniser.consume("short", "long"); + if (base) { + const postfix = tokeniser.consume("long"); + return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base, postfix } }); + } + if (prefix) tokeniser.error("Failed to parse integer type"); + } + + function decimal_type() { + const prefix = tokeniser.consume("unrestricted"); + const base = tokeniser.consume("float", "double"); + if (base) { + return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base } }); + } + if (prefix) tokeniser.error("Failed to parse float type"); + } + + const { source } = tokeniser; + const num_type = integer_type() || decimal_type(); + if (num_type) return num_type; + const base = tokeniser.consume( + "bigint", + "boolean", + "byte", + "octet", + "undefined", + ); + if (base) { + return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { base } }); + } +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function argument_list(tokeniser) { + return list(tokeniser, { + parser: _argument_js__WEBPACK_IMPORTED_MODULE_1__.Argument.parse, + listName: "arguments list", + }); +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string=} typeName (TODO: See Type.type for more details) + */ +function type_with_extended_attributes(tokeniser, typeName) { + const extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser); + const ret = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName); + if (ret) autoParenter(ret).extAttrs = extAttrs; + return ret; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string=} typeName (TODO: See Type.type for more details) + */ +function return_type(tokeniser, typeName) { + const typ = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName || "return-type"); + if (typ) { + return typ; + } + const voidToken = tokeniser.consume("void"); + if (voidToken) { + const ret = new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ + source: tokeniser.source, + tokens: { base: voidToken }, + }); + ret.type = "return-type"; + return ret; + } +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function stringifier(tokeniser) { + const special = tokeniser.consume("stringifier"); + if (!special) return; + const member = + _attribute_js__WEBPACK_IMPORTED_MODULE_4__.Attribute.parse(tokeniser, { special }) || + _operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse(tokeniser, { special }) || + tokeniser.error("Unterminated stringifier"); + return member; +} + +/** + * @param {string} str + */ +function getLastIndentation(str) { + const lines = str.split("\n"); + // the first line visually binds to the preceding token + if (lines.length) { + const match = lines[lines.length - 1].match(/^\s+/); + if (match) { + return match[0]; + } + } + return ""; +} + +/** + * @param {string} parentTrivia + */ +function getMemberIndentation(parentTrivia) { + const indentation = getLastIndentation(parentTrivia); + const indentCh = indentation.includes("\t") ? "\t" : " "; + return indentation + indentCh; +} + +/** + * @param {import("./interface.js").Interface} def + */ +function autofixAddExposedWindow(def) { + return () => { + if (def.extAttrs.length) { + const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("Exposed=Window,"); + const exposed = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.SimpleExtendedAttribute.parse(tokeniser); + exposed.tokens.separator = tokeniser.consume(","); + const existing = def.extAttrs[0]; + if (!/^\s/.test(existing.tokens.name.trivia)) { + existing.tokens.name.trivia = ` ${existing.tokens.name.trivia}`; + } + def.extAttrs.unshift(exposed); + } else { + autoParenter(def).extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse( + new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("[Exposed=Window]"), + ); + const trivia = def.tokens.base.trivia; + def.extAttrs.tokens.open.trivia = trivia; + def.tokens.base.trivia = `\n${getLastIndentation(trivia)}`; + } + }; +} + +/** + * Get the first syntax token for the given IDL object. + * @param {*} data + */ +function getFirstToken(data) { + if (data.extAttrs.length) { + return data.extAttrs.tokens.open; + } + if (data.type === "operation" && !data.special) { + return getFirstToken(data.idlType); + } + const tokens = Object.values(data.tokens).sort((x, y) => x.index - y.index); + return tokens[0]; +} + +/** + * @template T + * @param {T[]} array + * @param {(item: T) => boolean} predicate + */ +function findLastIndex(array, predicate) { + const index = array.slice().reverse().findIndex(predicate); + if (index === -1) { + return index; + } + return array.length - index - 1; +} + +/** + * Returns a proxy that auto-assign `parent` field. + * @template {Record} T + * @param {T} data + * @param {*} [parent] The object that will be assigned to `parent`. + * If absent, it will be `data` by default. + * @return {T} + */ +function autoParenter(data, parent) { + if (!parent) { + // Defaults to `data` unless specified otherwise. + parent = data; + } + if (!data) { + // This allows `autoParenter(undefined)` which again allows + // `autoParenter(parse())` where the function may return nothing. + return data; + } + const proxy = new Proxy(data, { + get(target, p) { + const value = target[p]; + if (Array.isArray(value) && p !== "source") { + // Wraps the array so that any added items will also automatically + // get their `parent` values. + return autoParenter(value, target); + } + return value; + }, + set(target, p, value) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/47357 + target[p] = value; + if (!value) { + return true; + } else if (Array.isArray(value)) { + // Assigning an array will add `parent` to its items. + for (const item of value) { + if (typeof item.parent !== "undefined") { + item.parent = parent; + } + } + } else if (typeof value.parent !== "undefined") { + value.parent = parent; + } + return true; + }, + }); + return proxy; +} + + +/***/ }), +/* 5 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Type: () => (/* binding */ Type) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3); +/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(8); + + + + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} typeName + */ +function generic_type(tokeniser, typeName) { + const base = tokeniser.consume( + "FrozenArray", + "ObservableArray", + "Promise", + "async_sequence", + "sequence", + "record", + ); + if (!base) { + return; + } + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( + new Type({ source: tokeniser.source, tokens: { base } }), + ); + ret.tokens.open = + tokeniser.consume("<") || + tokeniser.error(`No opening bracket after ${base.value}`); + switch (base.value) { + case "Promise": { + if (tokeniser.probe("[")) + tokeniser.error("Promise type cannot have extended attribute"); + const subtype = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser, typeName) || + tokeniser.error("Missing Promise subtype"); + ret.subtype.push(subtype); + break; + } + case "async_sequence": + case "sequence": + case "FrozenArray": + case "ObservableArray": { + const subtype = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) || + tokeniser.error(`Missing ${base.value} subtype`); + ret.subtype.push(subtype); + break; + } + case "record": { + if (tokeniser.probe("[")) + tokeniser.error("Record key cannot have extended attribute"); + const keyType = + tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes) || + tokeniser.error(`Record key must be one of: ${_tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes.join(", ")}`); + const keyIdlType = new Type({ + source: tokeniser.source, + tokens: { base: keyType }, + }); + keyIdlType.tokens.separator = + tokeniser.consume(",") || + tokeniser.error("Missing comma after record key type"); + keyIdlType.type = typeName; + const valueType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) || + tokeniser.error("Error parsing generic type record"); + ret.subtype.push(keyIdlType, valueType); + break; + } + } + if (!ret.idlType) tokeniser.error(`Error parsing generic type ${base.value}`); + ret.tokens.close = + tokeniser.consume(">") || + tokeniser.error(`Missing closing bracket after ${base.value}`); + return ret.this; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function type_suffix(tokeniser, obj) { + const nullable = tokeniser.consume("?"); + if (nullable) { + obj.tokens.nullable = nullable; + } + if (tokeniser.probe("?")) tokeniser.error("Can't nullable more than once"); +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} typeName + */ +function single_type(tokeniser, typeName) { + let ret = generic_type(tokeniser, typeName) || (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.primitive_type)(tokeniser); + if (!ret) { + const base = + tokeniser.consumeKind("identifier") || + tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes, ..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.typeNameKeywords); + if (!base) { + return; + } + ret = new Type({ source: tokeniser.source, tokens: { base } }); + if (tokeniser.probe("<")) + tokeniser.error(`Unsupported generic type ${base.value}`); + } + if (ret.generic === "Promise" && tokeniser.probe("?")) { + tokeniser.error("Promise type cannot be nullable"); + } + ret.type = typeName || null; + type_suffix(tokeniser, ret); + if (ret.nullable && ret.idlType === "any") + tokeniser.error("Type `any` cannot be made nullable"); + return ret; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} type + */ +function union_type(tokeniser, type) { + const tokens = {}; + tokens.open = tokeniser.consume("("); + if (!tokens.open) return; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Type({ source: tokeniser.source, tokens })); + ret.type = type || null; + while (true) { + const typ = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, type) || + tokeniser.error("No type after open parenthesis or 'or' in union type"); + if (typ.idlType === "any") + tokeniser.error("Type `any` cannot be included in a union type"); + if (typ.generic === "Promise") + tokeniser.error("Type `Promise` cannot be included in a union type"); + ret.subtype.push(typ); + const or = tokeniser.consume("or"); + if (or) { + typ.tokens.separator = or; + } else break; + } + if (ret.idlType.length < 2) { + tokeniser.error( + "At least two types are expected in a union type but found less", + ); + } + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated union type"); + type_suffix(tokeniser, ret); + return ret.this; +} + +class Type extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} typeName + */ + static parse(tokeniser, typeName) { + return single_type(tokeniser, typeName) || union_type(tokeniser, typeName); + } + + constructor({ source, tokens }) { + super({ source, tokens }); + Object.defineProperty(this, "subtype", { value: [], writable: true }); + this.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__.ExtendedAttributes({ source, tokens: {} }); + } + + get generic() { + if (this.subtype.length && this.tokens.base) { + return this.tokens.base.value; + } + return ""; + } + get nullable() { + return Boolean(this.tokens.nullable); + } + get union() { + return Boolean(this.subtype.length) && !this.tokens.base; + } + get idlType() { + if (this.subtype.length) { + return this.subtype; + } + // Adding prefixes/postfixes for "unrestricted float", etc. + const name = [this.tokens.prefix, this.tokens.base, this.tokens.postfix] + .filter((t) => t) + .map((t) => t.value) + .join(" "); + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name); + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + + if (this.idlType === "BufferSource") { + // XXX: For now this is a hack. Consider moving parents' extAttrs into types as the spec says: + // https://webidl.spec.whatwg.org/#idl-annotated-types + for (const extAttrs of [this.extAttrs, this.parent?.extAttrs]) { + for (const extAttr of extAttrs) { + if (extAttr.name !== "AllowShared") { + continue; + } + const message = `\`[AllowShared] BufferSource\` is now replaced with AllowSharedBufferSource.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)( + this.tokens.base, + this, + "migrate-allowshared", + message, + { autofix: replaceAllowShared(this, extAttr, extAttrs) }, + ); + } + } + } + + if (this.idlType === "void") { + const message = `\`void\` is now replaced by \`undefined\`. Refer to the \ +[relevant GitHub issue](https://github.com/whatwg/webidl/issues/60) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(this.tokens.base, this, "replace-void", message, { + autofix: replaceVoid(this), + }); + } + + /* + * If a union is nullable, its subunions cannot include a dictionary + * If not, subunions may include dictionaries if each union is not nullable + */ + const typedef = !this.union && defs.unique.get(this.idlType); + const target = this.union + ? this + : typedef && typedef.type === "typedef" + ? typedef.idlType + : undefined; + if (target && this.nullable) { + // do not allow any dictionary + const { reference } = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__.idlTypeIncludesDictionary)(target, defs) || {}; + if (reference) { + const targetToken = (this.union ? reference : this).tokens.base; + const message = "Nullable union cannot include a dictionary type."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)( + targetToken, + this, + "no-nullable-union-dict", + message, + ); + } + } else { + // allow some dictionary + for (const subtype of this.subtype) { + yield* subtype.validate(defs); + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const type_body = () => { + if (this.union || this.generic) { + return w.ts.wrap([ + w.token(this.tokens.base, w.ts.generic), + w.token(this.tokens.open), + ...this.subtype.map((t) => t.write(w)), + w.token(this.tokens.close), + ]); + } + const firstToken = this.tokens.prefix || this.tokens.base; + const prefix = this.tokens.prefix + ? [this.tokens.prefix.value, w.ts.trivia(this.tokens.base.trivia)] + : []; + const ref = w.reference( + w.ts.wrap([ + ...prefix, + this.tokens.base.value, + w.token(this.tokens.postfix), + ]), + { + unescaped: /** @type {string} (because it's not union) */ ( + this.idlType + ), + context: this, + }, + ); + return w.ts.wrap([w.ts.trivia(firstToken.trivia), ref]); + }; + return w.ts.wrap([ + this.extAttrs.write(w), + type_body(), + w.token(this.tokens.nullable), + w.token(this.tokens.separator), + ]); + } +} + +/** + * @param {Type} type + * @param {import("./extended-attributes.js").SimpleExtendedAttribute} extAttr + * @param {ExtendedAttributes} extAttrs + */ +function replaceAllowShared(type, extAttr, extAttrs) { + return () => { + const index = extAttrs.indexOf(extAttr); + extAttrs.splice(index, 1); + if (!extAttrs.length && type.tokens.base.trivia.match(/^\s$/)) { + type.tokens.base.trivia = ""; // (let's not remove comments) + } + + type.tokens.base.value = "AllowSharedBufferSource"; + }; +} + +/** + * @param {Type} type + */ +function replaceVoid(type) { + return () => { + type.tokens.base.value = "undefined"; + }; +} + + +/***/ }), +/* 6 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Base: () => (/* binding */ Base) +/* harmony export */ }); +class Base { + /** + * @param {object} initializer + * @param {Base["source"]} initializer.source + * @param {Base["tokens"]} initializer.tokens + */ + constructor({ source, tokens }) { + Object.defineProperties(this, { + source: { value: source }, + tokens: { value: tokens, writable: true }, + parent: { value: null, writable: true }, + this: { value: this }, // useful when escaping from proxy + }); + } + + toJSON() { + const json = { type: undefined, name: undefined, inheritance: undefined }; + let proto = this; + while (proto !== Object.prototype) { + const descMap = Object.getOwnPropertyDescriptors(proto); + for (const [key, value] of Object.entries(descMap)) { + if (value.enumerable || value.get) { + // @ts-ignore - allow indexing here + json[key] = this[key]; + } + } + proto = Object.getPrototypeOf(proto); + } + return json; + } +} + + +/***/ }), +/* 7 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ dictionaryIncludesRequiredField: () => (/* binding */ dictionaryIncludesRequiredField), +/* harmony export */ idlTypeIncludesDictionary: () => (/* binding */ idlTypeIncludesDictionary), +/* harmony export */ idlTypeIncludesEnforceRange: () => (/* binding */ idlTypeIncludesEnforceRange) +/* harmony export */ }); +/** + * @typedef {import("../validator.js").Definitions} Definitions + * @typedef {import("../productions/dictionary.js").Dictionary} Dictionary + * @typedef {import("../../lib/productions/type").Type} Type + * + * @param {Type} idlType + * @param {Definitions} defs + * @param {object} [options] + * @param {boolean} [options.useNullableInner] use when the input idlType is nullable and you want to use its inner type + * @return {{ reference: *, dictionary: Dictionary }} the type reference that ultimately includes dictionary. + */ +function idlTypeIncludesDictionary( + idlType, + defs, + { useNullableInner } = {}, +) { + if (!idlType.union) { + const def = defs.unique.get(idlType.idlType); + if (!def) { + return; + } + if (def.type === "typedef") { + const { typedefIncludesDictionary } = defs.cache; + if (typedefIncludesDictionary.has(def)) { + // Note that this also halts when it met indeterminate state + // to prevent infinite recursion + return typedefIncludesDictionary.get(def); + } + defs.cache.typedefIncludesDictionary.set(def, undefined); // indeterminate state + const result = idlTypeIncludesDictionary(def.idlType, defs); + defs.cache.typedefIncludesDictionary.set(def, result); + if (result) { + return { + reference: idlType, + dictionary: result.dictionary, + }; + } + } + if (def.type === "dictionary" && (useNullableInner || !idlType.nullable)) { + return { + reference: idlType, + dictionary: def, + }; + } + } + for (const subtype of idlType.subtype) { + const result = idlTypeIncludesDictionary(subtype, defs); + if (result) { + if (subtype.union) { + return result; + } + return { + reference: subtype, + dictionary: result.dictionary, + }; + } + } +} + +/** + * @param {Dictionary} dict dictionary type + * @param {Definitions} defs + * @return {boolean} + */ +function dictionaryIncludesRequiredField(dict, defs) { + if (defs.cache.dictionaryIncludesRequiredField.has(dict)) { + return defs.cache.dictionaryIncludesRequiredField.get(dict); + } + // Set cached result to indeterminate to short-circuit circular definitions. + // The final result will be updated to true or false. + defs.cache.dictionaryIncludesRequiredField.set(dict, undefined); + let result = dict.members.some((field) => field.required); + if (!result && dict.inheritance) { + const superdict = defs.unique.get(dict.inheritance); + if (!superdict) { + // Assume required members in the supertype if it is unknown. + result = true; + } else if (dictionaryIncludesRequiredField(superdict, defs)) { + result = true; + } + } + defs.cache.dictionaryIncludesRequiredField.set(dict, result); + return result; +} + +/** + * For now this only checks the most frequent cases: + * 1. direct inclusion of [EnforceRange] + * 2. typedef of that + * + * More complex cases with dictionaries and records are not covered yet. + * + * @param {Type} idlType + * @param {Definitions} defs + */ +function idlTypeIncludesEnforceRange(idlType, defs) { + if (idlType.union) { + // TODO: This should ideally be checked too + return false; + } + + if (idlType.extAttrs.some((e) => e.name === "EnforceRange")) { + return true; + } + + const def = defs.unique.get(idlType.idlType); + if (def?.type !== "typedef") { + return false; + } + + return def.idlType.extAttrs.some((e) => e.name === "EnforceRange"); +} + + +/***/ }), +/* 8 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ ExtendedAttributeParameters: () => (/* binding */ ExtendedAttributeParameters), +/* harmony export */ ExtendedAttributes: () => (/* binding */ ExtendedAttributes), +/* harmony export */ SimpleExtendedAttribute: () => (/* binding */ SimpleExtendedAttribute) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _array_base_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9); +/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(3); + + + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} tokenName + */ +function tokens(tokeniser, tokenName) { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, { + parser: _token_js__WEBPACK_IMPORTED_MODULE_2__.WrappedToken.parser(tokeniser, tokenName), + listName: tokenName + " list", + }); +} + +const extAttrValueSyntax = ["identifier", "decimal", "integer", "string"]; + +const shouldBeLegacyPrefixed = [ + "NoInterfaceObject", + "LenientSetter", + "LenientThis", + "TreatNonObjectAsNull", + "Unforgeable", +]; + +const renamedLegacies = new Map([ + .../** @type {[string, string][]} */ ( + shouldBeLegacyPrefixed.map((name) => [name, `Legacy${name}`]) + ), + ["NamedConstructor", "LegacyFactoryFunction"], + ["OverrideBuiltins", "LegacyOverrideBuiltIns"], + ["TreatNullAs", "LegacyNullToEmptyString"], +]); + +/** + * This will allow a set of extended attribute values to be parsed. + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function extAttrListItems(tokeniser) { + for (const syntax of extAttrValueSyntax) { + const toks = tokens(tokeniser, syntax); + if (toks.length) { + return toks; + } + } + tokeniser.error( + `Expected identifiers, strings, decimals, or integers but none found`, + ); +} + +class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const tokens = { assign: tokeniser.consume("=") }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( + new ExtendedAttributeParameters({ source: tokeniser.source, tokens }), + ); + ret.list = []; + if (tokens.assign) { + tokens.asterisk = tokeniser.consume("*"); + if (tokens.asterisk) { + return ret.this; + } + tokens.secondaryName = tokeniser.consumeKind(...extAttrValueSyntax); + } + tokens.open = tokeniser.consume("("); + if (tokens.open) { + ret.list = ret.rhsIsList + ? // [Exposed=(Window,Worker)] + extAttrListItems(tokeniser) + : // [LegacyFactoryFunction=Audio(DOMString src)] or [Constructor(DOMString str)] + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || + tokeniser.error("Unexpected token in extended attribute argument list"); + } else if (tokens.assign && !tokens.secondaryName) { + tokeniser.error("No right hand side to extended attribute assignment"); + } + return ret.this; + } + + get rhsIsList() { + return ( + this.tokens.assign && !this.tokens.asterisk && !this.tokens.secondaryName + ); + } + + get rhsType() { + if (this.rhsIsList) { + return this.list[0].tokens.value.type + "-list"; + } + if (this.tokens.asterisk) { + return "*"; + } + if (this.tokens.secondaryName) { + return this.tokens.secondaryName.type; + } + return null; + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { rhsType } = this; + return w.ts.wrap([ + w.token(this.tokens.assign), + w.token(this.tokens.asterisk), + w.reference_token(this.tokens.secondaryName, this.parent), + w.token(this.tokens.open), + ...this.list.map((p) => { + return rhsType === "identifier-list" + ? w.identifier(p, this.parent) + : p.write(w); + }), + w.token(this.tokens.close), + ]); + } +} + +class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const name = tokeniser.consumeKind("identifier"); + if (name) { + return new SimpleExtendedAttribute({ + source: tokeniser.source, + tokens: { name }, + params: ExtendedAttributeParameters.parse(tokeniser), + }); + } + } + + constructor({ source, tokens, params }) { + super({ source, tokens }); + params.parent = this; + Object.defineProperty(this, "params", { value: params }); + } + + get type() { + return "extended-attribute"; + } + get name() { + return this.tokens.name.value; + } + get rhs() { + const { rhsType: type, tokens, list } = this.params; + if (!type) { + return null; + } + const value = this.params.rhsIsList + ? list + : this.params.tokens.secondaryName + ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value) + : null; + return { type, value }; + } + get arguments() { + const { rhsIsList, list } = this.params; + if (!list || rhsIsList) { + return []; + } + return list; + } + + *validate(defs) { + const { name } = this; + if (name === "LegacyNoInterfaceObject") { + const message = `\`[LegacyNoInterfaceObject]\` extended attribute is an \ +undesirable feature that may be removed from Web IDL in the future. Refer to the \ +[relevant upstream PR](https://github.com/whatwg/webidl/pull/609) for more \ +information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)( + this.tokens.name, + this, + "no-nointerfaceobject", + message, + { level: "warning" }, + ); + } else if (renamedLegacies.has(name)) { + const message = `\`[${name}]\` extended attribute is a legacy feature \ +that is now renamed to \`[${renamedLegacies.get(name)}]\`. Refer to the \ +[relevant upstream PR](https://github.com/whatwg/webidl/pull/870) for more \ +information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)(this.tokens.name, this, "renamed-legacy", message, { + level: "warning", + autofix: renameLegacyExtendedAttribute(this), + }); + } + for (const arg of this.arguments) { + yield* arg.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + w.ts.trivia(this.tokens.name.trivia), + w.ts.extendedAttribute( + w.ts.wrap([ + w.ts.extendedAttributeReference(this.name), + this.params.write(w), + ]), + ), + w.token(this.tokens.separator), + ]); + } +} + +/** + * @param {SimpleExtendedAttribute} extAttr + */ +function renameLegacyExtendedAttribute(extAttr) { + return () => { + const { name } = extAttr; + extAttr.tokens.name.value = renamedLegacies.get(name); + if (name === "TreatNullAs") { + extAttr.params.tokens = {}; + } + }; +} + +// Note: we parse something simpler than the official syntax. It's all that ever +// seems to be used +class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.ArrayBase { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const tokens = {}; + tokens.open = tokeniser.consume("["); + const ret = new ExtendedAttributes({ source: tokeniser.source, tokens }); + if (!tokens.open) return ret; + ret.push( + ...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, { + parser: SimpleExtendedAttribute.parse, + listName: "extended attribute", + }), + ); + tokens.close = + tokeniser.consume("]") || + tokeniser.error( + "Expected a closing token for the extended attribute list", + ); + if (!ret.length) { + tokeniser.unconsume(tokens.close.index); + tokeniser.error("An extended attribute list must not be empty"); + } + if (tokeniser.probe("[")) { + tokeniser.error( + "Illegal double extended attribute lists, consider merging them", + ); + } + return ret; + } + + *validate(defs) { + for (const extAttr of this) { + yield* extAttr.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + if (!this.length) return ""; + return w.ts.wrap([ + w.token(this.tokens.open), + ...this.map((ea) => ea.write(w)), + w.token(this.tokens.close), + ]); + } +} + + +/***/ }), +/* 9 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ ArrayBase: () => (/* binding */ ArrayBase) +/* harmony export */ }); +class ArrayBase extends Array { + constructor({ source, tokens }) { + super(); + Object.defineProperties(this, { + source: { value: source }, + tokens: { value: tokens }, + parent: { value: null, writable: true }, + }); + } +} + + +/***/ }), +/* 10 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Eof: () => (/* binding */ Eof), +/* harmony export */ WrappedToken: () => (/* binding */ WrappedToken) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class WrappedToken extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} type + */ + static parser(tokeniser, type) { + return () => { + const value = tokeniser.consumeKind(type); + if (value) { + return new WrappedToken({ + source: tokeniser.source, + tokens: { value }, + }); + } + }; + } + + get type() { + return this.tokens.value.type; + } + + get value() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.value.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + w.token(this.tokens.value), + w.token(this.tokens.separator), + ]); + } +} + +class Eof extends WrappedToken { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const value = tokeniser.consumeKind("eof"); + if (value) { + return new Eof({ source: tokeniser.source, tokens: { value } }); + } + } + + get type() { + return "eof"; + } +} + + +/***/ }), +/* 11 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Argument: () => (/* binding */ Argument) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(2); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(3); +/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7); + + + + + + + + +class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const start_position = tokeniser.position; + /** @type {Base["tokens"]} */ + const tokens = {}; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( + new Argument({ source: tokeniser.source, tokens }), + ); + ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser); + tokens.optional = tokeniser.consume("optional"); + ret.idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "argument-type"); + if (!ret.idlType) { + return tokeniser.unconsume(start_position); + } + if (!tokens.optional) { + tokens.variadic = tokeniser.consume("..."); + } + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.argumentNameKeywords); + if (!tokens.name) { + return tokeniser.unconsume(start_position); + } + ret.default = tokens.optional ? _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(tokeniser) : null; + return ret.this; + } + + get type() { + return "argument"; + } + get optional() { + return !!this.tokens.optional; + } + get variadic() { + return !!this.tokens.variadic; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value); + } + + /** + * @param {import("../validator.js").Definitions} defs + */ + *validate(defs) { + yield* this.extAttrs.validate(defs); + yield* this.idlType.validate(defs); + const result = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.idlTypeIncludesDictionary)(this.idlType, defs, { + useNullableInner: true, + }); + if (result) { + if (this.idlType.nullable) { + const message = `Dictionary arguments cannot be nullable.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)( + this.tokens.name, + this, + "no-nullable-dict-arg", + message, + ); + } else if (!this.optional) { + if ( + this.parent && + !(0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.dictionaryIncludesRequiredField)(result.dictionary, defs) && + isLastRequiredArgument(this) + ) { + const message = `Dictionary argument must be optional if it has no required fields`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)( + this.tokens.name, + this, + "dict-arg-optional", + message, + { + autofix: autofixDictionaryArgumentOptionality(this), + }, + ); + } + } else if (!this.default) { + const message = `Optional dictionary arguments must have a default value of \`{}\`.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)( + this.tokens.name, + this, + "dict-arg-default", + message, + { + autofix: autofixOptionalDictionaryDefaultValue(this), + }, + ); + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.optional), + w.ts.type(this.idlType.write(w)), + w.token(this.tokens.variadic), + w.name_token(this.tokens.name, { data: this }), + this.default ? this.default.write(w) : "", + w.token(this.tokens.separator), + ]); + } +} + +/** + * @param {Argument} arg + */ +function isLastRequiredArgument(arg) { + const list = arg.parent.arguments || arg.parent.list; + const index = list.indexOf(arg); + const requiredExists = list.slice(index + 1).some((a) => !a.optional); + return !requiredExists; +} + +/** + * @param {Argument} arg + */ +function autofixDictionaryArgumentOptionality(arg) { + return () => { + const firstToken = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.getFirstToken)(arg.idlType); + arg.tokens.optional = { + ...firstToken, + type: "optional", + value: "optional", + }; + firstToken.trivia = " "; + autofixOptionalDictionaryDefaultValue(arg)(); + }; +} + +/** + * @param {Argument} arg + */ +function autofixOptionalDictionaryDefaultValue(arg) { + return () => { + arg.default = _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(new _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.Tokeniser(" = {}")); + }; +} + + +/***/ }), +/* 12 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Default: () => (/* binding */ Default) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Default extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const assign = tokeniser.consume("="); + if (!assign) { + return null; + } + const def = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_value)(tokeniser) || + tokeniser.consumeKind("string") || + tokeniser.consume("null", "[", "{") || + tokeniser.error("No value for default"); + const expression = [def]; + if (def.value === "[") { + const close = + tokeniser.consume("]") || + tokeniser.error("Default sequence value must be empty"); + expression.push(close); + } else if (def.value === "{") { + const close = + tokeniser.consume("}") || + tokeniser.error("Default dictionary value must be empty"); + expression.push(close); + } + return new Default({ + source: tokeniser.source, + tokens: { assign }, + expression, + }); + } + + constructor({ source, tokens, expression }) { + super({ source, tokens }); + expression.parent = this; + Object.defineProperty(this, "expression", { value: expression }); + } + + get type() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).type; + } + get value() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).value; + } + get negative() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).negative; + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + w.token(this.tokens.assign), + ...this.expression.map((t) => w.token(t)), + ]); + } +} + + +/***/ }), +/* 13 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Operation: () => (/* binding */ Operation) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); + + + + +class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {import("../tokeniser.js").Token} [options.special] + * @param {import("../tokeniser.js").Token} [options.regular] + */ + static parse(tokeniser, { special, regular } = {}) { + const tokens = { special }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( + new Operation({ source: tokeniser.source, tokens }), + ); + if (special && special.value === "stringifier") { + tokens.termination = tokeniser.consume(";"); + if (tokens.termination) { + ret.arguments = []; + return ret; + } + } + if (!special && !regular) { + tokens.special = tokeniser.consume("getter", "setter", "deleter"); + } + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Missing return type"); + tokens.name = + tokeniser.consumeKind("identifier") || tokeniser.consume("includes"); + tokens.open = + tokeniser.consume("(") || tokeniser.error("Invalid operation"); + ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated operation"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated operation, expected `;`"); + return ret.this; + } + + get type() { + return "operation"; + } + get name() { + const { name } = this.tokens; + if (!name) { + return ""; + } + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name.value); + } + get special() { + if (!this.tokens.special) { + return ""; + } + return this.tokens.special.value; + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + if (!this.name && ["", "static"].includes(this.special)) { + const message = `Regular or static operations must have both a return type and an identifier.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)(this.tokens.open, this, "incomplete-op", message); + } + if (this.idlType) { + if (this.idlType.generic === "async_sequence") { + const message = `async_sequence types cannot be returned by an operation.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)( + this.idlType.tokens.base, + this, + "async-sequence-idl-to-js", + message, + ); + } + yield* this.idlType.validate(defs); + } + for (const argument of this.arguments) { + yield* argument.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + const body = this.idlType + ? [ + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + w.token(this.tokens.open), + w.ts.wrap(this.arguments.map((arg) => arg.write(w))), + w.token(this.tokens.close), + ] + : []; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + this.tokens.name + ? w.token(this.tokens.special) + : w.token(this.tokens.special, w.ts.nameless, { data: this, parent }), + ...body, + w.token(this.tokens.termination), + ]), + { data: this, parent }, + ); + } +} + + +/***/ }), +/* 14 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Attribute: () => (/* binding */ Attribute) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); +/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); + + + + + +class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {import("../tokeniser.js").Token} [options.special] + * @param {boolean} [options.noInherit] + * @param {boolean} [options.readonly] + */ + static parse( + tokeniser, + { special, noInherit = false, readonly = false } = {}, + ) { + const start_position = tokeniser.position; + const tokens = { special }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( + new Attribute({ source: tokeniser.source, tokens }), + ); + if (!special && !noInherit) { + tokens.special = tokeniser.consume("inherit"); + } + if (ret.special === "inherit" && tokeniser.probe("readonly")) { + tokeniser.error("Inherited attributes cannot be read-only"); + } + tokens.readonly = tokeniser.consume("readonly"); + if (readonly && !tokens.readonly && tokeniser.probe("attribute")) { + tokeniser.error("Attributes must be readonly in this context"); + } + tokens.base = tokeniser.consume("attribute"); + if (!tokens.base) { + tokeniser.unconsume(start_position); + return; + } + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "attribute-type") || + tokeniser.error("Attribute lacks a type"); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.consume("async", "required") || + tokeniser.error("Attribute lacks a name"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated attribute, expected `;`"); + return ret.this; + } + + get type() { + return "attribute"; + } + get special() { + if (!this.tokens.special) { + return ""; + } + return this.tokens.special.value; + } + get readonly() { + return !!this.tokens.readonly; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value); + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + yield* this.idlType.validate(defs); + + if ( + ["async_sequence", "sequence", "record"].includes(this.idlType.generic) + ) { + const message = `Attributes cannot accept ${this.idlType.generic} types.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + this.tokens.name, + this, + "attr-invalid-type", + message, + ); + } + + { + const { reference } = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesDictionary)(this.idlType, defs) || {}; + if (reference) { + const targetToken = (this.idlType.union ? reference : this.idlType) + .tokens.base; + const message = "Attributes cannot accept dictionary types."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(targetToken, this, "attr-invalid-type", message); + } + } + + if (this.readonly) { + if ((0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesEnforceRange)(this.idlType, defs)) { + const targetToken = this.idlType.tokens.base; + const message = + "Readonly attributes cannot accept [EnforceRange] extended attribute."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(targetToken, this, "attr-invalid-type", message); + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.special), + w.token(this.tokens.readonly), + w.token(this.tokens.base), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + w.token(this.tokens.termination), + ]), + { data: this, parent }, + ); + } +} + + +/***/ }), +/* 15 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Enum: () => (/* binding */ Enum), +/* harmony export */ EnumValue: () => (/* binding */ EnumValue) +/* harmony export */ }); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); +/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6); + + + + +class EnumValue extends _token_js__WEBPACK_IMPORTED_MODULE_1__.WrappedToken { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const value = tokeniser.consumeKind("string"); + if (value) { + return new EnumValue({ source: tokeniser.source, tokens: { value } }); + } + } + + get type() { + return "enum-value"; + } + get value() { + return super.value.slice(1, -1); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.wrap([ + w.ts.trivia(this.tokens.value.trivia), + w.ts.definition( + w.ts.wrap(['"', w.ts.name(this.value, { data: this, parent }), '"']), + { data: this, parent }, + ), + w.token(this.tokens.separator), + ]); + } +} + +class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + tokens.base = tokeniser.consume("enum"); + if (!tokens.base) { + return; + } + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("No name for enum"); + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.autoParenter)(new Enum({ source: tokeniser.source, tokens })); + tokeniser.current = ret.this; + tokens.open = tokeniser.consume("{") || tokeniser.error("Bodyless enum"); + ret.values = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.list)(tokeniser, { + parser: EnumValue.parse, + allowDangler: true, + listName: "enumeration", + }); + if (tokeniser.probeKind("string")) { + tokeniser.error("No comma between enum values"); + } + tokens.close = + tokeniser.consume("}") || tokeniser.error("Unexpected value in enum"); + if (!ret.values.length) { + tokeniser.error("No value in enum"); + } + tokens.termination = + tokeniser.consume(";") || tokeniser.error("No semicolon after enum"); + return ret.this; + } + + get type() { + return "enum"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.unescape)(this.tokens.name.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.name_token(this.tokens.name, { data: this }), + w.token(this.tokens.open), + w.ts.wrap(this.values.map((v) => v.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this }, + ); + } +} + + +/***/ }), +/* 16 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Includes: () => (/* binding */ Includes) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const target = tokeniser.consumeKind("identifier"); + if (!target) { + return; + } + const tokens = { target }; + tokens.includes = tokeniser.consume("includes"); + if (!tokens.includes) { + tokeniser.unconsume(target.index); + return; + } + tokens.mixin = + tokeniser.consumeKind("identifier") || + tokeniser.error("Incomplete includes statement"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("No terminating ; for includes statement"); + return new Includes({ source: tokeniser.source, tokens }); + } + + get type() { + return "includes"; + } + get target() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.target.value); + } + get includes() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.mixin.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.reference_token(this.tokens.target, this), + w.token(this.tokens.includes), + w.reference_token(this.tokens.mixin, this), + w.token(this.tokens.termination), + ]), + { data: this }, + ); + } +} + + +/***/ }), +/* 17 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Typedef: () => (/* binding */ Typedef) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Typedef extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Typedef({ source: tokeniser.source, tokens })); + tokens.base = tokeniser.consume("typedef"); + if (!tokens.base) { + return; + } + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "typedef-type") || + tokeniser.error("Typedef lacks a type"); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Typedef lacks a name"); + tokeniser.current = ret.this; + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated typedef, expected `;`"); + return ret.this; + } + + get type() { + return "typedef"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value); + } + + *validate(defs) { + yield* this.idlType.validate(defs); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this }), + w.token(this.tokens.termination), + ]), + { data: this }, + ); + } +} + + +/***/ }), +/* 18 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ CallbackFunction: () => (/* binding */ CallbackFunction) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); + + + + +class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser, base) { + const tokens = { base }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( + new CallbackFunction({ source: tokeniser.source, tokens }), + ); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Callback lacks a name"); + tokeniser.current = ret.this; + tokens.assign = + tokeniser.consume("=") || tokeniser.error("Callback lacks an assignment"); + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Callback lacks a return type"); + tokens.open = + tokeniser.consume("(") || + tokeniser.error("Callback lacks parentheses for arguments"); + ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated callback"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated callback, expected `;`"); + return ret.this; + } + + get type() { + return "callback"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value); + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + for (const arg of this.arguments) { + yield* arg.validate(defs); + if (arg.idlType.generic === "async_sequence") { + const message = `async_sequence types cannot be returned as a callback argument.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)( + arg.tokens.name, + arg, + "async-sequence-idl-to-js", + message, + ); + } + } + yield* this.idlType.validate(defs); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.name_token(this.tokens.name, { data: this }), + w.token(this.tokens.assign), + w.ts.type(this.idlType.write(w)), + w.token(this.tokens.open), + ...this.arguments.map((arg) => arg.write(w)), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this }, + ); + } +} + + +/***/ }), +/* 19 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Interface: () => (/* binding */ Interface) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(21); +/* harmony import */ var _iterable_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(22); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3); +/* harmony import */ var _validators_interface_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(23); +/* harmony import */ var _constructor_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(24); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(2); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(8); + + + + + + + + + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function static_member(tokeniser) { + const special = tokeniser.consume("static"); + if (!special) return; + const member = + _attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse(tokeniser, { special }) || + _operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse(tokeniser, { special }) || + tokeniser.error("No body in static member"); + return member; +} + +class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {import("../tokeniser.js").Token} base + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token|null} [options.partial] + */ + static parse(tokeniser, base, { extMembers = [], partial = null } = {}) { + const tokens = { partial, base }; + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Interface({ source: tokeniser.source, tokens }), + { + inheritable: !partial, + allowedMembers: [ + ...extMembers, + [_constant_js__WEBPACK_IMPORTED_MODULE_3__.Constant.parse], + [_constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse], + [static_member], + [_helpers_js__WEBPACK_IMPORTED_MODULE_5__.stringifier], + [_iterable_js__WEBPACK_IMPORTED_MODULE_4__.IterableLike.parse], + [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse], + [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse], + ], + }, + ); + } + + get type() { + return "interface"; + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + if ( + !this.partial && + this.extAttrs.every((extAttr) => extAttr.name !== "Exposed") + ) { + const message = `Interfaces must have \`[Exposed]\` extended attribute. \ +To fix, add, for example, \`[Exposed=Window]\`. Please also consider carefully \ +if your interface should also be exposed in a Worker scope. Refer to the \ +[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + this.tokens.name, + this, + "require-exposed", + message, + { + autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autofixAddExposedWindow)(this), + }, + ); + } + const oldConstructors = this.extAttrs.filter( + (extAttr) => extAttr.name === "Constructor", + ); + for (const constructor of oldConstructors) { + const message = `Constructors should now be represented as a \`constructor()\` operation on the interface \ +instead of \`[Constructor]\` extended attribute. Refer to the \ +[WebIDL spec section on constructor operations](https://heycam.github.io/webidl/#idl-constructors) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + constructor.tokens.name, + this, + "constructor-member", + message, + { + autofix: autofixConstructor(this, constructor), + }, + ); + } + + const isGlobal = this.extAttrs.some((extAttr) => extAttr.name === "Global"); + if (isGlobal) { + const factoryFunctions = this.extAttrs.filter( + (extAttr) => extAttr.name === "LegacyFactoryFunction", + ); + for (const named of factoryFunctions) { + const message = `Interfaces marked as \`[Global]\` cannot have factory functions.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + named.tokens.name, + this, + "no-constructible-global", + message, + ); + } + + const constructors = this.members.filter( + (member) => member.type === "constructor", + ); + for (const named of constructors) { + const message = `Interfaces marked as \`[Global]\` cannot have constructors.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + named.tokens.base, + this, + "no-constructible-global", + message, + ); + } + } + + yield* super.validate(defs); + if (!this.partial) { + yield* (0,_validators_interface_js__WEBPACK_IMPORTED_MODULE_7__.checkInterfaceMemberDuplication)(defs, this); + } + } +} + +function autofixConstructor(interfaceDef, constructorExtAttr) { + interfaceDef = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(interfaceDef); + return () => { + const indentation = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)( + interfaceDef.extAttrs.tokens.open.trivia, + ); + const memberIndent = interfaceDef.members.length + ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)((0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getFirstToken)(interfaceDef.members[0]).trivia) + : (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getMemberIndentation)(indentation); + const constructorOp = _constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse( + new _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__.Tokeniser(`\n${memberIndent}constructor();`), + ); + constructorOp.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__.ExtendedAttributes({ + source: interfaceDef.source, + tokens: {}, + }); + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(constructorOp).arguments = constructorExtAttr.arguments; + + const existingIndex = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.findLastIndex)( + interfaceDef.members, + (m) => m.type === "constructor", + ); + interfaceDef.members.splice(existingIndex + 1, 0, constructorOp); + + const { close } = interfaceDef.tokens; + if (!close.trivia.includes("\n")) { + close.trivia += `\n${indentation}`; + } + + const { extAttrs } = interfaceDef; + const index = extAttrs.indexOf(constructorExtAttr); + const removed = extAttrs.splice(index, 1); + if (!extAttrs.length) { + extAttrs.tokens.open = extAttrs.tokens.close = undefined; + } else if (extAttrs.length === index) { + extAttrs[index - 1].tokens.separator = undefined; + } else if (!extAttrs[index].tokens.name.trivia.trim()) { + extAttrs[index].tokens.name.trivia = removed[0].tokens.name.trivia; + } + }; +} + + +/***/ }), +/* 20 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Container: () => (/* binding */ Container) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function inheritance(tokeniser) { + const colon = tokeniser.consume(":"); + if (!colon) { + return {}; + } + const inheritance = + tokeniser.consumeKind("identifier") || + tokeniser.error("Inheritance lacks a type"); + return { colon, inheritance }; +} + +/** + * Parser callback. + * @callback ParserCallback + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {...*} args + */ + +/** + * A parser callback and optional option object. + * @typedef AllowedMember + * @type {[ParserCallback, object?]} + */ + +class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {*} instance TODO: This should be {T extends Container}, but see https://github.com/microsoft/TypeScript/issues/4628 + * @param {*} args + */ + static parse(tokeniser, instance, { inheritable, allowedMembers }) { + const { tokens, type } = instance; + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error(`Missing name in ${type}`); + tokeniser.current = instance; + instance = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(instance); + if (inheritable) { + Object.assign(tokens, inheritance(tokeniser)); + } + tokens.open = tokeniser.consume("{") || tokeniser.error(`Bodyless ${type}`); + instance.members = []; + while (true) { + tokens.close = tokeniser.consume("}"); + if (tokens.close) { + tokens.termination = + tokeniser.consume(";") || + tokeniser.error(`Missing semicolon after ${type}`); + return instance.this; + } + const ea = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__.ExtendedAttributes.parse(tokeniser); + let mem; + for (const [parser, ...args] of allowedMembers) { + mem = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(parser(tokeniser, ...args)); + if (mem) { + break; + } + } + if (!mem) { + tokeniser.error("Unknown member"); + } + mem.extAttrs = ea; + instance.members.push(mem.this); + } + } + + get partial() { + return !!this.tokens.partial; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value); + } + get inheritance() { + if (!this.tokens.inheritance) { + return null; + } + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.inheritance.value); + } + + *validate(defs) { + for (const member of this.members) { + if (member.validate) { + yield* member.validate(defs); + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const inheritance = () => { + if (!this.tokens.inheritance) { + return ""; + } + return w.ts.wrap([ + w.token(this.tokens.colon), + w.ts.trivia(this.tokens.inheritance.trivia), + w.ts.inheritance( + w.reference(this.tokens.inheritance.value, { context: this }), + ), + ]); + }; + + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.callback), + w.token(this.tokens.partial), + w.token(this.tokens.base), + w.token(this.tokens.mixin), + w.name_token(this.tokens.name, { data: this }), + inheritance(), + w.token(this.tokens.open), + w.ts.wrap(this.members.map((m) => m.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this }, + ); + } +} + + +/***/ }), +/* 21 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Constant: () => (/* binding */ Constant) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); + + + + +class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + tokens.base = tokeniser.consume("const"); + if (!tokens.base) { + return; + } + let idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.primitive_type)(tokeniser); + if (!idlType) { + const base = + tokeniser.consumeKind("identifier") || + tokeniser.error("Const lacks a type"); + idlType = new _type_js__WEBPACK_IMPORTED_MODULE_1__.Type({ source: tokeniser.source, tokens: { base } }); + } + if (tokeniser.probe("?")) { + tokeniser.error("Unexpected nullable constant type"); + } + idlType.type = "const-type"; + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Const lacks a name"); + tokens.assign = + tokeniser.consume("=") || tokeniser.error("Const lacks value assignment"); + tokens.value = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_value)(tokeniser) || tokeniser.error("Const lacks a value"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated const, expected `;`"); + const ret = new Constant({ source: tokeniser.source, tokens }); + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(ret).idlType = idlType; + return ret; + } + + get type() { + return "const"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value); + } + get value() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_data)(this.tokens.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + w.token(this.tokens.assign), + w.token(this.tokens.value), + w.token(this.tokens.termination), + ]), + { data: this, parent }, + ); + } +} + + +/***/ }), +/* 22 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ IterableLike: () => (/* binding */ IterableLike) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); + + + + +class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_1__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const start_position = tokeniser.position; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)( + new IterableLike({ source: tokeniser.source, tokens: {} }), + ); + const { tokens } = ret; + tokens.readonly = tokeniser.consume("readonly"); + if (!tokens.readonly) { + tokens.async = tokeniser.consume("async"); + } + tokens.base = tokens.readonly + ? tokeniser.consume("maplike", "setlike") + : tokens.async + ? tokeniser.consume("iterable") + : tokeniser.consume("iterable", "async_iterable", "maplike", "setlike"); + if (!tokens.base) { + tokeniser.unconsume(start_position); + return; + } + + const { type } = ret; + const secondTypeRequired = type === "maplike"; + const secondTypeAllowed = + secondTypeRequired || type === "iterable" || type === "async_iterable"; + const argumentAllowed = + type === "async_iterable" || (ret.async && type === "iterable"); + + tokens.open = + tokeniser.consume("<") || + tokeniser.error(`Missing less-than sign \`<\` in ${type} declaration`); + const first = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.type_with_extended_attributes)(tokeniser) || + tokeniser.error(`Missing a type argument in ${type} declaration`); + ret.idlType = [first]; + ret.arguments = []; + + if (secondTypeAllowed) { + first.tokens.separator = tokeniser.consume(","); + if (first.tokens.separator) { + ret.idlType.push((0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.type_with_extended_attributes)(tokeniser)); + } else if (secondTypeRequired) { + tokeniser.error(`Missing second type argument in ${type} declaration`); + } + } + + tokens.close = + tokeniser.consume(">") || + tokeniser.error(`Missing greater-than sign \`>\` in ${type} declaration`); + + if (tokeniser.probe("(")) { + if (argumentAllowed) { + tokens.argsOpen = tokeniser.consume("("); + ret.arguments.push(...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.argument_list)(tokeniser)); + tokens.argsClose = + tokeniser.consume(")") || + tokeniser.error("Unterminated async iterable argument list"); + } else { + tokeniser.error(`Arguments are only allowed for \`async iterable\``); + } + } + + tokens.termination = + tokeniser.consume(";") || + tokeniser.error(`Missing semicolon after ${type} declaration`); + + return ret.this; + } + + get type() { + return this.tokens.base.value; + } + get readonly() { + return !!this.tokens.readonly; + } + get async() { + return !!this.tokens.async; + } + + *validate(defs) { + if (this.async && this.type === "iterable") { + const message = "`async iterable` is now changed to `async_iterable`."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + this.tokens.async, + this, + "obsolete-async-iterable-syntax", + message, + { + autofix: autofixAsyncIterableSyntax(this), + }, + ); + } + for (const type of this.idlType) { + yield* type.validate(defs); + } + for (const argument of this.arguments) { + yield* argument.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.readonly), + w.token(this.tokens.async), + w.token(this.tokens.base, w.ts.generic), + w.token(this.tokens.open), + w.ts.wrap(this.idlType.map((t) => t.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.argsOpen), + w.ts.wrap(this.arguments.map((arg) => arg.write(w))), + w.token(this.tokens.argsClose), + w.token(this.tokens.termination), + ]), + { data: this, parent: this.parent }, + ); + } +} + +/** + * @param {IterableLike} iterableLike + */ +function autofixAsyncIterableSyntax(iterableLike) { + return () => { + const async = iterableLike.tokens.async; + iterableLike.tokens.base = { + ...async, + type: "async_iterable", + value: "async_iterable", + }; + delete iterableLike.tokens.async; + }; +} + + +/***/ }), +/* 23 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ checkInterfaceMemberDuplication: () => (/* binding */ checkInterfaceMemberDuplication) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); + + +/** + * @param {import("../validator.js").Definitions} defs + * @param {import("../productions/container.js").Container} i + */ +function* checkInterfaceMemberDuplication(defs, i) { + const opNames = groupOperationNames(i); + const partials = defs.partials.get(i.name) || []; + const mixins = defs.mixinMap.get(i.name) || []; + for (const ext of [...partials, ...mixins]) { + const additions = getOperations(ext); + const statics = additions.filter((a) => a.special === "static"); + const nonstatics = additions.filter((a) => a.special !== "static"); + yield* checkAdditions(statics, opNames.statics, ext, i); + yield* checkAdditions(nonstatics, opNames.nonstatics, ext, i); + statics.forEach((op) => opNames.statics.add(op.name)); + nonstatics.forEach((op) => opNames.nonstatics.add(op.name)); + } + + /** + * @param {import("../productions/operation.js").Operation[]} additions + * @param {Set} existings + * @param {import("../productions/container.js").Container} ext + * @param {import("../productions/container.js").Container} base + */ + function* checkAdditions(additions, existings, ext, base) { + for (const addition of additions) { + const { name } = addition; + if (name && existings.has(name)) { + const isStatic = addition.special === "static" ? "static " : ""; + const message = `The ${isStatic}operation "${name}" has already been defined for the base interface "${base.name}" either in itself or in a mixin`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + addition.tokens.name, + ext, + "no-cross-overload", + message, + ); + } + } + } + + /** + * @param {import("../productions/container.js").Container} i + * @returns {import("../productions/operation.js").Operation[]} + */ + function getOperations(i) { + return i.members.filter(({ type }) => type === "operation"); + } + + /** + * @param {import("../productions/container.js").Container} i + */ + function groupOperationNames(i) { + const ops = getOperations(i); + return { + statics: new Set( + ops.filter((op) => op.special === "static").map((op) => op.name), + ), + nonstatics: new Set( + ops.filter((op) => op.special !== "static").map((op) => op.name), + ), + }; + } +} + + +/***/ }), +/* 24 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Constructor: () => (/* binding */ Constructor) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Constructor extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const base = tokeniser.consume("constructor"); + if (!base) { + return; + } + /** @type {Base["tokens"]} */ + const tokens = { base }; + tokens.open = + tokeniser.consume("(") || + tokeniser.error("No argument list in constructor"); + const args = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated constructor"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("No semicolon after constructor"); + const ret = new Constructor({ source: tokeniser.source, tokens }); + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(ret).arguments = args; + return ret; + } + + get type() { + return "constructor"; + } + + *validate(defs) { + for (const argument of this.arguments) { + yield* argument.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base, w.ts.nameless, { data: this, parent }), + w.token(this.tokens.open), + w.ts.wrap(this.arguments.map((arg) => arg.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this, parent }, + ); + } +} + + +/***/ }), +/* 25 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Mixin: () => (/* binding */ Mixin) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(21); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(14); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4); + + + + + + +class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {import("../tokeniser.js").Token} base + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token} [options.partial] + */ + static parse(tokeniser, base, { extMembers = [], partial } = {}) { + const tokens = { partial, base }; + tokens.mixin = tokeniser.consume("mixin"); + if (!tokens.mixin) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Mixin({ source: tokeniser.source, tokens }), + { + allowedMembers: [ + ...extMembers, + [_constant_js__WEBPACK_IMPORTED_MODULE_1__.Constant.parse], + [_helpers_js__WEBPACK_IMPORTED_MODULE_4__.stringifier], + [_attribute_js__WEBPACK_IMPORTED_MODULE_2__.Attribute.parse, { noInherit: true }], + [_operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse, { regular: true }], + ], + }, + ); + } + + get type() { + return "interface mixin"; + } +} + + +/***/ }), +/* 26 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Dictionary: () => (/* binding */ Dictionary) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _field_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27); + + + +class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token} [options.partial] + */ + static parse(tokeniser, { extMembers = [], partial } = {}) { + const tokens = { partial }; + tokens.base = tokeniser.consume("dictionary"); + if (!tokens.base) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Dictionary({ source: tokeniser.source, tokens }), + { + inheritable: !partial, + allowedMembers: [...extMembers, [_field_js__WEBPACK_IMPORTED_MODULE_1__.Field.parse]], + }, + ); + } + + get type() { + return "dictionary"; + } +} + + +/***/ }), +/* 27 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Field: () => (/* binding */ Field) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8); +/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12); + + + + + +class Field extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Field({ source: tokeniser.source, tokens })); + ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser); + tokens.required = tokeniser.consume("required"); + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "dictionary-type") || + tokeniser.error("Dictionary member lacks a type"); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Dictionary member lacks a name"); + ret.default = _default_js__WEBPACK_IMPORTED_MODULE_3__.Default.parse(tokeniser); + if (tokens.required && ret.default) + tokeniser.error("Required member must not have a default"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated dictionary member, expected `;`"); + return ret.this; + } + + get type() { + return "field"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value); + } + get required() { + return !!this.tokens.required; + } + + *validate(defs) { + yield* this.idlType.validate(defs); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.required), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + this.default ? this.default.write(w) : "", + w.token(this.tokens.termination), + ]), + { data: this, parent }, + ); + } +} + + +/***/ }), +/* 28 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Namespace: () => (/* binding */ Namespace) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(21); + + + + + + + +class Namespace extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + * @param {import("../tokeniser.js").Token} [options.partial] + */ + static parse(tokeniser, { extMembers = [], partial } = {}) { + const tokens = { partial }; + tokens.base = tokeniser.consume("namespace"); + if (!tokens.base) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Namespace({ source: tokeniser.source, tokens }), + { + allowedMembers: [ + ...extMembers, + [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse, { noInherit: true, readonly: true }], + [_constant_js__WEBPACK_IMPORTED_MODULE_5__.Constant.parse], + [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse, { regular: true }], + ], + }, + ); + } + + get type() { + return "namespace"; + } + + *validate(defs) { + if ( + !this.partial && + this.extAttrs.every((extAttr) => extAttr.name !== "Exposed") + ) { + const message = `Namespaces must have [Exposed] extended attribute. \ +To fix, add, for example, [Exposed=Window]. Please also consider carefully \ +if your namespace should also be exposed in a Worker scope. Refer to the \ +[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)( + this.tokens.name, + this, + "require-exposed", + message, + { + autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_4__.autofixAddExposedWindow)(this), + }, + ); + } + yield* super.validate(defs); + } +} + + +/***/ }), +/* 29 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ CallbackInterface: () => (/* binding */ CallbackInterface) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(21); + + + + +class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {*} callback + * @param {object} [options] + * @param {import("./container.js").AllowedMember[]} [options.extMembers] + */ + static parse(tokeniser, callback, { extMembers = [] } = {}) { + const tokens = { callback }; + tokens.base = tokeniser.consume("interface"); + if (!tokens.base) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new CallbackInterface({ source: tokeniser.source, tokens }), + { + allowedMembers: [ + ...extMembers, + [_constant_js__WEBPACK_IMPORTED_MODULE_2__.Constant.parse], + [_operation_js__WEBPACK_IMPORTED_MODULE_1__.Operation.parse, { regular: true }], + ], + }, + ); + } + + get type() { + return "callback interface"; + } +} + + +/***/ }), +/* 30 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ Writer: () => (/* binding */ Writer), +/* harmony export */ write: () => (/* binding */ write) +/* harmony export */ }); +function noop(arg) { + return arg; +} + +const templates = { + wrap: (items) => items.join(""), + trivia: noop, + name: noop, + reference: noop, + type: noop, + generic: noop, + nameless: noop, + inheritance: noop, + definition: noop, + extendedAttribute: noop, + extendedAttributeReference: noop, +}; + +class Writer { + constructor(ts) { + this.ts = Object.assign({}, templates, ts); + } + + /** + * @param {string} raw + * @param {object} options + * @param {string} [options.unescaped] + * @param {import("./productions/base.js").Base} [options.context] + * @returns + */ + reference(raw, { unescaped, context }) { + if (!unescaped) { + unescaped = raw.startsWith("_") ? raw.slice(1) : raw; + } + return this.ts.reference(raw, unescaped, context); + } + + /** + * @param {import("./tokeniser.js").Token} t + * @param {Function} wrapper + * @param {...any} args + * @returns + */ + token(t, wrapper = noop, ...args) { + if (!t) { + return ""; + } + const value = wrapper(t.value, ...args); + return this.ts.wrap([this.ts.trivia(t.trivia), value]); + } + + reference_token(t, context) { + return this.token(t, this.reference.bind(this), { context }); + } + + name_token(t, arg) { + return this.token(t, this.ts.name, arg); + } + + identifier(id, context) { + return this.ts.wrap([ + this.reference_token(id.tokens.value, context), + this.token(id.tokens.separator), + ]); + } +} + +function write(ast, { templates: ts = templates } = {}) { + ts = Object.assign({}, templates, ts); + + const w = new Writer(ts); + + return ts.wrap(ast.map((it) => it.write(w))); +} + + +/***/ }), +/* 31 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ validate: () => (/* binding */ validate) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); + + +function getMixinMap(all, unique) { + const map = new Map(); + const includes = all.filter((def) => def.type === "includes"); + for (const include of includes) { + const mixin = unique.get(include.includes); + if (!mixin) { + continue; + } + const array = map.get(include.target); + if (array) { + array.push(mixin); + } else { + map.set(include.target, [mixin]); + } + } + return map; +} + +/** + * @typedef {ReturnType} Definitions + */ +function groupDefinitions(all) { + const unique = new Map(); + const duplicates = new Set(); + const partials = new Map(); + for (const def of all) { + if (def.partial) { + const array = partials.get(def.name); + if (array) { + array.push(def); + } else { + partials.set(def.name, [def]); + } + continue; + } + if (!def.name) { + continue; + } + if (!unique.has(def.name)) { + unique.set(def.name, def); + } else { + duplicates.add(def); + } + } + return { + all, + unique, + partials, + duplicates, + mixinMap: getMixinMap(all, unique), + cache: { + typedefIncludesDictionary: new WeakMap(), + dictionaryIncludesRequiredField: new WeakMap(), + }, + }; +} + +function* checkDuplicatedNames({ unique, duplicates }) { + for (const dup of duplicates) { + const { name } = dup; + const message = `The name "${name}" of type "${ + unique.get(name).type + }" was already seen`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(dup.tokens.name, dup, "no-duplicate", message); + } +} + +function* validateIterable(ast) { + const defs = groupDefinitions(ast); + for (const def of defs.all) { + if (def.validate) { + yield* def.validate(defs); + } + } + yield* checkDuplicatedNames(defs); +} + +// Remove this once all of our support targets expose `.flat()` by default +function flatten(array) { + if (array.flat) { + return array.flat(); + } + return [].concat(...array); +} + +/** + * @param {import("./productions/base.js").Base[]} ast + * @return {import("./error.js").WebIDLErrorData[]} validation errors + */ +function validate(ast) { + return [...validateIterable(flatten(ast))]; +} + + +/***/ }) +/******/ ]); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk. +(() => { +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ WebIDLParseError: () => (/* reexport safe */ _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__.WebIDLParseError), +/* harmony export */ parse: () => (/* reexport safe */ _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__.parse), +/* harmony export */ validate: () => (/* reexport safe */ _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__.validate), +/* harmony export */ write: () => (/* reexport safe */ _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__.write) +/* harmony export */ }); +/* harmony import */ var _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); +/* harmony import */ var _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31); +/* harmony import */ var _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2); + + + + + +})(); + +/******/ return __webpack_exports__; +/******/ })() +; +}); +//# sourceMappingURL=webidl2.js.map \ No newline at end of file diff --git a/test/js/third_party/wpt-streams/streams/idlharness.any.js b/test/js/third_party/wpt-streams/streams/idlharness.any.js new file mode 100644 index 000000000000..42a17da58c5a --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/idlharness.any.js @@ -0,0 +1,79 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +idl_test( + ['streams'], + ['dom'], // for AbortSignal + async idl_array => { + // Empty try/catches ensure that if something isn't implemented (e.g., readable byte streams, or writable streams) + // the harness still sets things up correctly. Note that the corresponding interface tests will still fail. + + try { + new ReadableStream({ + start(c) { + self.readableStreamDefaultController = c; + } + }); + } catch {} + + try { + new ReadableStream({ + start(c) { + self.readableByteStreamController = c; + }, + type: 'bytes' + }); + } catch {} + + try { + let resolvePullCalledPromise; + const pullCalledPromise = new Promise(resolve => { + resolvePullCalledPromise = resolve; + }); + const stream = new ReadableStream({ + pull(c) { + self.readableStreamByobRequest = c.byobRequest; + resolvePullCalledPromise(); + }, + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + reader.read(new Uint8Array(1)); + await pullCalledPromise; + } catch {} + + try { + new WritableStream({ + start(c) { + self.writableStreamDefaultController = c; + } + }); + } catch {} + + try { + new TransformStream({ + start(c) { + self.transformStreamDefaultController = c; + } + }); + } catch {} + + idl_array.add_objects({ + ReadableStream: ["new ReadableStream()"], + ReadableStreamDefaultReader: ["(new ReadableStream()).getReader()"], + ReadableStreamBYOBReader: ["(new ReadableStream({ type: 'bytes' })).getReader({ mode: 'byob' })"], + ReadableStreamDefaultController: ["self.readableStreamDefaultController"], + ReadableByteStreamController: ["self.readableByteStreamController"], + ReadableStreamBYOBRequest: ["self.readableStreamByobRequest"], + WritableStream: ["new WritableStream()"], + WritableStreamDefaultWriter: ["(new WritableStream()).getWriter()"], + WritableStreamDefaultController: ["self.writableStreamDefaultController"], + TransformStream: ["new TransformStream()"], + TransformStreamDefaultController: ["self.transformStreamDefaultController"], + ByteLengthQueuingStrategy: ["new ByteLengthQueuingStrategy({ highWaterMark: 5 })"], + CountQueuingStrategy: ["new CountQueuingStrategy({ highWaterMark: 5 })"] + }); + } +); diff --git a/test/js/third_party/wpt-streams/streams/piping/abort.any.js b/test/js/third_party/wpt-streams/streams/piping/abort.any.js new file mode 100644 index 000000000000..f2e5429492e7 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/abort.any.js @@ -0,0 +1,448 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +// META: script=../resources/test-utils.js +'use strict'; + +// Tests for the use of pipeTo with AbortSignal. +// There is some extra complexity to avoid timeouts in environments where abort is not implemented. + +const error1 = new Error('error1'); +error1.name = 'error1'; +const error2 = new Error('error2'); +error2.name = 'error2'; + +const errorOnPull = { + pull(controller) { + // This will cause the test to error if pipeTo abort is not implemented. + controller.error('failed to abort'); + } +}; + +// To stop pull() being called immediately when the stream is created, we need to set highWaterMark to 0. +const hwm0 = { highWaterMark: 0 }; + +for (const invalidSignal of [null, 'AbortSignal', true, -1, Object.create(AbortSignal.prototype)]) { + promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { signal: invalidSignal }), 'pipeTo should reject') + .then(() => { + assert_equals(rs.events.length, 0, 'no ReadableStream methods should have been called'); + assert_equals(ws.events.length, 0, 'no WritableStream methods should have been called'); + }); + }, `a signal argument '${invalidSignal}' should cause pipeTo() to reject`); +} + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject') + .then(() => Promise.all([ + rs.getReader().closed, + promise_rejects_dom(t, 'AbortError', ws.getWriter().closed, 'writer.closed should reject') + ])) + .then(() => { + assert_equals(rs.events.length, 2, 'cancel should have been called'); + assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); + assert_equals(rs.events[1].name, 'AbortError', 'the argument to cancel should be an AbortError'); + assert_equals(rs.events[1].constructor.name, 'DOMException', + 'the argument to cancel should be a DOMException'); + }); +}, 'an aborted signal should cause the writable stream to reject with an AbortError'); + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(reason); + const pipeToPromise = rs.pipeTo(ws, { signal }); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + await rs.getReader().closed; + await promise_rejects_exactly(t, error, ws.getWriter().closed, 'the writable should be errored with the same object'); + assert_equals(signal.reason, error, 'signal.reason should be error'), + assert_equals(rs.events.length, 2, 'cancel should have been called'); + assert_equals(rs.events[0], 'cancel', 'first event should be cancel'); + assert_equals(rs.events[1], error, 'the readable should be canceled with the same object'); + }, `(reason: '${reason}') all the error objects should be the same object`); +} + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true }), 'pipeTo should reject') + .then(() => assert_equals(rs.events.length, 0, 'cancel should not be called')); +}, 'preventCancel should prevent canceling the readable'); + +promise_test(t => { + const rs = new ReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventAbort: true }), 'pipeTo should reject') + .then(() => { + assert_equals(ws.events.length, 0, 'writable should not have been aborted'); + return ws.getWriter().ready; + }); +}, 'preventAbort should prevent aborting the readable'); + +promise_test(t => { + const rs = recordingReadableStream(errorOnPull, hwm0); + const ws = recordingWritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal, preventCancel: true, preventAbort: true }), + 'pipeTo should reject') + .then(() => { + assert_equals(rs.events.length, 0, 'cancel should not be called'); + assert_equals(ws.events.length, 0, 'writable should not have been aborted'); + return ws.getWriter().ready; + }); +}, 'preventCancel and preventAbort should prevent canceling the readable and aborting the readable'); + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + const rs = new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.close(); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const ws = recordingWritableStream({ + write() { + abortController.abort(reason); + } + }); + const pipeToPromise = rs.pipeTo(ws, { signal }); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + assert_equals(signal.reason, error, 'signal.reason should be error'); + assert_equals(ws.events.length, 4, 'only chunk "a" should have been written'); + assert_array_equals(ws.events.slice(0, 3), ['write', 'a', 'abort'], 'events should match'); + assert_equals(ws.events[3], error, 'abort reason should be error'); + }, `(reason: '${reason}') abort should prevent further reads`); +} + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + c.enqueue('a'); + c.enqueue('b'); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + let resolveWrite; + const writePromise = new Promise(resolve => { + resolveWrite = resolve; + }); + const ws = recordingWritableStream({ + write() { + return writePromise; + } + }, new CountQueuingStrategy({ highWaterMark: Infinity })); + const pipeToPromise = rs.pipeTo(ws, { signal }); + await delay(0); + await abortController.abort(reason); + await readController.close(); // Make sure the test terminates when signal is not implemented. + await resolveWrite(); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + const error = await pipeToPromise.catch(e => e); + assert_equals(signal.reason, error, 'signal.reason should be error'); + assert_equals(ws.events.length, 6, 'chunks "a" and "b" should have been written'); + assert_array_equals(ws.events.slice(0, 5), ['write', 'a', 'write', 'b', 'abort'], 'events should match'); + assert_equals(ws.events[5], error, 'abort reason should be error'); + }, `(reason: '${reason}') all pending writes should complete on abort`); +} + +for (const reason of [null, undefined, error1]) { + promise_test(async t => { + let rejectPull; + const pullPromise = new Promise((_, reject) => { + rejectPull = reject; + }); + let rejectCancel; + const cancelPromise = new Promise((_, reject) => { + rejectCancel = reject; + }); + const rs = recordingReadableStream({ + async pull() { + await Promise.race([ + pullPromise, + cancelPromise, + ]); + }, + cancel(reason) { + rejectCancel(reason); + }, + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal }); + pipeToPromise.catch(() => {}); // Prevent unhandled rejection. + await delay(0); + abortController.abort(reason); + rejectPull('should not catch pull rejection'); + await delay(0); + assert_equals(rs.eventsWithoutPulls.length, 2, 'cancel should have been called'); + assert_equals(rs.eventsWithoutPulls[0], 'cancel', 'first event should be cancel'); + if (reason !== undefined) { + await promise_rejects_exactly(t, reason, pipeToPromise, 'pipeTo rejects with abort reason'); + } else { + await promise_rejects_dom(t, 'AbortError', pipeToPromise, 'pipeTo rejects with AbortError'); + } + }, `(reason: '${reason}') underlyingSource.cancel() should called when abort, even with pending pull`); +} + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + }, + cancel() { + return Promise.reject(error1); + } + }, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'a rejection from underlyingSource.cancel() should be returned by pipeTo()'); + +promise_test(t => { + const rs = new ReadableStream(errorOnPull, hwm0); + const ws = new WritableStream({ + abort() { + return Promise.reject(error1); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'a rejection from underlyingSink.abort() should be returned by pipeTo()'); + +promise_test(t => { + const events = []; + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + }, + cancel() { + events.push('cancel'); + return Promise.reject(error1); + } + }, hwm0); + const ws = new WritableStream({ + abort() { + events.push('abort'); + return Promise.reject(error2); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_exactly(t, error2, rs.pipeTo(ws, { signal }), 'pipeTo should reject') + .then(() => assert_array_equals(events, ['abort', 'cancel'], 'abort() should be called before cancel()')); +}, 'a rejection from underlyingSink.abort() should be preferred to one from underlyingSource.cancel()'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over closed readable'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.error(error1); + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over errored readable'); + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + } + }, hwm0); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + const writer = ws.getWriter(); + return writer.close().then(() => { + writer.releaseLock(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); + }); +}, 'abort signal takes priority over closed writable'); + +promise_test(t => { + const rs = new ReadableStream({ + pull(controller) { + controller.error('failed to abort'); + } + }, hwm0); + const ws = new WritableStream({ + start(controller) { + controller.error(error1); + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + abortController.abort(); + return promise_rejects_dom(t, 'AbortError', rs.pipeTo(ws, { signal }), 'pipeTo should reject'); +}, 'abort signal takes priority over errored writable'); + +promise_test(() => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventClose: true }); + readController.close(); + return Promise.resolve().then(() => { + abortController.abort(); + return pipeToPromise; + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is closed'); + +promise_test(t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + const ws = new WritableStream(); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); + readController.error(error1); + return Promise.resolve().then(() => { + abortController.abort(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is errored'); + +promise_test(t => { + let readController; + const rs = new ReadableStream({ + start(c) { + readController = c; + } + }); + let resolveWrite; + const writePromise = new Promise(resolve => { + resolveWrite = resolve; + }); + const ws = new WritableStream({ + write() { + readController.error(error1); + return writePromise; + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventAbort: true }); + readController.enqueue('a'); + return delay(0).then(() => { + abortController.abort(); + resolveWrite(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => ws.getWriter().write('this should succeed')); +}, 'abort should do nothing after the readable is errored, even with pending writes'); + +promise_test(t => { + const rs = recordingReadableStream({ + pull(controller) { + return delay(0).then(() => controller.close()); + } + }); + let writeController; + const ws = new WritableStream({ + start(c) { + writeController = c; + } + }); + const abortController = new AbortController(); + const signal = abortController.signal; + const pipeToPromise = rs.pipeTo(ws, { signal, preventCancel: true }); + return Promise.resolve().then(() => { + writeController.error(error1); + return Promise.resolve(); + }).then(() => { + abortController.abort(); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => { + assert_array_equals(rs.events, ['pull'], 'cancel should not have been called'); + }); +}, 'abort should do nothing after the writable is errored'); + +promise_test(async t => { + const rs = new ReadableStream({ + pull(c) { + c.enqueue(new Uint8Array([])); + }, + type: "bytes", + }); + const ws = new WritableStream(); + const [first, second] = rs.tee(); + + let aborted = false; + first.pipeTo(ws, { signal: AbortSignal.abort() }).catch(() => { + aborted = true; + }); + await delay(0); + assert_true(!aborted, "pipeTo should not resolve yet"); + await second.cancel(); + await delay(0); + assert_true(aborted, "pipeTo should be aborted now"); +}, "pipeTo on a teed readable byte stream should only be aborted when both branches are aborted"); diff --git a/test/js/third_party/wpt-streams/streams/piping/close-propagation-backward.any.js b/test/js/third_party/wpt-streams/streams/piping/close-propagation-backward.any.js new file mode 100644 index 000000000000..5ea47ab85c0c --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/close-propagation-backward.any.js @@ -0,0 +1,153 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err.name, 'TypeError', 'the promise must reject with a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + } + ); + +}, 'Closing must be propagated backward: starts closed; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + // Our recording streams do not deal well with errors generated by the system, so give them some help + let recordedError; + const rs = recordingReadableStream({ + cancel(cancelErr) { + recordedError = cancelErr; + throw error1; + } + }); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_equals(recordedError.name, 'TypeError', 'the cancel reason must be a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', recordedError]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel omitted; rejected cancel promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws, { preventCancel: falsy }).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err.name, 'TypeError', 'the promise must reject with a TypeError'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + } + ); + + }, `Closing must be propagated backward: starts closed; preventCancel = ${stringVersion} (falsy); fulfilled cancel ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { preventCancel: truthy })).then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + + }, `Closing must be propagated backward: starts closed; preventCancel = ${String(truthy)} (truthy)`); +} + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { preventCancel: true, preventAbort: true })) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, + rs.pipeTo(ws, { preventCancel: true, preventAbort: true, preventClose: true })) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return ws.getWriter().closed; + }); + +}, 'Closing must be propagated backward: starts closed; preventCancel = true, preventAbort = true, preventClose ' + + '= true'); diff --git a/test/js/third_party/wpt-streams/streams/piping/close-propagation-forward.any.js b/test/js/third_party/wpt-streams/streams/piping/close-propagation-forward.any.js new file mode 100644 index 000000000000..71b6e2628400 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/close-propagation-forward.any.js @@ -0,0 +1,589 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: starts closed; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: starts closed; preventClose omitted; rejected close promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: falsy }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + + }, `Closing must be propagated forward: starts closed; preventClose = ${stringVersion} (falsy); fulfilled close ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: truthy }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + + }, `Closing must be propagated forward: starts closed; preventClose = ${String(truthy)} (truthy)`); +} + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: true, preventAbort: true }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true'); + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.close(); + } + }); + + const ws = recordingWritableStream(); + + return rs.pipeTo(ws, { preventClose: true, preventAbort: true, preventCancel: true }).then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: starts closed; preventClose = true, preventAbort = true, preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => rs.controller.close()); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed asynchronously; dest never desires chunks; ' + + 'preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; fulfilled close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed) + ]); + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose omitted; rejected close promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws, { preventClose: true }); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.close()); + }, 10); + + return pipePromise.then(value => { + assert_equals(value, undefined, 'the promise must fulfill with undefined'); + }) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + + return rs.getReader().closed; + }); + +}, 'Closing must be propagated forward: becomes closed after one chunk; preventClose = true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.close(); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'close' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'close']); + }); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws, { preventClose: true }).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.close(); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the chunk must have been written, but close must not have happened'); + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the chunk must have been written, but close must not have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; preventClose = true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but close must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.close(); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but close must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close'], + 'all chunks must have been written and close must have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = rs.pipeTo(ws, { preventClose: true }).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but close must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.close(); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but close must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'all chunks must have been written, but close must not have happened'); + }); + +}, 'Closing must be propagated forward: shutdown must not occur until the final write completes; becomes closed after first write; preventClose = true'); + + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + let rejectWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise((resolve, reject) => { + rejectWritePromise = reject; + }); + } + }, { highWaterMark: 3 }); + const pipeToPromise = rs.pipeTo(ws); + return delay(0).then(() => { + rejectWritePromise(error1); + return promise_rejects_exactly(t, error1, pipeToPromise, 'pipeTo should reject'); + }).then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['write', 'a']); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed, 'ws should be errored') + ]); + }); +}, 'Closing must be propagated forward: erroring the writable while flushing pending writes should error pipeTo'); diff --git a/test/js/third_party/wpt-streams/streams/piping/error-propagation-backward.any.js b/test/js/third_party/wpt-streams/streams/piping/error-propagation-backward.any.js new file mode 100644 index 000000000000..ec74592f86ef --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/error-propagation-backward.any.js @@ -0,0 +1,630 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + start() { + return Promise.reject(error1); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: starts errored; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; ' + + 'fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel omitted; rejected ' + + 'cancel promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: falsy }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + + }, `Errors must be propagated backward: becomes errored before piping due to write; preventCancel = ` + + `${stringVersion} (falsy); fulfilled cancel promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: truthy }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + + }, `Errors must be propagated backward: becomes errored before piping due to write; preventCancel = ` + + `${String(truthy)} (truthy)`); +} + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true, preventAbort: true }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write, preventCancel = true; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + write() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('Hello'), 'writer.write() must reject with the write error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'writer.closed must reject with the write error')) + .then(() => { + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true, preventAbort: true, preventClose: true }), + 'pipeTo must reject with the write error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + }); + +}, 'Errors must be propagated backward: becomes errored before piping due to write; preventCancel = true, ' + + 'preventAbort = true, preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; fulfilled ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + }, + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel omitted; rejected ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('Hello'); + } + }); + + const ws = recordingWritableStream({ + write() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = ' + + 'false; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + }, + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = ' + + 'false; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + } + }); + + const ws = recordingWritableStream({ + write() { + if (ws.events.length > 2) { + return delay(0).then(() => { + throw error1; + }); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b']); + }); + +}, 'Errors must be propagated backward: becomes errored during piping due to write, but async; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel omitted; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel omitted; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + controller.close(); + } + }); + + const ws = recordingWritableStream({ + write(chunk) { + if (chunk === 'c') { + return Promise.reject(error1); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error').then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c']); + }); + +}, 'Errors must be propagated backward: becomes errored after piping due to last write; source is closed; ' + + 'preventCancel omitted (but cancel is never called)'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + controller.close(); + } + }); + + const ws = recordingWritableStream({ + write(chunk) { + if (chunk === 'c') { + return Promise.reject(error1); + } + return undefined; + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c']); + }); + +}, 'Errors must be propagated backward: becomes errored after piping due to last write; source is closed; ' + + 'preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'false; fulfilled cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'false; rejected cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => ws.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated backward: becomes errored after piping; dest never desires chunks; preventCancel = ' + + 'true'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return rs.pipeTo(ws).then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err, error1, 'the promise must reject with error1'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['abort', error1]); + } + ); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; fulfilled ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + cancel() { + throw error2; + } + }); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the cancel error') + .then(() => { + return ws.getWriter().closed.then( + () => assert_unreached('the promise must not fulfill'), + err => { + assert_equals(err, error1, 'the promise must reject with error1'); + + assert_array_equals(rs.eventsWithoutPulls, ['cancel', err]); + assert_array_equals(ws.events, ['abort', error1]); + } + ); + }); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel omitted; rejected ' + + 'cancel promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + ws.abort(error1); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventCancel: true })).then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated backward: becomes errored before piping via abort; preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + return flushAsyncEvents(); + } + }); + + const pipePromise = rs.pipeTo(ws); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + ws.controller.error(error1); + + return promise_rejects_exactly(t, error1, pipePromise); + }).then(() => { + assert_array_equals(rs.eventsWithoutPulls, ['cancel', error1]); + assert_array_equals(ws.events, ['write', 'a']); + }); + +}, 'Errors must be propagated backward: erroring via the controller errors once pending write completes'); diff --git a/test/js/third_party/wpt-streams/streams/piping/error-propagation-forward.any.js b/test/js/third_party/wpt-streams/streams/piping/error-propagation-forward.any.js new file mode 100644 index 000000000000..482da2f8a88e --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/error-propagation-forward.any.js @@ -0,0 +1,569 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = false; rejected abort promise'); + +for (const falsy of [undefined, null, false, +0, -0, NaN, '']) { + const stringVersion = Object.is(falsy, -0) ? '-0' : String(falsy); + + promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: falsy }), 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + + }, `Errors must be propagated forward: starts errored; preventAbort = ${stringVersion} (falsy); fulfilled abort ` + + `promise`); +} + +for (const truthy of [true, 'a', 1, Symbol(), { }]) { + promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: truthy }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + + }, `Errors must be propagated forward: starts errored; preventAbort = ${String(truthy)} (truthy)`); +} + + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true, preventCancel: true }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true'); + +promise_test(t => { + + const rs = recordingReadableStream({ + start() { + return Promise.reject(error1); + } + }); + + const ws = recordingWritableStream(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true, preventCancel: true, preventClose: true }), + 'pipeTo must reject with the same error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: starts errored; preventAbort = true, preventCancel = true, preventClose = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => rs.controller.error(error1), 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored while empty; dest never desires chunks; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello', 'abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'Hello']); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = false; fulfilled abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream({ + abort() { + throw error2; + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the abort error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['abort', error1]); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = false; rejected abort promise'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the same error'); + + t.step_timeout(() => { + rs.controller.enqueue('Hello'); + t.step_timeout(() => rs.controller.error(error1), 10); + }, 10); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }); + +}, 'Errors must be propagated forward: becomes errored after one chunk; dest never desires chunks; ' + + 'preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws)).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + rs.controller.error(error1); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + + return pipePromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'abort', error1]); + }); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true })).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + + return writeCalledPromise.then(() => { + rs.controller.error(error1); + + // Flush async events and verify that no shutdown occurs. + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + assert_equals(pipeComplete, false, 'the pipe must not be complete'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a']); // no 'abort' + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; preventAbort = true'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws)).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but abort must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.error(error1); + resolveWritePromise(); + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but abort must not have happened yet'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'abort', error1], + 'all chunks must have been written and abort must have happened'); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write'); + +promise_test(t => { + + const rs = recordingReadableStream(); + + let resolveWriteCalled; + const writeCalledPromise = new Promise(resolve => { + resolveWriteCalled = resolve; + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + resolveWriteCalled(); + + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + }, new CountQueuingStrategy({ highWaterMark: 2 })); + + let pipeComplete = false; + const pipePromise = promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true })).then(() => { + pipeComplete = true; + }); + + rs.controller.enqueue('a'); + rs.controller.enqueue('b'); + + return writeCalledPromise.then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a'], + 'the first chunk must have been written, but abort must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the first write is pending'); + + rs.controller.error(error1); + resolveWritePromise(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'the second chunk must have been written, but abort must not have happened'); + assert_false(pipeComplete, 'the pipe should not complete while the second write is pending'); + + resolveWritePromise(); + return pipePromise; + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'all chunks must have been written, but abort must not have happened'); + }); + +}, 'Errors must be propagated forward: shutdown must not occur until the final write completes; becomes errored after first write; preventAbort = true'); diff --git a/test/js/third_party/wpt-streams/streams/piping/flow-control.any.js b/test/js/third_party/wpt-streams/streams/piping/flow-control.any.js new file mode 100644 index 000000000000..09c4420f872a --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/flow-control.any.js @@ -0,0 +1,297 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +promise_test(t => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.close(); + } + }); + + const ws = recordingWritableStream(undefined, new CountQueuingStrategy({ highWaterMark: 0 })); + + const pipePromise = rs.pipeTo(ws, { preventCancel: true }); + + // Wait and make sure it doesn't do any reading. + return flushAsyncEvents().then(() => { + ws.controller.error(error1); + }) + .then(() => promise_rejects_exactly(t, error1, pipePromise, 'pipeTo must reject with the same error')) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, []); + }) + .then(() => readableStreamToArray(rs)) + .then(chunksNotPreviouslyRead => { + assert_array_equals(chunksNotPreviouslyRead, ['a', 'b']); + }); + +}, 'Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks'); + +promise_test(() => { + + const rs = recordingReadableStream({ + start(controller) { + controller.enqueue('b'); + controller.close(); + } + }); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }); + + const writer = ws.getWriter(); + const firstWritePromise = writer.write('a'); + assert_equals(writer.desiredSize, 0, 'after writing the writer\'s desiredSize must be 0'); + writer.releaseLock(); + + // firstWritePromise won't settle until we call resolveWritePromise. + + const pipePromise = rs.pipeTo(ws); + + return flushAsyncEvents().then(() => resolveWritePromise()) + .then(() => Promise.all([firstWritePromise, pipePromise])) + .then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close']); + }); + +}, 'Piping from a non-empty ReadableStream into a WritableStream that does not desire chunks, but then does'); + +promise_test(() => { + + const rs = recordingReadableStream(); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }); + + const writer = ws.getWriter(); + writer.write('a'); + + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); + assert_equals(writer.desiredSize, 0, 'after writing the writer\'s desiredSize must be 0'); + writer.releaseLock(); + + const pipePromise = rs.pipeTo(ws); + + rs.controller.enqueue('b'); + resolveWritePromise(); + rs.controller.close(); + + return pipePromise.then(() => { + assert_array_equals(rs.eventsWithoutPulls, []); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'close']); + }); + }); + +}, 'Piping from an empty ReadableStream into a WritableStream that does not desire chunks, but then the readable ' + + 'stream becomes non-empty and the writable stream starts desiring chunks'); + +promise_test(() => { + const unreadChunks = ['b', 'c', 'd']; + + const rs = recordingReadableStream({ + pull(controller) { + controller.enqueue(unreadChunks.shift()); + if (unreadChunks.length === 0) { + controller.close(); + } + } + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + let resolveWritePromise; + const ws = recordingWritableStream({ + write() { + if (!resolveWritePromise) { + // first write + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + } + return undefined; + } + }, new CountQueuingStrategy({ highWaterMark: 3 })); + + const writer = ws.getWriter(); + const firstWritePromise = writer.write('a'); + assert_equals(writer.desiredSize, 2, 'after writing the writer\'s desiredSize must be 2'); + writer.releaseLock(); + + // firstWritePromise won't settle until we call resolveWritePromise. + + const pipePromise = rs.pipeTo(ws); + + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a']); + assert_equals(unreadChunks.length, 1, 'chunks should continue to be enqueued until the HWM is reached'); + }).then(() => resolveWritePromise()) + .then(() => Promise.all([firstWritePromise, pipePromise])) + .then(() => { + assert_array_equals(rs.events, ['pull', 'pull', 'pull']); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b','write', 'c','write', 'd', 'close']); + }); + +}, 'Piping from a ReadableStream to a WritableStream that desires more chunks before finishing with previous ones'); + +class StepTracker { + constructor() { + this.waiters = []; + this.wakers = []; + } + + // Returns promise which resolves when step `n` is reached. Also schedules step n + 1 to happen shortly after the + // promise is resolved. + waitThenAdvance(n) { + if (this.waiters[n] === undefined) { + this.waiters[n] = new Promise(resolve => { + this.wakers[n] = resolve; + }); + this.waiters[n] + .then(() => flushAsyncEvents()) + .then(() => { + if (this.wakers[n + 1] !== undefined) { + this.wakers[n + 1](); + } + }); + } + if (n == 0) { + this.wakers[0](); + } + return this.waiters[n]; + } +} + +promise_test(() => { + const steps = new StepTracker(); + const desiredSizes = []; + const rs = recordingReadableStream({ + start(controller) { + steps.waitThenAdvance(1).then(() => enqueue('a')); + steps.waitThenAdvance(3).then(() => enqueue('b')); + steps.waitThenAdvance(5).then(() => enqueue('c')); + steps.waitThenAdvance(7).then(() => enqueue('d')); + steps.waitThenAdvance(11).then(() => controller.close()); + + function enqueue(chunk) { + controller.enqueue(chunk); + desiredSizes.push(controller.desiredSize); + } + } + }); + + const chunksFinishedWriting = []; + const writableStartPromise = Promise.resolve(); + let writeCalled = false; + const ws = recordingWritableStream({ + start() { + return writableStartPromise; + }, + write(chunk) { + const waitForStep = writeCalled ? 12 : 9; + writeCalled = true; + return steps.waitThenAdvance(waitForStep).then(() => { + chunksFinishedWriting.push(chunk); + }); + } + }); + + return writableStartPromise.then(() => { + const pipePromise = rs.pipeTo(ws); + steps.waitThenAdvance(0); + + return Promise.all([ + steps.waitThenAdvance(2).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 2, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 2, one chunk must have been written'); + + // When 'a' (the very first chunk) was enqueued, it was immediately used to fulfill the outstanding read request + // promise, leaving the queue empty. + assert_array_equals(desiredSizes, [1], + 'at step 2, the desiredSize at the last enqueue (step 1) must have been 1'); + assert_equals(rs.controller.desiredSize, 1, 'at step 2, the current desiredSize must be 1'); + }), + + steps.waitThenAdvance(4).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 4, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 4, one chunk must have been written'); + + // When 'b' was enqueued at step 3, the queue was also empty, since immediately after enqueuing 'a' at + // step 1, it was dequeued in order to fulfill the read() call that was made at step 0. Thus the queue + // had size 1 (thus desiredSize of 0). + assert_array_equals(desiredSizes, [1, 0], + 'at step 4, the desiredSize at the last enqueue (step 3) must have been 0'); + assert_equals(rs.controller.desiredSize, 0, 'at step 4, the current desiredSize must be 0'); + }), + + steps.waitThenAdvance(6).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 6, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 6, one chunk must have been written'); + + // When 'c' was enqueued at step 5, the queue was not empty; it had 'b' in it, since 'b' will not be read until + // the first write completes at step 9. Thus, the queue size is 2 after enqueuing 'c', giving a desiredSize of + // -1. + assert_array_equals(desiredSizes, [1, 0, -1], + 'at step 6, the desiredSize at the last enqueue (step 5) must have been -1'); + assert_equals(rs.controller.desiredSize, -1, 'at step 6, the current desiredSize must be -1'); + }), + + steps.waitThenAdvance(8).then(() => { + assert_array_equals(chunksFinishedWriting, [], 'at step 8, zero chunks must have finished writing'); + assert_array_equals(ws.events, ['write', 'a'], 'at step 8, one chunk must have been written'); + + // When 'd' was enqueued at step 7, the situation is the same as before, leading to a queue containing 'b', 'c', + // and 'd'. + assert_array_equals(desiredSizes, [1, 0, -1, -2], + 'at step 8, the desiredSize at the last enqueue (step 7) must have been -2'); + assert_equals(rs.controller.desiredSize, -2, 'at step 8, the current desiredSize must be -2'); + }), + + steps.waitThenAdvance(10).then(() => { + assert_array_equals(chunksFinishedWriting, ['a'], 'at step 10, one chunk must have finished writing'); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b'], + 'at step 10, two chunks must have been written'); + + assert_equals(rs.controller.desiredSize, -1, 'at step 10, the current desiredSize must be -1'); + }), + + pipePromise.then(() => { + assert_array_equals(desiredSizes, [1, 0, -1, -2], 'backpressure must have been exerted at the source'); + assert_array_equals(chunksFinishedWriting, ['a', 'b', 'c', 'd'], 'all chunks finished writing'); + + assert_array_equals(rs.eventsWithoutPulls, [], 'nothing unexpected should happen to the ReadableStream'); + assert_array_equals(ws.events, ['write', 'a', 'write', 'b', 'write', 'c', 'write', 'd', 'close'], + 'all chunks were written (and the WritableStream closed)'); + }) + ]); + }); +}, 'Piping to a WritableStream that does not consume the writes fast enough exerts backpressure on the ReadableStream'); diff --git a/test/js/third_party/wpt-streams/streams/piping/general-addition.any.js b/test/js/third_party/wpt-streams/streams/piping/general-addition.any.js new file mode 100644 index 000000000000..2562b7064338 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/general-addition.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker +'use strict'; + +promise_test(async t => { + /** @type {ReadableStreamDefaultController} */ + var con; + let synchronous = false; + new ReadableStream({ start(c) { con = c }}, { highWaterMark: 0 }).pipeTo( + new WritableStream({ write() { synchronous = true; } }) + ) + // wait until start algorithm finishes + await Promise.resolve(); + con.enqueue(); + assert_false(synchronous, 'write algorithm must not run synchronously'); +}, "enqueue() must not synchronously call write algorithm"); diff --git a/test/js/third_party/wpt-streams/streams/piping/general.any.js b/test/js/third_party/wpt-streams/streams/piping/general.any.js new file mode 100644 index 000000000000..272b25a28e80 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/general.any.js @@ -0,0 +1,212 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +'use strict'; + +test(() => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + assert_false(rs.locked, 'sanity check: the ReadableStream must not start locked'); + assert_false(ws.locked, 'sanity check: the WritableStream must not start locked'); + + rs.pipeTo(ws); + + assert_true(rs.locked, 'the ReadableStream must become locked'); + assert_true(ws.locked, 'the WritableStream must become locked'); + +}, 'Piping must lock both the ReadableStream and WritableStream'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + + return rs.pipeTo(ws).then(() => { + assert_false(rs.locked, 'the ReadableStream must become unlocked'); + assert_false(ws.locked, 'the WritableStream must become unlocked'); + }); + +}, 'Piping finishing must unlock both the ReadableStream and WritableStream'); + +promise_test(t => { + + const fakeRS = Object.create(ReadableStream.prototype); + const ws = new WritableStream(); + + return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(fakeRS, [ws]), + 'pipeTo should reject with a TypeError'); + +}, 'pipeTo must check the brand of its ReadableStream this value'); + +promise_test(t => { + + const rs = new ReadableStream(); + const fakeWS = Object.create(WritableStream.prototype); + + return promise_rejects_js(t, TypeError, ReadableStream.prototype.pipeTo.apply(rs, [fakeWS]), + 'pipeTo should reject with a TypeError'); + +}, 'pipeTo must check the brand of its WritableStream argument'); + +promise_test(t => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + rs.getReader(); + + assert_true(rs.locked, 'sanity check: the ReadableStream starts locked'); + assert_false(ws.locked, 'sanity check: the WritableStream does not start locked'); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws)).then(() => { + assert_false(ws.locked, 'the WritableStream must still be unlocked'); + }); + +}, 'pipeTo must fail if the ReadableStream is locked, and not lock the WritableStream'); + +promise_test(t => { + + const rs = new ReadableStream(); + const ws = new WritableStream(); + + ws.getWriter(); + + assert_false(rs.locked, 'sanity check: the ReadableStream does not start locked'); + assert_true(ws.locked, 'sanity check: the WritableStream starts locked'); + + return promise_rejects_js(t, TypeError, rs.pipeTo(ws)).then(() => { + assert_false(rs.locked, 'the ReadableStream must still be unlocked'); + }); + +}, 'pipeTo must fail if the WritableStream is locked, and not lock the ReadableStream'); + +promise_test(() => { + + const CHUNKS = 10; + + const rs = new ReadableStream({ + start(c) { + for (let i = 0; i < CHUNKS; ++i) { + c.enqueue(i); + } + c.close(); + } + }); + + const written = []; + const ws = new WritableStream({ + write(chunk) { + written.push(chunk); + }, + close() { + written.push('closed'); + } + }, new CountQueuingStrategy({ highWaterMark: CHUNKS })); + + return rs.pipeTo(ws).then(() => { + const targetValues = []; + for (let i = 0; i < CHUNKS; ++i) { + targetValues.push(i); + } + targetValues.push('closed'); + + assert_array_equals(written, targetValues, 'the correct values must be written'); + + // Ensure both readable and writable are closed by the time the pipe finishes. + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + + // NOTE: no requirement on *when* the pipe finishes; that is left to implementations. + +}, 'Piping from a ReadableStream from which lots of chunks are synchronously readable'); + +promise_test(t => { + + let controller; + const rs = recordingReadableStream({ + start(c) { + controller = c; + } + }); + + const ws = recordingWritableStream(); + + const pipePromise = rs.pipeTo(ws).then(() => { + assert_array_equals(ws.events, ['write', 'Hello', 'close']); + }); + + t.step_timeout(() => { + controller.enqueue('Hello'); + t.step_timeout(() => controller.close(), 10); + }, 10); + + return pipePromise; + +}, 'Piping from a ReadableStream for which a chunk becomes asynchronously readable after the pipeTo'); + +for (const preventAbort of [true, false]) { + promise_test(() => { + + const rs = new ReadableStream({ + pull() { + return Promise.reject(undefined); + } + }); + + return rs.pipeTo(new WritableStream(), { preventAbort }).then( + () => assert_unreached('pipeTo promise should be rejected'), + value => assert_equals(value, undefined, 'rejection value should be undefined')); + + }, `an undefined rejection from pull should cause pipeTo() to reject when preventAbort is ${preventAbort}`); +} + +for (const preventCancel of [true, false]) { + promise_test(() => { + + const rs = new ReadableStream({ + pull(controller) { + controller.enqueue(0); + } + }); + + const ws = new WritableStream({ + write() { + return Promise.reject(undefined); + } + }); + + return rs.pipeTo(ws, { preventCancel }).then( + () => assert_unreached('pipeTo promise should be rejected'), + value => assert_equals(value, undefined, 'rejection value should be undefined')); + + }, `an undefined rejection from write should cause pipeTo() to reject when preventCancel is ${preventCancel}`); +} + +promise_test(t => { + const rs = new ReadableStream(); + const ws = new WritableStream(); + return promise_rejects_js(t, TypeError, rs.pipeTo(ws, { + get preventAbort() { + ws.getWriter(); + } + }), 'pipeTo should reject'); +}, 'pipeTo() should reject if an option getter grabs a writer'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const ws = new WritableStream(); + + return rs.pipeTo(ws, null); +}, 'pipeTo() promise should resolve if null is passed'); diff --git a/test/js/third_party/wpt-streams/streams/piping/multiple-propagation.any.js b/test/js/third_party/wpt-streams/streams/piping/multiple-propagation.any.js new file mode 100644 index 000000000000..a78652fc0679 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/multiple-propagation.any.js @@ -0,0 +1,227 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1!'); +error1.name = 'error1'; + +const error2 = new Error('error2!'); +error2.name = 'error2'; + +function createErroredWritableStream(t) { + return Promise.resolve().then(() => { + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + const writer = ws.getWriter(); + return promise_rejects_exactly(t, error2, writer.closed, 'the writable stream must be errored with error2') + .then(() => { + writer.releaseLock(); + assert_array_equals(ws.events, []); + return ws; + }); + }); +} + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + // Trying to abort a stream that is erroring will give the writable's error + return promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error2, ws.getWriter().closed, 'the writable stream must be errored with error2') + ]); + }); + +}, 'Piping from an errored readable stream to an erroring writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'); + }); +}, 'Piping from an errored readable stream to an errored writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error2); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the readable stream\'s error') + .then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error2, ws.getWriter().closed, 'the writable stream must be errored with error2') + ]); + }); + +}, 'Piping from an errored readable stream to an erroring writable stream; preventAbort = true'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error1, rs.pipeTo(ws, { preventAbort: true }), + 'pipeTo must reject with the readable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'); + }); + +}, 'Piping from an errored readable stream to an errored writable stream; preventAbort = true'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + const closePromise = writer.close(); + writer.releaseLock(); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['abort', error1]); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + promise_rejects_exactly(t, error1, ws.getWriter().closed, + 'closed must reject with error1'), + promise_rejects_exactly(t, error1, closePromise, + 'close() must reject with error1') + ]); + }); + +}, 'Piping from an errored readable stream to a closing writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.error(error1); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + const closePromise = writer.close(); + writer.releaseLock(); + + return flushAsyncEvents().then(() => { + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the readable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + promise_rejects_exactly(t, error1, rs.getReader().closed, 'the readable stream must be errored with error1'), + ws.getWriter().closed, + closePromise + ]); + }); + }); + +}, 'Piping from an errored readable stream to a closed writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + const ws = recordingWritableStream({ + start(c) { + c.error(error1); + } + }); + + return promise_rejects_exactly(t, error1, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error').then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, []); + + return Promise.all([ + rs.getReader().closed, + promise_rejects_exactly(t, error1, ws.getWriter().closed, 'the writable stream must be errored with error1') + ]); + }); + +}, 'Piping from a closed readable stream to an erroring writable stream'); + +promise_test(t => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + return createErroredWritableStream(t) + .then(ws => promise_rejects_exactly(t, error2, rs.pipeTo(ws), 'pipeTo must reject with the writable stream\'s error')) + .then(() => { + assert_array_equals(rs.events, []); + + return rs.getReader().closed; + }); + +}, 'Piping from a closed readable stream to an errored writable stream'); + +promise_test(() => { + const rs = recordingReadableStream({ + start(c) { + c.close(); + } + }); + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return rs.pipeTo(ws).then(() => { + assert_array_equals(rs.events, []); + assert_array_equals(ws.events, ['close']); + + return Promise.all([ + rs.getReader().closed, + ws.getWriter().closed + ]); + }); + +}, 'Piping from a closed readable stream to a closed writable stream'); diff --git a/test/js/third_party/wpt-streams/streams/piping/pipe-through.any.js b/test/js/third_party/wpt-streams/streams/piping/pipe-through.any.js new file mode 100644 index 000000000000..26b1cd26a3c8 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/pipe-through.any.js @@ -0,0 +1,331 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +function duckTypedPassThroughTransform() { + let enqueueInReadable; + let closeReadable; + + return { + writable: new WritableStream({ + write(chunk) { + enqueueInReadable(chunk); + }, + + close() { + closeReadable(); + } + }), + + readable: new ReadableStream({ + start(c) { + enqueueInReadable = c.enqueue.bind(c); + closeReadable = c.close.bind(c); + } + }) + }; +} + +function uninterestingReadableWritablePair() { + return { writable: new WritableStream(), readable: new ReadableStream() }; +} + +promise_test(() => { + const readableEnd = sequentialReadableStream(5).pipeThrough(duckTypedPassThroughTransform()); + + return readableStreamToArray(readableEnd).then(chunks => + assert_array_equals(chunks, [1, 2, 3, 4, 5]), 'chunks should match'); +}, 'Piping through a duck-typed pass-through transform stream should work'); + +promise_test(() => { + const transform = { + writable: new WritableStream({ + start(c) { + c.error(new Error('this rejection should not be reported as unhandled')); + } + }), + readable: new ReadableStream() + }; + + sequentialReadableStream(5).pipeThrough(transform); + + // The test harness should complain about unhandled rejections by then. + return flushAsyncEvents(); + +}, 'Piping through a transform errored on the writable end does not cause an unhandled promise rejection'); + +test(() => { + let calledPipeTo = false; + class BadReadableStream extends ReadableStream { + pipeTo() { + calledPipeTo = true; + } + } + + const brs = new BadReadableStream({ + start(controller) { + controller.close(); + } + }); + const readable = new ReadableStream(); + const writable = new WritableStream(); + const result = brs.pipeThrough({ readable, writable }); + + assert_false(calledPipeTo, 'the overridden pipeTo should not have been called'); + assert_equals(result, readable, 'return value should be the passed readable property'); +}, 'pipeThrough should not call pipeTo on this'); + +test(t => { + let calledFakePipeTo = false; + const realPipeTo = ReadableStream.prototype.pipeTo; + t.add_cleanup(() => { + ReadableStream.prototype.pipeTo = realPipeTo; + }); + ReadableStream.prototype.pipeTo = () => { + calledFakePipeTo = true; + }; + const rs = new ReadableStream(); + const readable = new ReadableStream(); + const writable = new WritableStream(); + const result = rs.pipeThrough({ readable, writable }); + + assert_false(calledFakePipeTo, 'the monkey-patched pipeTo should not have been called'); + assert_equals(result, readable, 'return value should be the passed readable property'); + +}, 'pipeThrough should not call pipeTo on the ReadableStream prototype'); + +const badReadables = [null, undefined, 0, NaN, true, 'ReadableStream', Object.create(ReadableStream.prototype)]; +for (const readable of badReadables) { + test(() => { + assert_throws_js(TypeError, + ReadableStream.prototype.pipeThrough.bind(readable, uninterestingReadableWritablePair()), + 'pipeThrough should throw'); + }, `pipeThrough should brand-check this and not allow '${readable}'`); + + test(() => { + const rs = new ReadableStream(); + let writableGetterCalled = false; + assert_throws_js( + TypeError, + () => rs.pipeThrough({ + get writable() { + writableGetterCalled = true; + return new WritableStream(); + }, + readable + }), + 'pipeThrough should brand-check readable' + ); + assert_false(writableGetterCalled, 'writable should not have been accessed'); + }, `pipeThrough should brand-check readable and not allow '${readable}'`); +} + +const badWritables = [null, undefined, 0, NaN, true, 'WritableStream', Object.create(WritableStream.prototype)]; +for (const writable of badWritables) { + test(() => { + const rs = new ReadableStream({ + start(c) { + c.close(); + } + }); + let readableGetterCalled = false; + assert_throws_js(TypeError, () => rs.pipeThrough({ + get readable() { + readableGetterCalled = true; + return new ReadableStream(); + }, + writable + }), + 'pipeThrough should brand-check writable'); + assert_true(readableGetterCalled, 'readable should have been accessed'); + }, `pipeThrough should brand-check writable and not allow '${writable}'`); +} + +test(t => { + const error = new Error(); + error.name = 'custom'; + + const rs = new ReadableStream({ + pull: t.unreached_func('pull should not be called') + }, { highWaterMark: 0 }); + + const throwingWritable = { + readable: rs, + get writable() { + throw error; + } + }; + assert_throws_exactly(error, + () => ReadableStream.prototype.pipeThrough.call(rs, throwingWritable, {}), + 'pipeThrough should rethrow the error thrown by the writable getter'); + + const throwingReadable = { + get readable() { + throw error; + }, + writable: {} + }; + assert_throws_exactly(error, + () => ReadableStream.prototype.pipeThrough.call(rs, throwingReadable, {}), + 'pipeThrough should rethrow the error thrown by the readable getter'); + +}, 'pipeThrough should rethrow errors from accessing readable or writable'); + +const badSignals = [null, 0, NaN, true, 'AbortSignal', Object.create(AbortSignal.prototype)]; +for (const signal of badSignals) { + test(() => { + const rs = new ReadableStream(); + assert_throws_js(TypeError, () => rs.pipeThrough(uninterestingReadableWritablePair(), { signal }), + 'pipeThrough should throw'); + }, `invalid values of signal should throw; specifically '${signal}'`); +} + +test(() => { + const rs = new ReadableStream(); + const controller = new AbortController(); + const signal = controller.signal; + rs.pipeThrough(uninterestingReadableWritablePair(), { signal }); +}, 'pipeThrough should accept a real AbortSignal'); + +test(() => { + const rs = new ReadableStream(); + rs.getReader(); + assert_throws_js(TypeError, () => rs.pipeThrough(uninterestingReadableWritablePair()), + 'pipeThrough should throw'); +}, 'pipeThrough should throw if this is locked'); + +test(() => { + const rs = new ReadableStream(); + const writable = new WritableStream(); + const readable = new ReadableStream(); + writable.getWriter(); + assert_throws_js(TypeError, () => rs.pipeThrough({writable, readable}), + 'pipeThrough should throw'); +}, 'pipeThrough should throw if writable is locked'); + +test(() => { + const rs = new ReadableStream(); + const writable = new WritableStream(); + const readable = new ReadableStream(); + readable.getReader(); + assert_equals(rs.pipeThrough({ writable, readable }), readable, + 'pipeThrough should not throw'); +}, 'pipeThrough should not care if readable is locked'); + +promise_test(() => { + const rs = recordingReadableStream(); + const writable = new WritableStream({ + start(controller) { + controller.error(); + } + }); + const readable = new ReadableStream(); + rs.pipeThrough({ writable, readable }, { preventCancel: true }); + return flushAsyncEvents(0).then(() => { + assert_array_equals(rs.events, ['pull'], 'cancel should not have been called'); + }); +}, 'preventCancel should work'); + +promise_test(() => { + const rs = new ReadableStream({ + start(controller) { + controller.close(); + } + }); + const writable = recordingWritableStream(); + const readable = new ReadableStream(); + rs.pipeThrough({ writable, readable }, { preventClose: true }); + return flushAsyncEvents(0).then(() => { + assert_array_equals(writable.events, [], 'writable should not be closed'); + }); +}, 'preventClose should work'); + +promise_test(() => { + const rs = new ReadableStream({ + start(controller) { + controller.error(); + } + }); + const writable = recordingWritableStream(); + const readable = new ReadableStream(); + rs.pipeThrough({ writable, readable }, { preventAbort: true }); + return flushAsyncEvents(0).then(() => { + assert_array_equals(writable.events, [], 'writable should not be aborted'); + }); +}, 'preventAbort should work'); + +test(() => { + const rs = new ReadableStream(); + const readable = new ReadableStream(); + const writable = new WritableStream(); + assert_throws_js(TypeError, () => rs.pipeThrough({readable, writable}, { + get preventAbort() { + writable.getWriter(); + } + }), 'pipeThrough should throw'); +}, 'pipeThrough() should throw if an option getter grabs a writer'); + +test(() => { + const rs = new ReadableStream(); + const readable = new ReadableStream(); + const writable = new WritableStream(); + rs.pipeThrough({readable, writable}, null); +}, 'pipeThrough() should not throw if option is null'); + +test(() => { + const rs = new ReadableStream(); + const readable = new ReadableStream(); + const writable = new WritableStream(); + rs.pipeThrough({readable, writable}, {signal:undefined}); +}, 'pipeThrough() should not throw if signal is undefined'); + +function tryPipeThrough(pair, options) +{ + const rs = new ReadableStream(); + if (!pair) + pair = {readable:new ReadableStream(), writable:new WritableStream()}; + try { + rs.pipeThrough(pair, options) + } catch (e) { + return e; + } +} + +test(() => { + let result = tryPipeThrough({ + get readable() { + return new ReadableStream(); + }, + get writable() { + throw "writable threw"; + } + }, { }); + assert_equals(result, "writable threw"); + + result = tryPipeThrough({ + get readable() { + throw "readable threw"; + }, + get writable() { + throw "writable threw"; + } + }, { }); + assert_equals(result, "readable threw"); + + result = tryPipeThrough({ + get readable() { + throw "readable threw"; + }, + get writable() { + throw "writable threw"; + } + }, { + get preventAbort() { + throw "preventAbort threw"; + } + }); + assert_equals(result, "readable threw"); + +}, 'pipeThrough() should throw if readable/writable getters throw'); diff --git a/test/js/third_party/wpt-streams/streams/piping/then-interception.any.js b/test/js/third_party/wpt-streams/streams/piping/then-interception.any.js new file mode 100644 index 000000000000..543f916d940d --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/then-interception.any.js @@ -0,0 +1,68 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +function interceptThen() { + const intercepted = []; + let callCount = 0; + Object.prototype.then = function(resolver) { + if (!this.done) { + intercepted.push(this.value); + } + const retval = Object.create(null); + retval.done = ++callCount === 3; + retval.value = callCount; + resolver(retval); + if (retval.done) { + delete Object.prototype.then; + } + } + return intercepted; +} + +promise_test(async t => { + const rs = new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.close(); + } + }); + const ws = recordingWritableStream(); + + const intercepted = interceptThen(); + t.add_cleanup(() => { + delete Object.prototype.then; + }); + + await rs.pipeTo(ws); + delete Object.prototype.then; + + + assert_array_equals(intercepted, [], 'nothing should have been intercepted'); + assert_array_equals(ws.events, ['write', 'a', 'close'], 'written chunk should be "a"'); +}, 'piping should not be observable'); + +promise_test(async t => { + const rs = new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.close(); + } + }); + const ws = recordingWritableStream(); + + const [ branch1, branch2 ] = rs.tee(); + + const intercepted = interceptThen(); + t.add_cleanup(() => { + delete Object.prototype.then; + }); + + await branch1.pipeTo(ws); + delete Object.prototype.then; + branch2.cancel(); + + assert_array_equals(intercepted, [], 'nothing should have been intercepted'); + assert_array_equals(ws.events, ['write', 'a', 'close'], 'written chunk should be "a"'); +}, 'tee should not be observable'); diff --git a/test/js/third_party/wpt-streams/streams/piping/throwing-options.any.js b/test/js/third_party/wpt-streams/streams/piping/throwing-options.any.js new file mode 100644 index 000000000000..b9f906778f63 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/throwing-options.any.js @@ -0,0 +1,65 @@ +// META: global=window,worker +'use strict'; + +class ThrowingOptions { + constructor(whatShouldThrow) { + this.whatShouldThrow = whatShouldThrow; + this.touched = []; + } + + get preventClose() { + this.maybeThrow('preventClose'); + return false; + } + + get preventAbort() { + this.maybeThrow('preventAbort'); + return false; + } + + get preventCancel() { + this.maybeThrow('preventCancel'); + return false; + } + + get signal() { + this.maybeThrow('signal'); + return undefined; + } + + maybeThrow(forWhat) { + this.touched.push(forWhat); + if (this.whatShouldThrow === forWhat) { + throw new Error(this.whatShouldThrow); + } + } +} + +const checkOrder = ['preventAbort', 'preventCancel', 'preventClose', 'signal']; + +for (let i = 0; i < checkOrder.length; ++i) { + const whatShouldThrow = checkOrder[i]; + const whatShouldBeTouched = checkOrder.slice(0, i + 1); + + promise_test(t => { + const options = new ThrowingOptions(whatShouldThrow); + return promise_rejects_js( + t, Error, + new ReadableStream().pipeTo(new WritableStream(), options), + 'pipeTo should reject') + .then(() => assert_array_equals( + options.touched, whatShouldBeTouched, + 'options should be touched in the right order')); + }, `pipeTo should stop after getting ${whatShouldThrow} throws`); + + test(() => { + const options = new ThrowingOptions(whatShouldThrow); + assert_throws_js( + Error, + () => new ReadableStream().pipeThrough(new TransformStream(), options), + 'pipeThrough should throw'); + assert_array_equals( + options.touched, whatShouldBeTouched, + 'options should be touched in the right order'); + }, `pipeThrough should stop after getting ${whatShouldThrow} throws`); +} diff --git a/test/js/third_party/wpt-streams/streams/piping/transform-streams.any.js b/test/js/third_party/wpt-streams/streams/piping/transform-streams.any.js new file mode 100644 index 000000000000..caae9fbad884 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/piping/transform-streams.any.js @@ -0,0 +1,22 @@ +// META: global=window,worker +'use strict'; + +promise_test(() => { + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.enqueue('c'); + c.close(); + } + }); + + const ts = new TransformStream(); + + const ws = new WritableStream(); + + return rs.pipeThrough(ts).pipeTo(ws).then(() => { + const writer = ws.getWriter(); + return writer.closed; + }); +}, 'Piping through an identity transform stream should close the destination when the source closes'); diff --git a/test/js/third_party/wpt-streams/streams/queuing-strategies.any.js b/test/js/third_party/wpt-streams/streams/queuing-strategies.any.js new file mode 100644 index 000000000000..fa959ebba283 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/queuing-strategies.any.js @@ -0,0 +1,150 @@ +// META: global=window,worker +'use strict'; + +const highWaterMarkConversions = new Map([ + [-Infinity, -Infinity], + [-5, -5], + [false, 0], + [true, 1], + [NaN, NaN], + ['foo', NaN], + ['0', 0], + [{}, NaN], + [() => {}, NaN] +]); + +for (const QueuingStrategy of [CountQueuingStrategy, ByteLengthQueuingStrategy]) { + test(() => { + new QueuingStrategy({ highWaterMark: 4 }); + }, `${QueuingStrategy.name}: Can construct a with a valid high water mark`); + + test(() => { + const highWaterMark = 1; + const highWaterMarkObjectGetter = { + get highWaterMark() { return highWaterMark; } + }; + const error = new Error('wow!'); + const highWaterMarkObjectGetterThrowing = { + get highWaterMark() { throw error; } + }; + + assert_throws_js(TypeError, () => new QueuingStrategy(), 'construction fails with undefined'); + assert_throws_js(TypeError, () => new QueuingStrategy(null), 'construction fails with null'); + assert_throws_js(TypeError, () => new QueuingStrategy(true), 'construction fails with true'); + assert_throws_js(TypeError, () => new QueuingStrategy(5), 'construction fails with 5'); + assert_throws_js(TypeError, () => new QueuingStrategy({}), 'construction fails with {}'); + assert_throws_exactly(error, () => new QueuingStrategy(highWaterMarkObjectGetterThrowing), + 'construction fails with an object with a throwing highWaterMark getter'); + + assert_equals((new QueuingStrategy(highWaterMarkObjectGetter)).highWaterMark, highWaterMark); + }, `${QueuingStrategy.name}: Constructor behaves as expected with strange arguments`); + + test(() => { + for (const [input, output] of highWaterMarkConversions.entries()) { + const strategy = new QueuingStrategy({ highWaterMark: input }); + assert_equals(strategy.highWaterMark, output, `${input} gets set correctly`); + } + }, `${QueuingStrategy.name}: highWaterMark constructor values are converted per the unrestricted double rules`); + + test(() => { + const size1 = (new QueuingStrategy({ highWaterMark: 5 })).size; + const size2 = (new QueuingStrategy({ highWaterMark: 10 })).size; + + assert_equals(size1, size2); + }, `${QueuingStrategy.name}: size is the same function across all instances`); + + test(() => { + const size = (new QueuingStrategy({ highWaterMark: 5 })).size; + assert_equals(size.name, 'size'); + }, `${QueuingStrategy.name}: size should have the right name`); + + test(() => { + class SubClass extends QueuingStrategy { + size() { + return 2; + } + + subClassMethod() { + return true; + } + } + + const sc = new SubClass({ highWaterMark: 77 }); + assert_equals(sc.constructor.name, 'SubClass', 'constructor.name should be correct'); + assert_equals(sc.highWaterMark, 77, 'highWaterMark should come from the parent class'); + assert_equals(sc.size(), 2, 'size() on the subclass should override the parent'); + assert_true(sc.subClassMethod(), 'subClassMethod() should work'); + }, `${QueuingStrategy.name}: subclassing should work correctly`); + + test(() => { + const size = new QueuingStrategy({ highWaterMark: 5 }).size; + assert_false('prototype' in size); + }, `${QueuingStrategy.name}: size should not have a prototype property`); +} + +test(() => { + const size = new CountQueuingStrategy({ highWaterMark: 5 }).size; + assert_throws_js(TypeError, () => new size()); +}, `CountQueuingStrategy: size should not be a constructor`); + +test(() => { + const size = new ByteLengthQueuingStrategy({ highWaterMark: 5 }).size; + assert_throws_js(TypeError, () => new size({ byteLength: 1024 })); +}, `ByteLengthQueuingStrategy: size should not be a constructor`); + +test(() => { + const size = (new CountQueuingStrategy({ highWaterMark: 5 })).size; + assert_equals(size.length, 0); +}, 'CountQueuingStrategy: size should have the right length'); + +test(() => { + const size = (new ByteLengthQueuingStrategy({ highWaterMark: 5 })).size; + assert_equals(size.length, 1); +}, 'ByteLengthQueuingStrategy: size should have the right length'); + +test(() => { + const size = 1024; + const chunk = { byteLength: size }; + const chunkGetter = { + get byteLength() { return size; } + }; + const error = new Error('wow!'); + const chunkGetterThrowing = { + get byteLength() { throw error; } + }; + + const sizeFunction = (new CountQueuingStrategy({ highWaterMark: 5 })).size; + + assert_equals(sizeFunction(), 1, 'size returns 1 with undefined'); + assert_equals(sizeFunction(null), 1, 'size returns 1 with null'); + assert_equals(sizeFunction('potato'), 1, 'size returns 1 with non-object type'); + assert_equals(sizeFunction({}), 1, 'size returns 1 with empty object'); + assert_equals(sizeFunction(chunk), 1, 'size returns 1 with a chunk'); + assert_equals(sizeFunction(chunkGetter), 1, 'size returns 1 with chunk getter'); + assert_equals(sizeFunction(chunkGetterThrowing), 1, + 'size returns 1 with chunk getter that throws'); +}, 'CountQueuingStrategy: size behaves as expected with strange arguments'); + +test(() => { + const size = 1024; + const chunk = { byteLength: size }; + const chunkGetter = { + get byteLength() { return size; } + }; + const error = new Error('wow!'); + const chunkGetterThrowing = { + get byteLength() { throw error; } + }; + + const sizeFunction = (new ByteLengthQueuingStrategy({ highWaterMark: 5 })).size; + + assert_throws_js(TypeError, () => sizeFunction(), 'size fails with undefined'); + assert_throws_js(TypeError, () => sizeFunction(null), 'size fails with null'); + assert_equals(sizeFunction('potato'), undefined, 'size succeeds with undefined with a random non-object type'); + assert_equals(sizeFunction({}), undefined, 'size succeeds with undefined with an object without hwm property'); + assert_equals(sizeFunction(chunk), size, 'size succeeds with the right amount with an object with a hwm'); + assert_equals(sizeFunction(chunkGetter), size, + 'size succeeds with the right amount with an object with a hwm getter'); + assert_throws_exactly(error, () => sizeFunction(chunkGetterThrowing), + 'size fails with the error thrown by the getter'); +}, 'ByteLengthQueuingStrategy: size behaves as expected with strange arguments'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/bad-buffers-and-views.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/bad-buffers-and-views.any.js new file mode 100644 index 000000000000..0f018d6d7819 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/bad-buffers-and-views.any.js @@ -0,0 +1,391 @@ +// META: global=window,worker +'use strict'; + +promise_test(() => { + const stream = new ReadableStream({ + start(c) { + c.close(); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const view = new Uint8Array([1, 2, 3]); + return reader.read(view).then(({ value, done }) => { + // Sanity checks + assert_true(value instanceof Uint8Array, 'The value read must be a Uint8Array'); + assert_not_equals(value, view, 'The value read must not be the *same* Uint8Array'); + assert_array_equals(value, [], 'The value read must be an empty Uint8Array, since the stream is closed'); + assert_true(done, 'done must be true, since the stream is closed'); + + // The important assertions + assert_not_equals(value.buffer, view.buffer, 'a different ArrayBuffer must underlie the value'); + assert_equals(view.buffer.byteLength, 0, 'the original buffer must be detached'); + }); +}, 'ReadableStream with byte source: read()ing from a closed stream still transfers the buffer'); + +promise_test(() => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array([1, 2, 3])); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const view = new Uint8Array([4, 5, 6]); + return reader.read(view).then(({ value, done }) => { + // Sanity checks + assert_true(value instanceof Uint8Array, 'The value read must be a Uint8Array'); + assert_not_equals(value, view, 'The value read must not be the *same* Uint8Array'); + assert_array_equals(value, [1, 2, 3], 'The value read must be the enqueued Uint8Array, not the original values'); + assert_false(done, 'done must be false, since the stream is not closed'); + + // The important assertions + assert_not_equals(value.buffer, view.buffer, 'a different ArrayBuffer must underlie the value'); + assert_equals(view.buffer.byteLength, 0, 'the original buffer must be detached'); + }); +}, 'ReadableStream with byte source: read()ing from a stream with queued chunks still transfers the buffer'); + +test(() => { + new ReadableStream({ + start(c) { + const view = new Uint8Array([1, 2, 3]); + c.enqueue(view); + assert_throws_js(TypeError, () => c.enqueue(view)); + }, + type: 'bytes' + }); +}, 'ReadableStream with byte source: enqueuing an already-detached buffer throws'); + +test(() => { + new ReadableStream({ + start(c) { + const view = new Uint8Array([]); + assert_throws_js(TypeError, () => c.enqueue(view)); + }, + type: 'bytes' + }); +}, 'ReadableStream with byte source: enqueuing a zero-length buffer throws'); + +test(() => { + new ReadableStream({ + start(c) { + const view = new Uint8Array(new ArrayBuffer(10), 0, 0); + assert_throws_js(TypeError, () => c.enqueue(view)); + }, + type: 'bytes' + }); +}, 'ReadableStream with byte source: enqueuing a zero-length view on a non-zero-length buffer throws'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array([1, 2, 3])); + }, + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + const view = new Uint8Array([4, 5, 6]); + return reader.read(view).then(() => { + // view is now detached + return promise_rejects_js(t, TypeError, reader.read(view)); + }); +}, 'ReadableStream with byte source: reading into an already-detached buffer rejects'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array([1, 2, 3])); + }, + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + const view = new Uint8Array(); + return promise_rejects_js(t, TypeError, reader.read(view)); +}, 'ReadableStream with byte source: reading into a zero-length buffer rejects'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array([1, 2, 3])); + }, + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + const view = new Uint8Array(new ArrayBuffer(10), 0, 0); + return promise_rejects_js(t, TypeError, reader.read(view)); +}, 'ReadableStream with byte source: reading into a zero-length view on a non-zero-length buffer rejects'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + c.byobRequest.view.buffer.transfer(); + + assert_throws_js(TypeError, () => c.byobRequest.respond(1), + 'respond() must throw if the corresponding view has become detached'); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respond() throws if the BYOB request\'s buffer has been detached (in the ' + + 'readable state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + c.close(); + c.byobRequest.view.buffer.transfer(); + + assert_throws_js(TypeError, () => c.byobRequest.respond(0), + 'respond() must throw if the corresponding view has become detached'); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respond() throws if the BYOB request\'s buffer has been detached (in the ' + + 'closed state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array([1, 2, 3]); + view.buffer.transfer(); + + assert_throws_js(TypeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view\'s buffer has been detached ' + + '(in the readable state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(); + + assert_throws_js(TypeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view\'s buffer is zero-length ' + + '(in the readable state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(c.byobRequest.view.buffer, 0, 0); + + assert_throws_js(TypeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view is zero-length on a ' + + 'non-zero-length buffer (in the readable state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = c.byobRequest.view.subarray(1, 2); + + assert_throws_js(RangeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view has a different offset ' + + '(in the readable state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + c.close(); + + const view = c.byobRequest.view.subarray(1, 1); + + assert_throws_js(RangeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view has a different offset ' + + '(in the closed state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(new ArrayBuffer(10), 0, 3); + + assert_throws_js(RangeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view\'s buffer has a ' + + 'different length (in the readable state)'); + +async_test(t => { + // Tests https://github.com/nodejs/node/issues/41886 + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(new ArrayBuffer(11), 0, 3); + + assert_throws_js(RangeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes', + autoAllocateChunkSize: 10 + }); + const reader = stream.getReader(); + + reader.read(); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view\'s buffer has a ' + + 'different length (autoAllocateChunkSize)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(c.byobRequest.view.buffer, 0, 4); + view[0] = 20; + view[1] = 21; + view[2] = 22; + view[3] = 23; + + assert_throws_js(RangeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + const buffer = new ArrayBuffer(10); + const view = new Uint8Array(buffer, 0, 3); + view[0] = 10; + view[1] = 11; + view[2] = 12; + reader.read(view); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view has a larger length ' + + '(in the readable state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + c.close(); + + // Detach it by reading into it + const view = new Uint8Array([1, 2, 3]); + reader.read(view); + + assert_throws_js(TypeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view\'s buffer has been detached ' + + '(in the closed state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(); + + c.close(); + + assert_throws_js(RangeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view\'s buffer is zero-length ' + + '(in the closed state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(c.byobRequest.view.buffer, 0, 1); + + c.close(); + + assert_throws_js(TypeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view is non-zero-length ' + + '(in the closed state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + const view = new Uint8Array(new ArrayBuffer(10), 0, 0); + + c.close(); + + assert_throws_js(RangeError, () => c.byobRequest.respondWithNewView(view)); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: respondWithNewView() throws if the supplied view\'s buffer has a ' + + 'different length (in the closed state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + c.byobRequest.view.buffer.transfer(); + + assert_throws_js(TypeError, () => c.enqueue(new Uint8Array([1])), + 'enqueue() must throw if the BYOB request\'s buffer has become detached'); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: enqueue() throws if the BYOB request\'s buffer has been detached (in the ' + + 'readable state)'); + +async_test(t => { + const stream = new ReadableStream({ + pull: t.step_func_done(c => { + c.close(); + c.byobRequest.view.buffer.transfer(); + + assert_throws_js(TypeError, () => c.enqueue(new Uint8Array([1])), + 'enqueue() must throw if the BYOB request\'s buffer has become detached'); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + reader.read(new Uint8Array([4, 5, 6])); +}, 'ReadableStream with byte source: enqueue() throws if the BYOB request\'s buffer has been detached (in the ' + + 'closed state)'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/construct-byob-request.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/construct-byob-request.any.js new file mode 100644 index 000000000000..8d460a1c81b7 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/construct-byob-request.any.js @@ -0,0 +1,53 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +'use strict'; + +// Prior to whatwg/stream#870 it was possible to construct a ReadableStreamBYOBRequest directly. This made it possible +// to construct requests that were out-of-sync with the state of the ReadableStream. They could then be used to call +// internal operations, resulting in asserts or bad behaviour. This file contains regression tests for the change. + +function getRealByteStreamController() { + let controller; + new ReadableStream({ + start(c) { + controller = c; + }, + type: 'bytes' + }); + return controller; +} + +// Create an object pretending to have prototype |prototype|, of type |type|. |type| is one of "undefined", "null", +// "fake", or "real". "real" will call the realObjectCreator function to get a real instance of the object. +function createDummyObject(prototype, type, realObjectCreator) { + switch (type) { + case 'undefined': + return undefined; + + case 'null': + return null; + + case 'fake': + return Object.create(prototype); + + case 'real': + return realObjectCreator(); + } + + throw new Error('not reached'); +} + +const dummyTypes = ['undefined', 'null', 'fake', 'real']; + +for (const controllerType of dummyTypes) { + const controller = createDummyObject(ReadableByteStreamController.prototype, controllerType, + getRealByteStreamController); + for (const viewType of dummyTypes) { + const view = createDummyObject(Uint8Array.prototype, viewType, () => new Uint8Array(16)); + test(() => { + assert_throws_js(TypeError, () => new ReadableStreamBYOBRequest(controller, view), + 'constructor should throw'); + }, `ReadableStreamBYOBRequest constructor should throw when passed a ${controllerType} ` + + `ReadableByteStreamController and a ${viewType} view`); + } +} diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/crashtests/tee-locked-stream.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/crashtests/tee-locked-stream.any.js new file mode 100644 index 000000000000..285b427e2778 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/crashtests/tee-locked-stream.any.js @@ -0,0 +1,9 @@ +// META: global=window,worker +'use strict'; + +test(() => { + const byteReadable = new ReadableStream({type: 'bytes'}); + byteReadable.getReader(); + assert_throws_js(TypeError, () => byteReadable.tee(), 'byteReadable.tee() must throw'); +}, 'tee() on a locked byte stream does not crash'); + diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/enqueue-with-detached-buffer.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/enqueue-with-detached-buffer.any.js new file mode 100644 index 000000000000..d2b37f00a9d6 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/enqueue-with-detached-buffer.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker + +promise_test(async t => { + const error = new Error('cannot proceed'); + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((controller) => { + const buffer = controller.byobRequest.view.buffer; + // Detach the buffer. + structuredClone(buffer, { transfer: [buffer] }); + + // Try to enqueue with a new buffer. + assert_throws_js(TypeError, () => controller.enqueue(new Uint8Array([42]))); + + // If we got here the test passed. + controller.error(error); + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + await promise_rejects_exactly(t, error, reader.read(new Uint8Array(1))); +}, 'enqueue after detaching byobRequest.view.buffer should throw'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/general.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/general.any.js new file mode 100644 index 000000000000..6787ce1b474b --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/general.any.js @@ -0,0 +1,2987 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +test(() => { + assert_throws_js(TypeError, () => new ReadableStream().getReader({ mode: 'byob' })); +}, 'getReader({mode: "byob"}) throws on non-bytes streams'); + + +test(() => { + // Constructing ReadableStream with an empty underlying byte source object as parameter shouldn't throw. + new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' }); + // Constructor must perform ToString(type). + new ReadableStream({ type: { toString() {return 'bytes';} } }) + .getReader({ mode: 'byob' }); + new ReadableStream({ type: { toString: null, valueOf() {return 'bytes';} } }) + .getReader({ mode: 'byob' }); +}, 'ReadableStream with byte source can be constructed with no errors'); + +test(() => { + const ReadableStreamBYOBReader = new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' }).constructor; + const rs = new ReadableStream({ type: 'bytes' }); + + let reader = rs.getReader({ mode: { toString() { return 'byob'; } } }); + assert_true(reader instanceof ReadableStreamBYOBReader, 'must give a BYOB reader'); + reader.releaseLock(); + + reader = rs.getReader({ mode: { toString: null, valueOf() {return 'byob';} } }); + assert_true(reader instanceof ReadableStreamBYOBReader, 'must give a BYOB reader'); + reader.releaseLock(); + + reader = rs.getReader({ mode: 'byob', notmode: 'ignored' }); + assert_true(reader instanceof ReadableStreamBYOBReader, 'must give a BYOB reader'); +}, 'getReader({mode}) must perform ToString()'); + +promise_test(() => { + let startCalled = false; + let startCalledBeforePull = false; + let desiredSize; + let controller; + + let resolveTestPromise; + const testPromise = new Promise(resolve => { + resolveTestPromise = resolve; + }); + + new ReadableStream({ + start(c) { + controller = c; + startCalled = true; + }, + pull() { + startCalledBeforePull = startCalled; + desiredSize = controller.desiredSize; + resolveTestPromise(); + }, + type: 'bytes' + }, { + highWaterMark: 256 + }); + + return testPromise.then(() => { + assert_true(startCalledBeforePull, 'start should be called before pull'); + assert_equals(desiredSize, 256, 'desiredSize should equal highWaterMark'); + }); + +}, 'ReadableStream with byte source: Construct and expect start and pull being called'); + +promise_test(() => { + let pullCount = 0; + let checkedNoPull = false; + + let resolveTestPromise; + const testPromise = new Promise(resolve => { + resolveTestPromise = resolve; + }); + let resolveStartPromise; + + new ReadableStream({ + start() { + return new Promise(resolve => { + resolveStartPromise = resolve; + }); + }, + pull() { + if (checkedNoPull) { + resolveTestPromise(); + } + + ++pullCount; + }, + type: 'bytes' + }, { + highWaterMark: 256 + }); + + Promise.resolve().then(() => { + assert_equals(pullCount, 0); + checkedNoPull = true; + resolveStartPromise(); + }); + + return testPromise; + +}, 'ReadableStream with byte source: No automatic pull call if start doesn\'t finish'); + +test(() => { + assert_throws_js(Error, () => new ReadableStream({ start() { throw new Error(); }, type:'bytes' }), + 'start() can throw an exception with type: bytes'); +}, 'ReadableStream with byte source: start() throws an exception'); + +promise_test(t => { + new ReadableStream({ + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }, { + highWaterMark: 0 + }); + + return Promise.resolve(); +}, 'ReadableStream with byte source: Construct with highWaterMark of 0'); + +test(() => { + new ReadableStream({ + start(c) { + assert_equals(c.desiredSize, 10, 'desiredSize must start at the highWaterMark'); + c.close(); + assert_equals(c.desiredSize, 0, 'after closing, desiredSize must be 0'); + }, + type: 'bytes' + }, { + highWaterMark: 10 + }); +}, 'ReadableStream with byte source: desiredSize when closed'); + +test(() => { + new ReadableStream({ + start(c) { + assert_equals(c.desiredSize, 10, 'desiredSize must start at the highWaterMark'); + c.error(); + assert_equals(c.desiredSize, null, 'after erroring, desiredSize must be null'); + }, + type: 'bytes' + }, { + highWaterMark: 10 + }); +}, 'ReadableStream with byte source: desiredSize when errored'); + +promise_test(t => { + const stream = new ReadableStream({ + type: 'bytes' + }); + + const reader = stream.getReader(); + reader.releaseLock(); + + return promise_rejects_js(t, TypeError, reader.closed, 'closed must reject'); +}, 'ReadableStream with byte source: getReader(), then releaseLock()'); + +promise_test(t => { + const stream = new ReadableStream({ + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + reader.releaseLock(); + + return promise_rejects_js(t, TypeError, reader.closed, 'closed must reject'); +}, 'ReadableStream with byte source: getReader() with mode set to byob, then releaseLock()'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.close(); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader(); + + return reader.closed.then(() => { + assert_throws_js(TypeError, () => stream.getReader(), 'getReader() must throw'); + }); +}, 'ReadableStream with byte source: Test that closing a stream does not release a reader automatically'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.close(); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.closed.then(() => { + assert_throws_js(TypeError, () => stream.getReader({ mode: 'byob' }), 'getReader() must throw'); + }); +}, 'ReadableStream with byte source: Test that closing a stream does not release a BYOB reader automatically'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.error(error1); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader(); + + return promise_rejects_exactly(t, error1, reader.closed, 'closed must reject').then(() => { + assert_throws_js(TypeError, () => stream.getReader(), 'getReader() must throw'); + }); +}, 'ReadableStream with byte source: Test that erroring a stream does not release a reader automatically'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.error(error1); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return promise_rejects_exactly(t, error1, reader.closed, 'closed must reject').then(() => { + assert_throws_js(TypeError, () => stream.getReader({ mode: 'byob' }), 'getReader() must throw'); + }); +}, 'ReadableStream with byte source: Test that erroring a stream does not release a BYOB reader automatically'); + +promise_test(async t => { + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + c.enqueue(new Uint8Array([1, 2, 3])); + } + }); + + const reader1 = rs.getReader({mode: 'byob'}); + reader1.releaseLock(); + + const reader2 = rs.getReader({mode: 'byob'}); + + // Should be a no-op + reader1.releaseLock(); + + const result = await reader2.read(new Uint8Array([0, 0, 0])); + assert_typed_array_equals(result.value, new Uint8Array([1, 2, 3]), + 'read() should still work on reader2 even after reader1 is released'); + assert_false(result.done, 'done'); + +}, 'ReadableStream with byte source: cannot use an already-released BYOB reader to unlock a stream again'); + +promise_test(async t => { + const stream = new ReadableStream({ + type: 'bytes' + }); + + const reader = stream.getReader(); + const read = reader.read(); + reader.releaseLock(); + await promise_rejects_js(t, TypeError, read, 'pending read must reject'); +}, 'ReadableStream with byte source: releaseLock() on ReadableStreamDefaultReader must reject pending read()'); + +promise_test(async t => { + const stream = new ReadableStream({ + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const read = reader.read(new Uint8Array(1)); + reader.releaseLock(); + await promise_rejects_js(t, TypeError, read, 'pending read must reject'); +}, 'ReadableStream with byte source: releaseLock() on ReadableStreamBYOBReader must reject pending read()'); + +promise_test(() => { + let pullCount = 0; + + const stream = new ReadableStream({ + pull() { + ++pullCount; + }, + type: 'bytes' + }, { + highWaterMark: 8 + }); + + stream.getReader(); + + assert_equals(pullCount, 0, 'No pull as start() just finished and is not yet reflected to the state of the stream'); + + return Promise.resolve().then(() => { + assert_equals(pullCount, 1, 'pull must be invoked'); + }); +}, 'ReadableStream with byte source: Automatic pull() after start()'); + +promise_test(() => { + let pullCount = 0; + + const stream = new ReadableStream({ + pull() { + ++pullCount; + }, + type: 'bytes' + }, { + highWaterMark: 0 + }); + + const reader = stream.getReader(); + reader.read(); + + assert_equals(pullCount, 0, 'No pull as start() just finished and is not yet reflected to the state of the stream'); + + return Promise.resolve().then(() => { + assert_equals(pullCount, 1, 'pull must be invoked'); + }); +}, 'ReadableStream with byte source: Automatic pull() after start() and read()'); + +// View buffers are detached after pull() returns, so record the information at the time that pull() was called. +function extractViewInfo(view) { + return { + constructor: view.constructor, + bufferByteLength: view.buffer.byteLength, + byteOffset: view.byteOffset, + byteLength: view.byteLength + }; +} + +promise_test(() => { + let pullCount = 0; + let controller; + const byobRequests = []; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + const byobRequest = controller.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + view[0] = 0x01; + byobRequest.respond(1); + } else if (pullCount === 1) { + view[0] = 0x02; + view[1] = 0x03; + byobRequest.respond(2); + } + + ++pullCount; + }, + type: 'bytes', + autoAllocateChunkSize: 16 + }, { + highWaterMark: 0 + }); + + const reader = stream.getReader(); + const p0 = reader.read(); + const p1 = reader.read(); + + assert_equals(pullCount, 0, 'No pull() as start() just finished and is not yet reflected to the state of the stream'); + + return Promise.resolve().then(() => { + assert_equals(pullCount, 1, 'pull() must have been invoked once'); + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 16, 'first view.buffer.byteLength should be 16'); + assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 16, 'first view.byteLength should be 16'); + + return p0; + }).then(result => { + assert_equals(pullCount, 2, 'pull() must have been invoked twice'); + const value = result.value; + assert_not_equals(value, undefined, 'first read should have a value'); + assert_equals(value.constructor, Uint8Array, 'first value should be a Uint8Array'); + assert_equals(value.buffer.byteLength, 16, 'first value.buffer.byteLength should be 16'); + assert_equals(value.byteOffset, 0, 'first value.byteOffset should be 0'); + assert_equals(value.byteLength, 1, 'first value.byteLength should be 1'); + assert_equals(value[0], 0x01, 'first value[0] should be 0x01'); + const byobRequest = byobRequests[1]; + assert_true(byobRequest.nonNull, 'second byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 16, 'second view.buffer.byteLength should be 16'); + assert_equals(viewInfo.byteOffset, 0, 'second view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 16, 'second view.byteLength should be 16'); + + return p1; + }).then(result => { + assert_equals(pullCount, 2, 'pull() should only be invoked twice'); + const value = result.value; + assert_not_equals(value, undefined, 'second read should have a value'); + assert_equals(value.constructor, Uint8Array, 'second value should be a Uint8Array'); + assert_equals(value.buffer.byteLength, 16, 'second value.buffer.byteLength should be 16'); + assert_equals(value.byteOffset, 0, 'second value.byteOffset should be 0'); + assert_equals(value.byteLength, 2, 'second value.byteLength should be 2'); + assert_equals(value[0], 0x02, 'second value[0] should be 0x02'); + assert_equals(value[1], 0x03, 'second value[1] should be 0x03'); + }); +}, 'ReadableStream with byte source: autoAllocateChunkSize'); + +promise_test(() => { + let pullCount = 0; + let controller; + const byobRequests = []; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + const byobRequest = controller.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + view[0] = 0x01; + byobRequest.respond(1); + } else if (pullCount === 1) { + view[0] = 0x02; + view[1] = 0x03; + byobRequest.respond(2); + } + + ++pullCount; + }, + type: 'bytes', + autoAllocateChunkSize: 16 + }, { + highWaterMark: 0 + }); + + const reader = stream.getReader(); + return reader.read().then(result => { + const value = result.value; + assert_not_equals(value, undefined, 'first read should have a value'); + assert_equals(value.constructor, Uint8Array, 'first value should be a Uint8Array'); + assert_equals(value.buffer.byteLength, 16, 'first value.buffer.byteLength should be 16'); + assert_equals(value.byteOffset, 0, 'first value.byteOffset should be 0'); + assert_equals(value.byteLength, 1, 'first value.byteLength should be 1'); + assert_equals(value[0], 0x01, 'first value[0] should be 0x01'); + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 16, 'first view.buffer.byteLength should be 16'); + assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 16, 'first view.byteLength should be 16'); + + reader.releaseLock(); + const byobReader = stream.getReader({ mode: 'byob' }); + return byobReader.read(new Uint8Array(32)); + }).then(result => { + const value = result.value; + assert_not_equals(value, undefined, 'second read should have a value'); + assert_equals(value.constructor, Uint8Array, 'second value should be a Uint8Array'); + assert_equals(value.buffer.byteLength, 32, 'second value.buffer.byteLength should be 32'); + assert_equals(value.byteOffset, 0, 'second value.byteOffset should be 0'); + assert_equals(value.byteLength, 2, 'second value.byteLength should be 2'); + assert_equals(value[0], 0x02, 'second value[0] should be 0x02'); + assert_equals(value[1], 0x03, 'second value[1] should be 0x03'); + const byobRequest = byobRequests[1]; + assert_true(byobRequest.nonNull, 'second byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 32, 'second view.buffer.byteLength should be 32'); + assert_equals(viewInfo.byteOffset, 0, 'second view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 32, 'second view.byteLength should be 32'); + assert_equals(pullCount, 2, 'pullCount should be 2'); + }); +}, 'ReadableStream with byte source: Mix of auto allocate and BYOB'); + +promise_test(() => { + let pullCount = 0; + + const stream = new ReadableStream({ + pull() { + ++pullCount; + }, + type: 'bytes' + }, { + highWaterMark: 0 + }); + + const reader = stream.getReader(); + reader.read(new Uint8Array(8)); + + assert_equals(pullCount, 0, 'No pull as start() just finished and is not yet reflected to the state of the stream'); + + return Promise.resolve().then(() => { + assert_equals(pullCount, 1, 'pull must be invoked'); + }); +}, 'ReadableStream with byte source: Automatic pull() after start() and read(view)'); + +promise_test(() => { + let pullCount = 0; + + let controller; + let desiredSizeInStart; + let desiredSizeInPull; + + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array(16)); + desiredSizeInStart = c.desiredSize; + controller = c; + }, + pull() { + ++pullCount; + + if (pullCount === 1) { + desiredSizeInPull = controller.desiredSize; + } + }, + type: 'bytes' + }, { + highWaterMark: 8 + }); + + return Promise.resolve().then(() => { + assert_equals(pullCount, 0, 'No pull as the queue was filled by start()'); + assert_equals(desiredSizeInStart, -8, 'desiredSize after enqueue() in start()'); + + const reader = stream.getReader(); + + const promise = reader.read(); + assert_equals(pullCount, 1, 'The first pull() should be made on read()'); + assert_equals(desiredSizeInPull, 8, 'desiredSize in pull()'); + + return promise.then(result => { + assert_false(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.constructor, Uint8Array, 'view.constructor'); + assert_equals(view.buffer.byteLength, 16, 'view.buffer'); + assert_equals(view.byteOffset, 0, 'view.byteOffset'); + assert_equals(view.byteLength, 16, 'view.byteLength'); + }); + }); +}, 'ReadableStream with byte source: enqueue(), getReader(), then read()'); + +promise_test(() => { + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + const promise = reader.read().then(result => { + assert_false(result.done); + + const view = result.value; + assert_equals(view.constructor, Uint8Array); + assert_equals(view.buffer.byteLength, 1); + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 1); + }); + + controller.enqueue(new Uint8Array(1)); + + return promise; +}, 'ReadableStream with byte source: Push source that doesn\'t understand pull signal'); + +test(() => { + assert_throws_js(TypeError, () => new ReadableStream({ + pull: 'foo', + type: 'bytes' + }), 'constructor should throw'); +}, 'ReadableStream with byte source: pull() function is not callable'); + +promise_test(() => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint16Array(16)); + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + return reader.read().then(result => { + assert_false(result.done); + + const view = result.value; + assert_equals(view.constructor, Uint8Array); + assert_equals(view.buffer.byteLength, 32); + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 32); + }); +}, 'ReadableStream with byte source: enqueue() with Uint16Array, getReader(), then read()'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(16); + view[0] = 0x01; + view[8] = 0x02; + c.enqueue(view); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const byobReader = stream.getReader({ mode: 'byob' }); + + return byobReader.read(new Uint8Array(8)).then(result => { + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.constructor, Uint8Array, 'value.constructor'); + assert_equals(view.buffer.byteLength, 8, 'value.buffer.byteLength'); + assert_equals(view.byteOffset, 0, 'value.byteOffset'); + assert_equals(view.byteLength, 8, 'value.byteLength'); + assert_equals(view[0], 0x01); + + byobReader.releaseLock(); + + const reader = stream.getReader(); + + return reader.read(); + }).then(result => { + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.constructor, Uint8Array, 'value.constructor'); + assert_equals(view.buffer.byteLength, 16, 'value.buffer.byteLength'); + assert_equals(view.byteOffset, 8, 'value.byteOffset'); + assert_equals(view.byteLength, 8, 'value.byteLength'); + assert_equals(view[0], 0x02); + }); +}, 'ReadableStream with byte source: enqueue(), read(view) partially, then read()'); + +promise_test(t => { + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader(); + + controller.enqueue(new Uint8Array(16)); + controller.close(); + + return reader.read().then(result => { + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 16, 'byteLength'); + + return reader.read(); + }).then(result => { + assert_true(result.done, 'done'); + assert_equals(result.value, undefined, 'value'); + }); +}, 'ReadableStream with byte source: getReader(), enqueue(), close(), then read()'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array(16)); + c.close(); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader(); + + return reader.read().then(result => { + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 16, 'byteLength'); + + return reader.read(); + }).then(result => { + assert_true(result.done, 'done'); + assert_equals(result.value, undefined, 'value'); + }); +}, 'ReadableStream with byte source: enqueue(), close(), getReader(), then read()'); + +promise_test(() => { + let controller; + let byobRequest; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + controller.enqueue(new Uint8Array(16)); + byobRequest = controller.byobRequest; + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + return reader.read().then(result => { + assert_false(result.done, 'done'); + assert_equals(result.value.byteLength, 16, 'byteLength'); + assert_equals(byobRequest, null, 'byobRequest must be null'); + }); +}, 'ReadableStream with byte source: Respond to pull() by enqueue()'); + +promise_test(() => { + let pullCount = 0; + + let controller; + let byobRequest; + const desiredSizes = []; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequest = controller.byobRequest; + desiredSizes.push(controller.desiredSize); + controller.enqueue(new Uint8Array(1)); + desiredSizes.push(controller.desiredSize); + controller.enqueue(new Uint8Array(1)); + desiredSizes.push(controller.desiredSize); + + ++pullCount; + }, + type: 'bytes' + }, { + highWaterMark: 0 + }); + + const reader = stream.getReader(); + + const p0 = reader.read(); + const p1 = reader.read(); + const p2 = reader.read(); + + // Respond to the first pull call. + controller.enqueue(new Uint8Array(1)); + + assert_equals(pullCount, 0, 'pullCount after the enqueue() outside pull'); + + return Promise.all([p0, p1, p2]).then(result => { + assert_equals(pullCount, 1, 'pullCount after completion of all read()s'); + + assert_equals(result[0].done, false, 'result[0].done'); + assert_equals(result[0].value.byteLength, 1, 'result[0].value.byteLength'); + assert_equals(result[1].done, false, 'result[1].done'); + assert_equals(result[1].value.byteLength, 1, 'result[1].value.byteLength'); + assert_equals(result[2].done, false, 'result[2].done'); + assert_equals(result[2].value.byteLength, 1, 'result[2].value.byteLength'); + assert_equals(byobRequest, null, 'byobRequest should be null'); + assert_equals(desiredSizes[0], 0, 'desiredSize on pull should be 0'); + assert_equals(desiredSizes[1], 0, 'desiredSize after 1st enqueue() should be 0'); + assert_equals(desiredSizes[2], 0, 'desiredSize after 2nd enqueue() should be 0'); + assert_equals(pullCount, 1, 'pull() should only be called once'); + }); +}, 'ReadableStream with byte source: Respond to pull() by enqueue() asynchronously'); + +promise_test(() => { + let pullCount = 0; + + let byobRequest; + const desiredSizes = []; + + const stream = new ReadableStream({ + pull(c) { + byobRequest = c.byobRequest; + desiredSizes.push(c.desiredSize); + + if (pullCount < 3) { + c.enqueue(new Uint8Array(1)); + } else { + c.close(); + } + + ++pullCount; + }, + type: 'bytes' + }, { + highWaterMark: 256 + }); + + const reader = stream.getReader(); + + const p0 = reader.read(); + const p1 = reader.read(); + const p2 = reader.read(); + + assert_equals(pullCount, 0, 'No pull as start() just finished and is not yet reflected to the state of the stream'); + + return Promise.all([p0, p1, p2]).then(result => { + assert_equals(pullCount, 4, 'pullCount after completion of all read()s'); + + assert_equals(result[0].done, false, 'result[0].done'); + assert_equals(result[0].value.byteLength, 1, 'result[0].value.byteLength'); + assert_equals(result[1].done, false, 'result[1].done'); + assert_equals(result[1].value.byteLength, 1, 'result[1].value.byteLength'); + assert_equals(result[2].done, false, 'result[2].done'); + assert_equals(result[2].value.byteLength, 1, 'result[2].value.byteLength'); + assert_equals(byobRequest, null, 'byobRequest should be null'); + assert_equals(desiredSizes[0], 256, 'desiredSize on pull should be 256'); + assert_equals(desiredSizes[1], 256, 'desiredSize after 1st enqueue() should be 256'); + assert_equals(desiredSizes[2], 256, 'desiredSize after 2nd enqueue() should be 256'); + assert_equals(desiredSizes[3], 256, 'desiredSize after 3rd enqueue() should be 256'); + }); +}, 'ReadableStream with byte source: Respond to multiple pull() by separate enqueue()'); + +promise_test(() => { + let controller; + + let pullCount = 0; + const byobRequestDefined = []; + let byobRequestViewDefined; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequestDefined.push(controller.byobRequest !== null); + const initialByobRequest = controller.byobRequest; + + const view = controller.byobRequest.view; + view[0] = 0x01; + controller.byobRequest.respond(1); + + byobRequestDefined.push(controller.byobRequest !== null); + byobRequestViewDefined = initialByobRequest.view !== null; + + ++pullCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(1)).then(result => { + assert_false(result.done, 'result.done'); + assert_equals(result.value.byteLength, 1, 'result.value.byteLength'); + assert_equals(result.value[0], 0x01, 'result.value[0]'); + assert_equals(pullCount, 1, 'pull() should be called only once'); + assert_true(byobRequestDefined[0], 'byobRequest must not be null before respond()'); + assert_false(byobRequestDefined[1], 'byobRequest must be null after respond()'); + assert_false(byobRequestViewDefined, 'view of initial byobRequest must be null after respond()'); + }); +}, 'ReadableStream with byte source: read(view), then respond()'); + +promise_test(() => { + let controller; + + let pullCount = 0; + const byobRequestDefined = []; + let byobRequestViewDefined; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequestDefined.push(controller.byobRequest !== null); + const initialByobRequest = controller.byobRequest; + + const transferredView = transferArrayBufferView(controller.byobRequest.view); + transferredView[0] = 0x01; + controller.byobRequest.respondWithNewView(transferredView); + + byobRequestDefined.push(controller.byobRequest !== null); + byobRequestViewDefined = initialByobRequest.view !== null; + + ++pullCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(1)).then(result => { + assert_false(result.done, 'result.done'); + assert_equals(result.value.byteLength, 1, 'result.value.byteLength'); + assert_equals(result.value[0], 0x01, 'result.value[0]'); + assert_equals(pullCount, 1, 'pull() should be called only once'); + assert_true(byobRequestDefined[0], 'byobRequest must not be null before respondWithNewView()'); + assert_false(byobRequestDefined[1], 'byobRequest must be null after respondWithNewView()'); + assert_false(byobRequestViewDefined, 'view of initial byobRequest must be null after respondWithNewView()'); + }); +}, 'ReadableStream with byte source: read(view), then respondWithNewView() with a transferred ArrayBuffer'); + +promise_test(() => { + let controller; + let byobRequestWasDefined; + let incorrectRespondException; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequestWasDefined = controller.byobRequest !== null; + + try { + controller.byobRequest.respond(2); + } catch (e) { + incorrectRespondException = e; + } + + controller.byobRequest.respond(1); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(1)).then(() => { + assert_true(byobRequestWasDefined, 'byobRequest should be non-null'); + assert_not_equals(incorrectRespondException, undefined, 'respond() must throw'); + assert_equals(incorrectRespondException.name, 'RangeError', 'respond() must throw a RangeError'); + }); +}, 'ReadableStream with byte source: read(view), then respond() with too big value'); + +promise_test(() => { + let pullCount = 0; + + let controller; + let byobRequest; + let viewInfo; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + ++pullCount; + + byobRequest = controller.byobRequest; + const view = byobRequest.view; + viewInfo = extractViewInfo(view); + + view[0] = 0x01; + view[1] = 0x02; + view[2] = 0x03; + + controller.byobRequest.respond(3); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint16Array(2)).then(result => { + assert_equals(pullCount, 1); + + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 2, 'byteLength'); + + const dataView = new DataView(view.buffer, view.byteOffset, view.byteLength); + assert_equals(dataView.getUint16(0), 0x0102); + + return reader.read(new Uint8Array(1)); + }).then(result => { + assert_equals(pullCount, 1); + assert_not_equals(byobRequest, null, 'byobRequest must not be null'); + assert_equals(viewInfo.constructor, Uint8Array, 'view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 4, 'view.buffer.byteLength should be 4'); + assert_equals(viewInfo.byteOffset, 0, 'view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 4, 'view.byteLength should be 4'); + + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 1, 'byteLength'); + + assert_equals(view[0], 0x03); + }); +}, 'ReadableStream with byte source: respond(3) to read(view) with 2 element Uint16Array enqueues the 1 byte ' + + 'remainder'); + +promise_test(() => { + let pullCount = 0; + + let controller; + let byobRequest; + let viewInfo; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + ++pullCount; + + byobRequest = controller.byobRequest; + const view = byobRequest.view; + viewInfo = extractViewInfo(view); + + view[0] = 0x01; + view[1] = 0x02; + view[2] = 0x03; + + controller.byobRequest.respond(3); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const read1 = reader.read(new Uint16Array(2)); + const read2 = reader.read(new Uint8Array(1)); + + return read1.then(result => { + assert_equals(pullCount, 1); + + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 2, 'byteLength'); + + const dataView = new DataView(view.buffer, view.byteOffset, view.byteLength); + assert_equals(dataView.getUint16(0), 0x0102); + + return read2; + }).then(result => { + assert_equals(pullCount, 1); + assert_not_equals(byobRequest, null, 'byobRequest must not be null'); + assert_equals(viewInfo.constructor, Uint8Array, 'view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 4, 'view.buffer.byteLength should be 4'); + assert_equals(viewInfo.byteOffset, 0, 'view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 4, 'view.byteLength should be 4'); + + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 1, 'byteLength'); + + assert_equals(view[0], 0x03); + }); +}, 'ReadableStream with byte source: respond(3) to read(view) with 2 element Uint16Array fulfills second read(view) ' + + 'with the 1 byte remainder'); + +promise_test(t => { + const stream = new ReadableStream({ + start(controller) { + const view = new Uint8Array(16); + view[15] = 0x01; + controller.enqueue(view); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(16)).then(result => { + assert_false(result.done); + + const view = result.value; + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 16); + assert_equals(view[15], 0x01); + }); +}, 'ReadableStream with byte source: enqueue(), getReader(), then read(view)'); + +promise_test(t => { + let cancelCount = 0; + let reason; + + const passedReason = new TypeError('foo'); + + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array(16)); + }, + pull: t.unreached_func('pull() should not be called'), + cancel(r) { + if (cancelCount === 0) { + reason = r; + } + + ++cancelCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + return reader.cancel(passedReason).then(result => { + assert_equals(result, undefined); + assert_equals(cancelCount, 1); + assert_equals(reason, passedReason, 'reason should equal the passed reason'); + }); +}, 'ReadableStream with byte source: enqueue(), getReader(), then cancel() (mode = not BYOB)'); + +promise_test(t => { + let cancelCount = 0; + let reason; + + const passedReason = new TypeError('foo'); + + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array(16)); + }, + pull: t.unreached_func('pull() should not be called'), + cancel(r) { + if (cancelCount === 0) { + reason = r; + } + + ++cancelCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.cancel(passedReason).then(result => { + assert_equals(result, undefined); + assert_equals(cancelCount, 1); + assert_equals(reason, passedReason, 'reason should equal the passed reason'); + }); +}, 'ReadableStream with byte source: enqueue(), getReader(), then cancel() (mode = BYOB)'); + +promise_test(t => { + let cancelCount = 0; + let reason; + + const passedReason = new TypeError('foo'); + + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + cancel(r) { + if (cancelCount === 0) { + reason = r; + } + + ++cancelCount; + + return 'bar'; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const readPromise = reader.read(new Uint8Array(1)).then(result => { + assert_true(result.done, 'result.done'); + assert_equals(result.value, undefined, 'result.value'); + }); + + const cancelPromise = reader.cancel(passedReason).then(result => { + assert_equals(result, undefined, 'cancel() return value should be fulfilled with undefined'); + assert_equals(cancelCount, 1, 'cancel() should be called only once'); + assert_equals(reason, passedReason, 'reason should equal the passed reason'); + }); + + return Promise.all([readPromise, cancelPromise]); +}, 'ReadableStream with byte source: getReader(), read(view), then cancel()'); + +promise_test(() => { + let pullCount = 0; + + let controller; + let byobRequest; + const viewInfos = []; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequest = controller.byobRequest; + + viewInfos.push(extractViewInfo(controller.byobRequest.view)); + controller.enqueue(new Uint8Array(1)); + viewInfos.push(extractViewInfo(controller.byobRequest.view)); + + ++pullCount; + }, + type: 'bytes' + }); + + return Promise.resolve().then(() => { + assert_equals(pullCount, 0, 'No pull() as no read(view) yet'); + + const reader = stream.getReader({ mode: 'byob' }); + + const promise = reader.read(new Uint16Array(1)).then(result => { + assert_true(result.done, 'result.done'); + assert_equals(result.value, undefined, 'result.value'); + }); + + assert_equals(pullCount, 1, '1 pull() should have been made in response to partial fill by enqueue()'); + assert_not_equals(byobRequest, null, 'byobRequest should not be null'); + assert_equals(viewInfos[0].byteLength, 2, 'byteLength before enqueue() should be 2'); + assert_equals(viewInfos[1].byteLength, 1, 'byteLength after enqueue() should be 1'); + + reader.cancel(); + + assert_equals(pullCount, 1, 'pull() should only be called once'); + return promise; + }); +}, 'ReadableStream with byte source: cancel() with partially filled pending pull() request'); + +promise_test(() => { + let controller; + let pullCalled = false; + + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(8); + view[7] = 0x01; + c.enqueue(view); + + controller = c; + }, + pull() { + pullCalled = true; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const buffer = new ArrayBuffer(16); + + return reader.read(new Uint8Array(buffer, 8, 8)).then(result => { + assert_false(result.done); + + assert_false(pullCalled, 'pull() must not have been called'); + + const view = result.value; + assert_equals(view.constructor, Uint8Array); + assert_equals(view.buffer.byteLength, 16); + assert_equals(view.byteOffset, 8); + assert_equals(view.byteLength, 8); + assert_equals(view[7], 0x01); + }); +}, 'ReadableStream with byte source: enqueue(), getReader(), then read(view) where view.buffer is not fully ' + + 'covered by view'); + +promise_test(() => { + let controller; + let pullCalled = false; + + const stream = new ReadableStream({ + start(c) { + let view; + + view = new Uint8Array(16); + view[15] = 123; + c.enqueue(view); + + view = new Uint8Array(8); + view[7] = 111; + c.enqueue(view); + + controller = c; + }, + pull() { + pullCalled = true; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(24)).then(result => { + assert_false(result.done, 'done'); + + assert_false(pullCalled, 'pull() must not have been called'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 24, 'byteLength'); + assert_equals(view[15], 123, 'Contents are set from the first chunk'); + assert_equals(view[23], 111, 'Contents are set from the second chunk'); + }); +}, 'ReadableStream with byte source: Multiple enqueue(), getReader(), then read(view)'); + +promise_test(() => { + let pullCalled = false; + + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(16); + view[15] = 0x01; + c.enqueue(view); + }, + pull() { + pullCalled = true; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(24)).then(result => { + assert_false(result.done); + + assert_false(pullCalled, 'pull() must not have been called'); + + const view = result.value; + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 16); + assert_equals(view[15], 0x01); + }); +}, 'ReadableStream with byte source: enqueue(), getReader(), then read(view) with a bigger view'); + +promise_test(() => { + let pullCalled = false; + + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(16); + view[7] = 0x01; + view[15] = 0x02; + c.enqueue(view); + }, + pull() { + pullCalled = true; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(8)).then(result => { + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 8); + assert_equals(view[7], 0x01); + + return reader.read(new Uint8Array(8)); + }).then(result => { + assert_false(result.done, 'done'); + + assert_false(pullCalled, 'pull() must not have been called'); + + const view = result.value; + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 8); + assert_equals(view[7], 0x02); + }); +}, 'ReadableStream with byte source: enqueue(), getReader(), then read(view) with smaller views'); + +promise_test(() => { + let controller; + let viewInfo; + + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(1); + view[0] = 0xff; + c.enqueue(view); + + controller = c; + }, + pull() { + if (controller.byobRequest === null) { + return; + } + + const view = controller.byobRequest.view; + viewInfo = extractViewInfo(view); + + view[0] = 0xaa; + controller.byobRequest.respond(1); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint16Array(1)).then(result => { + assert_false(result.done); + + const view = result.value; + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 2); + + const dataView = new DataView(view.buffer, view.byteOffset, view.byteLength); + assert_equals(dataView.getUint16(0), 0xffaa); + + assert_equals(viewInfo.constructor, Uint8Array, 'view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 2, 'view.buffer.byteLength should be 2'); + assert_equals(viewInfo.byteOffset, 1, 'view.byteOffset should be 1'); + assert_equals(viewInfo.byteLength, 1, 'view.byteLength should be 1'); + }); +}, 'ReadableStream with byte source: enqueue() 1 byte, getReader(), then read(view) with Uint16Array'); + +promise_test(() => { + let pullCount = 0; + + let controller; + let byobRequest; + let viewInfo; + let desiredSize; + + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(3); + view[0] = 0x01; + view[2] = 0x02; + c.enqueue(view); + + controller = c; + }, + pull() { + byobRequest = controller.byobRequest; + + const view = controller.byobRequest.view; + + viewInfo = extractViewInfo(view); + + view[0] = 0x03; + controller.byobRequest.respond(1); + + desiredSize = controller.desiredSize; + + ++pullCount; + }, + type: 'bytes' + }); + + // Wait for completion of the start method to be reflected. + return Promise.resolve().then(() => { + const reader = stream.getReader({ mode: 'byob' }); + + const promise = reader.read(new Uint16Array(2)).then(result => { + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.constructor, Uint16Array, 'constructor'); + assert_equals(view.buffer.byteLength, 4, 'buffer.byteLength'); + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 2, 'byteLength'); + + const dataView = new DataView(view.buffer, view.byteOffset, view.byteLength); + assert_equals(dataView.getUint16(0), 0x0100, 'contents are set'); + + const p = reader.read(new Uint16Array(1)); + + assert_equals(pullCount, 1); + + return p; + }).then(result => { + assert_false(result.done, 'done'); + + const view = result.value; + assert_equals(view.buffer.byteLength, 2, 'buffer.byteLength'); + assert_equals(view.byteOffset, 0, 'byteOffset'); + assert_equals(view.byteLength, 2, 'byteLength'); + + const dataView = new DataView(view.buffer, view.byteOffset, view.byteLength); + assert_equals(dataView.getUint16(0), 0x0203, 'contents are set'); + + assert_not_equals(byobRequest, null, 'byobRequest must not be null'); + assert_equals(viewInfo.constructor, Uint8Array, 'view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 2, 'view.buffer.byteLength should be 2'); + assert_equals(viewInfo.byteOffset, 1, 'view.byteOffset should be 1'); + assert_equals(viewInfo.byteLength, 1, 'view.byteLength should be 1'); + assert_equals(desiredSize, 0, 'desiredSize should be zero'); + }); + + assert_equals(pullCount, 0); + + return promise; + }); +}, 'ReadableStream with byte source: enqueue() 3 byte, getReader(), then read(view) with 2-element Uint16Array'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(1); + view[0] = 0xff; + c.enqueue(view); + c.close(); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + + return promise_rejects_js(t, TypeError, reader.read(new Uint16Array(1)), 'read(view) must fail') + .then(() => promise_rejects_js(t, TypeError, reader.closed, 'reader.closed should reject')); +}, 'ReadableStream with byte source: read(view) with Uint16Array on close()-d stream with 1 byte enqueue()-d must ' + + 'fail'); + +promise_test(t => { + let controller; + + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(1); + view[0] = 0xff; + c.enqueue(view); + + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const readPromise = reader.read(new Uint16Array(1)); + + assert_throws_js(TypeError, () => controller.close(), 'controller.close() must throw'); + + return promise_rejects_js(t, TypeError, readPromise, 'read(view) must fail') + .then(() => promise_rejects_js(t, TypeError, reader.closed, 'reader.closed must reject')); +}, 'ReadableStream with byte source: A stream must be errored if close()-d before fulfilling read(view) with ' + + 'Uint16Array'); + +test(() => { + let controller; + + new ReadableStream({ + start(c) { + controller = c; + }, + type: 'bytes' + }); + + // Enqueue a chunk so that the stream doesn't get closed. This is to check duplicate close() calls are rejected + // even if the stream has not yet entered the closed state. + const view = new Uint8Array(1); + controller.enqueue(view); + controller.close(); + + assert_throws_js(TypeError, () => controller.close(), 'controller.close() must throw'); +}, 'ReadableStream with byte source: Throw if close()-ed more than once'); + +test(() => { + let controller; + + new ReadableStream({ + start(c) { + controller = c; + }, + type: 'bytes' + }); + + // Enqueue a chunk so that the stream doesn't get closed. This is to check enqueue() after close() is rejected + // even if the stream has not yet entered the closed state. + const view = new Uint8Array(1); + controller.enqueue(view); + controller.close(); + + assert_throws_js(TypeError, () => controller.enqueue(view), 'controller.close() must throw'); +}, 'ReadableStream with byte source: Throw on enqueue() after close()'); + +promise_test(() => { + let controller; + let byobRequest; + let viewInfo; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequest = controller.byobRequest; + const view = controller.byobRequest.view; + viewInfo = extractViewInfo(view); + + view[15] = 0x01; + controller.byobRequest.respond(16); + controller.close(); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint8Array(16)).then(result => { + assert_false(result.done); + + const view = result.value; + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 16); + assert_equals(view[15], 0x01); + + return reader.read(new Uint8Array(16)); + }).then(result => { + assert_true(result.done); + + const view = result.value; + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 0); + + assert_not_equals(byobRequest, null, 'byobRequest must not be null'); + assert_equals(viewInfo.constructor, Uint8Array, 'view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 16, 'view.buffer.byteLength should be 16'); + assert_equals(viewInfo.byteOffset, 0, 'view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 16, 'view.byteLength should be 16'); + }); +}, 'ReadableStream with byte source: read(view), then respond() and close() in pull()'); + +promise_test(() => { + let pullCount = 0; + + let controller; + const viewInfos = []; + const viewInfosAfterRespond = []; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + if (controller.byobRequest === null) { + return; + } + + for (let i = 0; i < 4; ++i) { + const view = controller.byobRequest.view; + viewInfos.push(extractViewInfo(view)); + + view[0] = 0x01; + controller.byobRequest.respond(1); + viewInfosAfterRespond.push(extractViewInfo(view)); + } + + ++pullCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint32Array(1)).then(result => { + assert_false(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(view.byteLength, 4, 'result.value.byteLength'); + assert_equals(view[0], 0x01010101, 'result.value[0]'); + + assert_equals(pullCount, 1, 'pull() should only be called once'); + + for (let i = 0; i < 4; ++i) { + assert_equals(viewInfos[i].constructor, Uint8Array, 'view.constructor should be Uint8Array'); + assert_equals(viewInfos[i].bufferByteLength, 4, 'view.buffer.byteLength should be 4'); + + assert_equals(viewInfos[i].byteOffset, i, 'view.byteOffset should be i'); + assert_equals(viewInfos[i].byteLength, 4 - i, 'view.byteLength should be 4 - i'); + + assert_equals(viewInfosAfterRespond[i].bufferByteLength, 0, 'view.buffer should be transferred after respond()'); + } + }); +}, 'ReadableStream with byte source: read(view) with Uint32Array, then fill it by multiple respond() calls'); + +promise_test(() => { + let pullCount = 0; + + let controller; + const viewInfos = []; + const viewInfosAfterEnqueue = []; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + if (controller.byobRequest === null) { + return; + } + + for (let i = 0; i < 4; ++i) { + const view = controller.byobRequest.view; + viewInfos.push(extractViewInfo(view)); + + controller.enqueue(new Uint8Array([0x01])); + viewInfosAfterEnqueue.push(extractViewInfo(view)); + } + + ++pullCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return reader.read(new Uint32Array(1)).then(result => { + assert_false(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(view.byteLength, 4, 'result.value.byteLength'); + assert_equals(view[0], 0x01010101, 'result.value[0]'); + + assert_equals(pullCount, 1, 'pull() should only be called once'); + + for (let i = 0; i < 4; ++i) { + assert_equals(viewInfos[i].constructor, Uint8Array, 'view.constructor should be Uint8Array'); + assert_equals(viewInfos[i].bufferByteLength, 4, 'view.buffer.byteLength should be 4'); + + assert_equals(viewInfos[i].byteOffset, i, 'view.byteOffset should be i'); + assert_equals(viewInfos[i].byteLength, 4 - i, 'view.byteLength should be 4 - i'); + + assert_equals(viewInfosAfterEnqueue[i].bufferByteLength, 0, 'view.buffer should be transferred after enqueue()'); + } + }); +}, 'ReadableStream with byte source: read(view) with Uint32Array, then fill it by multiple enqueue() calls'); + +promise_test(() => { + let pullCount = 0; + + let controller; + let byobRequest; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequest = controller.byobRequest; + + ++pullCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + const p0 = reader.read().then(result => { + assert_equals(pullCount, 1); + + controller.enqueue(new Uint8Array(2)); + + // Since the queue has data no less than HWM, no more pull. + assert_equals(pullCount, 1); + + assert_false(result.done); + + const view = result.value; + assert_equals(view.constructor, Uint8Array); + assert_equals(view.buffer.byteLength, 1); + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 1); + }); + + assert_equals(pullCount, 0, 'No pull should have been made since the startPromise has not yet been handled'); + + const p1 = reader.read().then(result => { + assert_equals(pullCount, 1); + + assert_false(result.done); + + const view = result.value; + assert_equals(view.constructor, Uint8Array); + assert_equals(view.buffer.byteLength, 2); + assert_equals(view.byteOffset, 0); + assert_equals(view.byteLength, 2); + + assert_equals(byobRequest, null, 'byobRequest must be null'); + }); + + assert_equals(pullCount, 0, 'No pull should have been made since the startPromise has not yet been handled'); + + controller.enqueue(new Uint8Array(1)); + + assert_equals(pullCount, 0, 'No pull should have been made since the startPromise has not yet been handled'); + + return Promise.all([p0, p1]); +}, 'ReadableStream with byte source: read() twice, then enqueue() twice'); + +promise_test(t => { + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const p0 = reader.read(new Uint8Array(16)).then(result => { + assert_true(result.done, '1st read: done'); + + const view = result.value; + assert_equals(view.buffer.byteLength, 16, '1st read: buffer.byteLength'); + assert_equals(view.byteOffset, 0, '1st read: byteOffset'); + assert_equals(view.byteLength, 0, '1st read: byteLength'); + }); + + const p1 = reader.read(new Uint8Array(32)).then(result => { + assert_true(result.done, '2nd read: done'); + + const view = result.value; + assert_equals(view.buffer.byteLength, 32, '2nd read: buffer.byteLength'); + assert_equals(view.byteOffset, 0, '2nd read: byteOffset'); + assert_equals(view.byteLength, 0, '2nd read: byteLength'); + }); + + controller.close(); + controller.byobRequest.respond(0); + + return Promise.all([p0, p1]); +}, 'ReadableStream with byte source: Multiple read(view), close() and respond()'); + +promise_test(t => { + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const p0 = reader.read(new Uint8Array(16)).then(result => { + assert_false(result.done, '1st read: done'); + + const view = result.value; + assert_equals(view.buffer.byteLength, 16, '1st read: buffer.byteLength'); + assert_equals(view.byteOffset, 0, '1st read: byteOffset'); + assert_equals(view.byteLength, 16, '1st read: byteLength'); + }); + + const p1 = reader.read(new Uint8Array(16)).then(result => { + assert_false(result.done, '2nd read: done'); + + const view = result.value; + assert_equals(view.buffer.byteLength, 16, '2nd read: buffer.byteLength'); + assert_equals(view.byteOffset, 0, '2nd read: byteOffset'); + assert_equals(view.byteLength, 8, '2nd read: byteLength'); + }); + + controller.enqueue(new Uint8Array(24)); + + return Promise.all([p0, p1]); +}, 'ReadableStream with byte source: Multiple read(view), big enqueue()'); + +promise_test(t => { + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + let bytesRead = 0; + + function pump() { + return reader.read(new Uint8Array(7)).then(result => { + if (result.done) { + assert_equals(bytesRead, 1024); + return undefined; + } + + bytesRead += result.value.byteLength; + + return pump(); + }); + } + const promise = pump(); + + controller.enqueue(new Uint8Array(512)); + controller.enqueue(new Uint8Array(512)); + controller.close(); + + return promise; +}, 'ReadableStream with byte source: Multiple read(view) and multiple enqueue()'); + +promise_test(t => { + let pullCalled = false; + const stream = new ReadableStream({ + pull(controller) { + pullCalled = true; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return promise_rejects_js(t, TypeError, reader.read(), 'read() must fail') + .then(() => assert_false(pullCalled, 'pull() must not have been called')); +}, 'ReadableStream with byte source: read(view) with passing undefined as view must fail'); + +promise_test(t => { + const stream = new ReadableStream({ + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return promise_rejects_js(t, TypeError, reader.read({}), 'read(view) must fail'); +}, 'ReadableStream with byte source: read(view) with passing an empty object as view must fail'); + +promise_test(t => { + const stream = new ReadableStream({ + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return promise_rejects_js(t, TypeError, + reader.read({ buffer: new ArrayBuffer(10), byteOffset: 0, byteLength: 10 }), + 'read(view) must fail'); +}, 'ReadableStream with byte source: Even read(view) with passing ArrayBufferView like object as view must fail'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.error(error1); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader(); + + return promise_rejects_exactly(t, error1, reader.read(), 'read() must fail'); +}, 'ReadableStream with byte source: read() on an errored stream'); + +promise_test(t => { + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + const promise = promise_rejects_exactly(t, error1, reader.read(), 'read() must fail'); + + controller.error(error1); + + return promise; +}, 'ReadableStream with byte source: read(), then error()'); + +promise_test(t => { + const stream = new ReadableStream({ + start(c) { + c.error(error1); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return promise_rejects_exactly(t, error1, reader.read(new Uint8Array(1)), 'read() must fail'); +}, 'ReadableStream with byte source: read(view) on an errored stream'); + +promise_test(t => { + let controller; + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const promise = promise_rejects_exactly(t, error1, reader.read(new Uint8Array(1)), 'read() must fail'); + + controller.error(error1); + + return promise; +}, 'ReadableStream with byte source: read(view), then error()'); + +promise_test(t => { + let controller; + let byobRequest; + + const testError = new TypeError('foo'); + + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + byobRequest = controller.byobRequest; + throw testError; + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + const promise = promise_rejects_exactly(t, testError, reader.read(), 'read() must fail'); + return promise_rejects_exactly(t, testError, promise.then(() => reader.closed)) + .then(() => assert_equals(byobRequest, null, 'byobRequest must be null')); +}, 'ReadableStream with byte source: Throwing in pull function must error the stream'); + +promise_test(t => { + let byobRequest; + + const stream = new ReadableStream({ + pull(controller) { + byobRequest = controller.byobRequest; + controller.error(error1); + throw new TypeError('foo'); + }, + type: 'bytes' + }); + + const reader = stream.getReader(); + + return promise_rejects_exactly(t, error1, reader.read(), 'read() must fail') + .then(() => promise_rejects_exactly(t, error1, reader.closed, 'closed must fail')) + .then(() => assert_equals(byobRequest, null, 'byobRequest must be null')); +}, 'ReadableStream with byte source: Throwing in pull in response to read() must be ignored if the stream is ' + + 'errored in it'); + +promise_test(t => { + let byobRequest; + + const testError = new TypeError('foo'); + + const stream = new ReadableStream({ + pull(controller) { + byobRequest = controller.byobRequest; + throw testError; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return promise_rejects_exactly(t, testError, reader.read(new Uint8Array(1)), 'read(view) must fail') + .then(() => promise_rejects_exactly(t, testError, reader.closed, 'reader.closed must reject')) + .then(() => assert_not_equals(byobRequest, null, 'byobRequest must not be null')); +}, 'ReadableStream with byte source: Throwing in pull in response to read(view) function must error the stream'); + +promise_test(t => { + let byobRequest; + + const stream = new ReadableStream({ + pull(controller) { + byobRequest = controller.byobRequest; + controller.error(error1); + throw new TypeError('foo'); + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + return promise_rejects_exactly(t, error1, reader.read(new Uint8Array(1)), 'read(view) must fail') + .then(() => promise_rejects_exactly(t, error1, reader.closed, 'closed must fail')) + .then(() => assert_not_equals(byobRequest, null, 'byobRequest must not be null')); +}, 'ReadableStream with byte source: Throwing in pull in response to read(view) must be ignored if the stream is ' + + 'errored in it'); + +promise_test(() => { + let byobRequest; + const rs = new ReadableStream({ + pull(controller) { + byobRequest = controller.byobRequest; + byobRequest.respond(4); + }, + type: 'bytes' + }); + const reader = rs.getReader({ mode: 'byob' }); + const view = new Uint8Array(16); + return reader.read(view).then(() => { + assert_throws_js(TypeError, () => byobRequest.respond(4), 'respond() should throw a TypeError'); + }); +}, 'calling respond() twice on the same byobRequest should throw'); + +promise_test(() => { + let byobRequest; + const newView = () => new Uint8Array(16); + const rs = new ReadableStream({ + pull(controller) { + byobRequest = controller.byobRequest; + byobRequest.respondWithNewView(newView()); + }, + type: 'bytes' + }); + const reader = rs.getReader({ mode: 'byob' }); + return reader.read(newView()).then(() => { + assert_throws_js(TypeError, () => byobRequest.respondWithNewView(newView()), + 'respondWithNewView() should throw a TypeError'); + }); +}, 'calling respondWithNewView() twice on the same byobRequest should throw'); + +promise_test(() => { + let controller; + let byobRequest; + let resolvePullCalledPromise; + const pullCalledPromise = new Promise(resolve => { + resolvePullCalledPromise = resolve; + }); + let resolvePull; + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + pull(c) { + byobRequest = c.byobRequest; + resolvePullCalledPromise(); + return new Promise(resolve => { + resolvePull = resolve; + }); + }, + type: 'bytes' + }); + const reader = rs.getReader({ mode: 'byob' }); + const readPromise = reader.read(new Uint8Array(16)); + return pullCalledPromise.then(() => { + controller.close(); + byobRequest.respond(0); + resolvePull(); + return readPromise.then(() => { + assert_throws_js(TypeError, () => byobRequest.respond(0), 'respond() should throw'); + }); + }); +}, 'calling respond(0) twice on the same byobRequest should throw even when closed'); + +promise_test(() => { + let controller; + let byobRequest; + let resolvePullCalledPromise; + const pullCalledPromise = new Promise(resolve => { + resolvePullCalledPromise = resolve; + }); + let resolvePull; + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + pull(c) { + byobRequest = c.byobRequest; + resolvePullCalledPromise(); + return new Promise(resolve => { + resolvePull = resolve; + }); + }, + type: 'bytes' + }); + const reader = rs.getReader({ mode: 'byob' }); + const readPromise = reader.read(new Uint8Array(16)); + return pullCalledPromise.then(() => { + const cancelPromise = reader.cancel('meh'); + assert_throws_js(TypeError, () => byobRequest.respond(0), 'respond() should throw'); + resolvePull(); + return Promise.all([readPromise, cancelPromise]); + }); +}, 'calling respond() should throw when canceled'); + +promise_test(async t => { + let resolvePullCalledPromise; + const pullCalledPromise = new Promise(resolve => { + resolvePullCalledPromise = resolve; + }); + let resolvePull; + const rs = new ReadableStream({ + pull() { + resolvePullCalledPromise(); + return new Promise(resolve => { + resolvePull = resolve; + }); + }, + type: 'bytes' + }); + const reader = rs.getReader({ mode: 'byob' }); + const read = reader.read(new Uint8Array(16)); + await pullCalledPromise; + resolvePull(); + await delay(0); + reader.releaseLock(); + await promise_rejects_js(t, TypeError, read, 'pending read should reject'); +}, 'pull() resolving should not resolve read()'); + +promise_test(() => { + // Tests https://github.com/whatwg/streams/issues/686 + + let controller; + const rs = new ReadableStream({ + autoAllocateChunkSize: 128, + start(c) { + controller = c; + }, + type: 'bytes' + }); + + const readPromise = rs.getReader().read(); + + const br = controller.byobRequest; + controller.close(); + + br.respond(0); + + return readPromise; +}, 'ReadableStream with byte source: default reader + autoAllocateChunkSize + byobRequest interaction'); + +test(() => { + assert_throws_js(TypeError, () => new ReadableStream({ autoAllocateChunkSize: 0, type: 'bytes' }), + 'controller cannot be setup with autoAllocateChunkSize = 0'); +}, 'ReadableStream with byte source: autoAllocateChunkSize cannot be 0'); + +test(() => { + const ReadableStreamBYOBReader = new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' }).constructor; + const stream = new ReadableStream({ type: 'bytes' }); + new ReadableStreamBYOBReader(stream); +}, 'ReadableStreamBYOBReader can be constructed directly'); + +test(() => { + const ReadableStreamBYOBReader = new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' }).constructor; + assert_throws_js(TypeError, () => new ReadableStreamBYOBReader({}), 'constructor must throw'); +}, 'ReadableStreamBYOBReader constructor requires a ReadableStream argument'); + +test(() => { + const ReadableStreamBYOBReader = new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' }).constructor; + const stream = new ReadableStream({ type: 'bytes' }); + stream.getReader(); + assert_throws_js(TypeError, () => new ReadableStreamBYOBReader(stream), 'constructor must throw'); +}, 'ReadableStreamBYOBReader constructor requires an unlocked ReadableStream'); + +test(() => { + const ReadableStreamBYOBReader = new ReadableStream({ type: 'bytes' }).getReader({ mode: 'byob' }).constructor; + const stream = new ReadableStream(); + assert_throws_js(TypeError, () => new ReadableStreamBYOBReader(stream), 'constructor must throw'); +}, 'ReadableStreamBYOBReader constructor requires a ReadableStream with type "bytes"'); + +test(() => { + assert_throws_js(RangeError, () => new ReadableStream({ type: 'bytes' }, { + size() { + return 1; + } + }), 'constructor should throw for size function'); + + assert_throws_js(RangeError, + () => new ReadableStream({ type: 'bytes' }, new CountQueuingStrategy({ highWaterMark: 1 })), + 'constructor should throw when strategy is CountQueuingStrategy'); + + assert_throws_js(RangeError, + () => new ReadableStream({ type: 'bytes' }, new ByteLengthQueuingStrategy({ highWaterMark: 512 })), + 'constructor should throw when strategy is ByteLengthQueuingStrategy'); + + class HasSizeMethod { + size() {} + } + + assert_throws_js(RangeError, () => new ReadableStream({ type: 'bytes' }, new HasSizeMethod()), + 'constructor should throw when size on the prototype chain'); +}, 'ReadableStream constructor should not accept a strategy with a size defined if type is "bytes"'); + +promise_test(async t => { + const stream = new ReadableStream({ + pull: t.step_func(c => { + const view = new Uint8Array(c.byobRequest.view.buffer, 0, 1); + view[0] = 1; + + c.byobRequest.respondWithNewView(view); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array([4, 5, 6])); + assert_false(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(view.byteLength, 1, 'result.value.byteLength'); + assert_equals(view[0], 1, 'result.value[0]'); + assert_equals(view.buffer.byteLength, 3, 'result.value.buffer.byteLength'); + assert_array_equals([...new Uint8Array(view.buffer)], [1, 5, 6], 'result.value.buffer'); +}, 'ReadableStream with byte source: respondWithNewView() with a smaller view'); + +promise_test(async t => { + const stream = new ReadableStream({ + pull: t.step_func(c => { + const view = new Uint8Array(c.byobRequest.view.buffer, 0, 0); + + c.close(); + + c.byobRequest.respondWithNewView(view); + }), + type: 'bytes' + }); + const reader = stream.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array([4, 5, 6])); + assert_true(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(view.byteLength, 0, 'result.value.byteLength'); + assert_equals(view.buffer.byteLength, 3, 'result.value.buffer.byteLength'); + assert_array_equals([...new Uint8Array(view.buffer)], [4, 5, 6], 'result.value.buffer'); +}, 'ReadableStream with byte source: respondWithNewView() with a zero-length view (in the closed state)'); + +promise_test(async t => { + let controller; + let resolvePullCalledPromise; + const pullCalledPromise = new Promise(resolve => { + resolvePullCalledPromise = resolve; + }); + const stream = new ReadableStream({ + start: t.step_func((c) => { + controller = c; + }), + pull: t.step_func(() => { + resolvePullCalledPromise(); + }), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const readPromise = reader.read(new Uint8Array([4, 5, 6])); + await pullCalledPromise; + + // Transfer the original BYOB request's buffer, and respond with a new view on that buffer + const transferredView = transferArrayBufferView(controller.byobRequest.view); + const newView = transferredView.subarray(0, 1); + newView[0] = 42; + + controller.byobRequest.respondWithNewView(newView); + + const result = await readPromise; + assert_false(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(view.byteLength, 1, 'result.value.byteLength'); + assert_equals(view[0], 42, 'result.value[0]'); + assert_equals(view.buffer.byteLength, 3, 'result.value.buffer.byteLength'); + assert_array_equals([...new Uint8Array(view.buffer)], [42, 5, 6], 'result.value.buffer'); + +}, 'ReadableStream with byte source: respondWithNewView() with a transferred non-zero-length view ' + + '(in the readable state)'); + +promise_test(async t => { + let controller; + let resolvePullCalledPromise; + const pullCalledPromise = new Promise(resolve => { + resolvePullCalledPromise = resolve; + }); + const stream = new ReadableStream({ + start: t.step_func((c) => { + controller = c; + }), + pull: t.step_func(() => { + resolvePullCalledPromise(); + }), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const readPromise = reader.read(new Uint8Array([4, 5, 6])); + await pullCalledPromise; + + // Transfer the original BYOB request's buffer, and respond with an empty view on that buffer + const transferredView = transferArrayBufferView(controller.byobRequest.view); + const newView = transferredView.subarray(0, 0); + + controller.close(); + controller.byobRequest.respondWithNewView(newView); + + const result = await readPromise; + assert_true(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(view.byteLength, 0, 'result.value.byteLength'); + assert_equals(view.buffer.byteLength, 3, 'result.value.buffer.byteLength'); + assert_array_equals([...new Uint8Array(view.buffer)], [4, 5, 6], 'result.value.buffer'); + +}, 'ReadableStream with byte source: respondWithNewView() with a transferred zero-length view ' + + '(in the closed state)'); + +promise_test(async t => { + let controller; + let pullCount = 0; + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 10, + start: t.step_func((c) => { + controller = c; + }), + pull: t.step_func(() => { + ++pullCount; + }) + }); + + await flushAsyncEvents(); + assert_equals(pullCount, 0, 'pull() must not have been invoked yet'); + + const reader1 = rs.getReader(); + const read1 = reader1.read(); + assert_equals(pullCount, 1, 'pull() must have been invoked once'); + const byobRequest1 = controller.byobRequest; + assert_equals(byobRequest1.view.byteLength, 10, 'first byobRequest.view.byteLength'); + + // enqueue() must discard the auto-allocated BYOB request + controller.enqueue(new Uint8Array([1, 2, 3])); + assert_equals(byobRequest1.view, null, 'first byobRequest must be invalidated after enqueue()'); + + const result1 = await read1; + assert_false(result1.done, 'first result.done'); + const view1 = result1.value; + assert_equals(view1.byteOffset, 0, 'first result.value.byteOffset'); + assert_equals(view1.byteLength, 3, 'first result.value.byteLength'); + assert_array_equals([...new Uint8Array(view1.buffer)], [1, 2, 3], 'first result.value.buffer'); + + reader1.releaseLock(); + + // read(view) should work after discarding the auto-allocated BYOB request + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5, 6])); + assert_equals(pullCount, 2, 'pull() must have been invoked twice'); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2.view.byteOffset, 0, 'second byobRequest.view.byteOffset'); + assert_equals(byobRequest2.view.byteLength, 3, 'second byobRequest.view.byteLength'); + assert_array_equals([...new Uint8Array(byobRequest2.view.buffer)], [4, 5, 6], 'second byobRequest.view.buffer'); + + byobRequest2.respond(3); + assert_equals(byobRequest2.view, null, 'second byobRequest must be invalidated after respond()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + const view2 = result2.value; + assert_equals(view2.byteOffset, 0, 'second result.value.byteOffset'); + assert_equals(view2.byteLength, 3, 'second result.value.byteLength'); + assert_array_equals([...new Uint8Array(view2.buffer)], [4, 5, 6], 'second result.value.buffer'); + + reader2.releaseLock(); + assert_equals(pullCount, 2, 'pull() must only have been invoked twice'); +}, 'ReadableStream with byte source: enqueue() discards auto-allocated BYOB request'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint8Array([1, 2, 3])); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([1, 2, 3]), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5, 6])); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + assert_equals(controller.byobRequest, byobRequest1, 'byobRequest should be unchanged'); + assert_array_equals([...new Uint8Array(byobRequest1.view.buffer)], [1, 2, 3], 'byobRequest.view.buffer should be unchanged'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // respond() should fulfill the *second* read() request + byobRequest1.view[0] = 11; + byobRequest1.respond(1); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after respond()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([11, 5, 6]).subarray(0, 1), 'second result.value'); + +}, 'ReadableStream with byte source: releaseLock() with pending read(view), read(view) on second reader, respond()'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint8Array([1, 2, 3])); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([1, 2, 3]), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint16Array(1)); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + assert_equals(controller.byobRequest, byobRequest1, 'byobRequest should be unchanged'); + assert_array_equals([...new Uint8Array(byobRequest1.view.buffer)], [1, 2, 3], 'byobRequest.view.buffer should be unchanged'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // respond(1) should partially fill the second read(), but not yet fulfill it + byobRequest1.view[0] = 0x11; + byobRequest1.respond(1); + + // second BYOB request should use remaining buffer from the second read() + const byobRequest2 = controller.byobRequest; + assert_not_equals(byobRequest2, null, 'second byobRequest should exist'); + assert_typed_array_equals(byobRequest2.view, new Uint8Array([0x11, 0]).subarray(1, 2), 'second byobRequest.view'); + + // second respond(1) should fill the read request and fulfill it + byobRequest2.view[0] = 0x22; + byobRequest2.respond(1); + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + const view2 = result2.value; + assert_equals(view2.byteOffset, 0, 'second result.value.byteOffset'); + assert_equals(view2.byteLength, 2, 'second result.value.byteLength'); + const dataView2 = new DataView(view2.buffer, view2.byteOffset, view2.byteLength); + assert_equals(dataView2.getUint16(0), 0x1122, 'second result.value[0]'); + +}, 'ReadableStream with byte source: releaseLock() with pending read(view), read(view) on second reader with ' + + '1 element Uint16Array, respond(1)'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint8Array([1, 2, 3])); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([1, 2, 3]), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5])); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + assert_equals(controller.byobRequest, byobRequest1, 'byobRequest should be unchanged'); + assert_array_equals([...new Uint8Array(byobRequest1.view.buffer)], [1, 2, 3], 'byobRequest.view.buffer should be unchanged'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // respond(3) should fulfill the second read(), and put 1 remaining byte in the queue + byobRequest1.view[0] = 6; + byobRequest1.view[1] = 7; + byobRequest1.view[2] = 8; + byobRequest1.respond(3); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after respond()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([6, 7]), 'second result.value'); + + // third read() should fulfill with the remaining byte + const result3 = await reader2.read(new Uint8Array([0, 0, 0])); + assert_false(result3.done, 'third result.done'); + assert_typed_array_equals(result3.value, new Uint8Array([8, 0, 0]).subarray(0, 1), 'third result.value'); + +}, 'ReadableStream with byte source: releaseLock() with pending read(view), read(view) on second reader with ' + + '2 element Uint8Array, respond(3)'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint8Array([1, 2, 3])); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([1, 2, 3]), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5, 6])); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // respondWithNewView() should fulfill the *second* read() request + byobRequest1.view[0] = 11; + byobRequest1.view[1] = 12; + byobRequest1.respondWithNewView(byobRequest1.view.subarray(0, 2)); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after respondWithNewView()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([11, 12, 6]).subarray(0, 2), 'second result.value'); + +}, 'ReadableStream with byte source: releaseLock() with pending read(view), read(view) on second reader, respondWithNewView()'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint8Array([1, 2, 3])); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([1, 2, 3]), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5, 6])); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // enqueue() should fulfill the *second* read() request + controller.enqueue(new Uint8Array([11, 12])); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after enqueue()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([11, 12, 6]).subarray(0, 2), 'second result.value'); + +}, 'ReadableStream with byte source: releaseLock() with pending read(view), read(view) on second reader, enqueue()'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint8Array([1, 2, 3])); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([1, 2, 3]), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5, 6])); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // close() followed by respond(0) should fulfill the second read() + controller.close(); + byobRequest1.respond(0); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after respond()'); + + const result2 = await read2; + assert_true(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([4, 5, 6]).subarray(0, 0), 'second result.value'); +}, 'ReadableStream with byte source: releaseLock() with pending read(view), read(view) on second reader, ' + + 'close(), respond(0)'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 4, + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader(); + const read1 = reader1.read(); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array(4), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader(); + const read2 = reader2.read(); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // respond() should fulfill the *second* read() request + byobRequest1.view[0] = 11; + byobRequest1.respond(1); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after respond()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([11, 0, 0, 0]).subarray(0, 1), 'second result.value'); + +}, 'ReadableStream with byte source: autoAllocateChunkSize, releaseLock() with pending read(), read() on second reader, respond()'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 4, + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader(); + const read1 = reader1.read(); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array(4), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader(); + const read2 = reader2.read(); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // enqueue() should fulfill the *second* read() request + controller.enqueue(new Uint8Array([11])); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after enqueue()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([11]), 'second result.value'); + +}, 'ReadableStream with byte source: autoAllocateChunkSize, releaseLock() with pending read(), read() on second reader, enqueue()'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 4, + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader(); + const read1 = reader1.read(); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array(4), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5, 6])); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // respond() should fulfill the *second* read() request + byobRequest1.view[0] = 11; + byobRequest1.respond(1); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after respond()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([11, 5, 6]).subarray(0, 1), 'second result.value'); + +}, 'ReadableStream with byte source: autoAllocateChunkSize, releaseLock() with pending read(), read(view) on second reader, respond()'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 4, + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader(); + const read1 = reader1.read(); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array(4), 'first byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint8Array([4, 5, 6])); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // enqueue() should fulfill the *second* read() request + controller.enqueue(new Uint8Array([11])); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2, null, 'byobRequest should be null after enqueue()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([11, 5, 6]).subarray(0, 1), 'second result.value'); + +}, 'ReadableStream with byte source: autoAllocateChunkSize, releaseLock() with pending read(), read(view) on second reader, enqueue()'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint16Array(1)); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([0, 0]), 'first byobRequest.view'); + + // respond(1) should partially fill the first read(), but not yet fulfill it + byobRequest1.view[0] = 0x11; + byobRequest1.respond(1); + const byobRequest2 = controller.byobRequest; + assert_not_equals(byobRequest2, null, 'second byobRequest should exist'); + assert_typed_array_equals(byobRequest2.view, new Uint8Array([0x11, 0]).subarray(1, 2), 'second byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader({ mode: 'byob' }); + const read2 = reader2.read(new Uint16Array(1)); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + assert_equals(controller.byobRequest, byobRequest2, 'byobRequest should be unchanged'); + assert_typed_array_equals(byobRequest2.view, new Uint8Array([0x11, 0]).subarray(1, 2), 'byobRequest.view should be unchanged'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // second respond(1) should fill the read request and fulfill it + byobRequest2.view[0] = 0x22; + byobRequest2.respond(1); + assert_equals(controller.byobRequest, null, 'byobRequest should be invalidated after second respond()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + const view2 = result2.value; + assert_equals(view2.byteOffset, 0, 'second result.value.byteOffset'); + assert_equals(view2.byteLength, 2, 'second result.value.byteLength'); + const dataView2 = new DataView(view2.buffer, view2.byteOffset, view2.byteLength); + assert_equals(dataView2.getUint16(0), 0x1122, 'second result.value[0]'); + +}, 'ReadableStream with byte source: read(view) with 1 element Uint16Array, respond(1), releaseLock(), read(view) on ' + + 'second reader with 1 element Uint16Array, respond(1)'); + +promise_test(async t => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }) + }); + await flushAsyncEvents(); + + const reader1 = rs.getReader({ mode: 'byob' }); + const read1 = reader1.read(new Uint16Array(1)); + const byobRequest1 = controller.byobRequest; + assert_not_equals(byobRequest1, null, 'first byobRequest should exist'); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([0, 0]), 'first byobRequest.view'); + + // respond(1) should partially fill the first read(), but not yet fulfill it + byobRequest1.view[0] = 0x11; + byobRequest1.respond(1); + const byobRequest2 = controller.byobRequest; + assert_not_equals(byobRequest2, null, 'second byobRequest should exist'); + assert_typed_array_equals(byobRequest2.view, new Uint8Array([0x11, 0]).subarray(1, 2), 'second byobRequest.view'); + + // releaseLock() should reject the pending read, but *not* invalidate the BYOB request + reader1.releaseLock(); + const reader2 = rs.getReader(); + const read2 = reader2.read(); + assert_not_equals(controller.byobRequest, null, 'byobRequest should not be invalidated after releaseLock()'); + assert_equals(controller.byobRequest, byobRequest2, 'byobRequest should be unchanged'); + assert_typed_array_equals(byobRequest2.view, new Uint8Array([0x11, 0]).subarray(1, 2), 'byobRequest.view should be unchanged'); + await promise_rejects_js(t, TypeError, read1, 'pending read must reject after releaseLock()'); + + // enqueue() should fulfill the read request and put remaining byte in the queue + controller.enqueue(new Uint8Array([0x22])); + assert_equals(controller.byobRequest, null, 'byobRequest should be invalidated after second respond()'); + + const result2 = await read2; + assert_false(result2.done, 'second result.done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x11]), 'second result.value'); + + const result3 = await reader2.read(); + assert_false(result3.done, 'third result.done'); + assert_typed_array_equals(result3.value, new Uint8Array([0x22]), 'third result.value'); + +}, 'ReadableStream with byte source: read(view) with 1 element Uint16Array, respond(1), releaseLock(), read() on ' + + 'second reader, enqueue()'); + +promise_test(async t => { + // Tests https://github.com/nodejs/node/issues/41886 + const stream = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 10, + pull: t.step_func((c) => { + const newView = new Uint8Array(c.byobRequest.view.buffer, 0, 3); + newView.set([20, 21, 22]); + c.byobRequest.respondWithNewView(newView); + }) + }); + + const reader = stream.getReader(); + const result = await reader.read(); + assert_false(result.done, 'result.done'); + + const view = result.value; + assert_equals(view.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(view.byteLength, 3, 'result.value.byteLength'); + assert_equals(view.buffer.byteLength, 10, 'result.value.buffer.byteLength'); + assert_array_equals([...new Uint8Array(view)], [20, 21, 22], 'result.value'); +}, 'ReadableStream with byte source: autoAllocateChunkSize, read(), respondWithNewView()'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/non-transferable-buffers.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/non-transferable-buffers.any.js new file mode 100644 index 000000000000..a70bb6cb23c7 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/non-transferable-buffers.any.js @@ -0,0 +1,70 @@ +// META: global=window,worker +'use strict'; + +promise_test(async t => { + const rs = new ReadableStream({ + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = rs.getReader({ mode: 'byob' }); + const memory = new WebAssembly.Memory({ initial: 1 }); + const view = new Uint8Array(memory.buffer, 0, 1); + await promise_rejects_js(t, TypeError, reader.read(view)); +}, 'ReadableStream with byte source: read() with a non-transferable buffer'); + +promise_test(async t => { + const rs = new ReadableStream({ + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = rs.getReader({ mode: 'byob' }); + const memory = new WebAssembly.Memory({ initial: 1 }); + const view = new Uint8Array(memory.buffer, 0, 1); + await promise_rejects_js(t, TypeError, reader.read(view, { min: 1 })); +}, 'ReadableStream with byte source: fill() with a non-transferable buffer'); + +test(t => { + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const memory = new WebAssembly.Memory({ initial: 1 }); + const view = new Uint8Array(memory.buffer, 0, 1); + assert_throws_js(TypeError, () => controller.enqueue(view)); +}, 'ReadableStream with byte source: enqueue() with a non-transferable buffer'); + +promise_test(async t => { + let byobRequest; + let resolvePullCalledPromise; + const pullCalledPromise = new Promise(resolve => { + resolvePullCalledPromise = resolve; + }); + const rs = new ReadableStream({ + pull(controller) { + byobRequest = controller.byobRequest; + resolvePullCalledPromise(); + }, + type: 'bytes' + }); + + const memory = new WebAssembly.Memory({ initial: 1 }); + // Make sure the backing buffers of both views have the same length + const byobView = new Uint8Array(new ArrayBuffer(memory.buffer.byteLength), 0, 1); + const newView = new Uint8Array(memory.buffer, byobView.byteOffset, byobView.byteLength); + + const reader = rs.getReader({ mode: 'byob' }); + reader.read(byobView).then( + t.unreached_func('read() should not resolve'), + t.unreached_func('read() should not reject') + ); + await pullCalledPromise; + + assert_throws_js(TypeError, () => byobRequest.respondWithNewView(newView)); +}, 'ReadableStream with byte source: respondWithNewView() with a non-transferable buffer'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/patched-global.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/patched-global.any.js new file mode 100644 index 000000000000..39aa40e591ec --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/patched-global.any.js @@ -0,0 +1,54 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +'use strict'; + +// Tests which patch the global environment are kept separate to avoid +// interfering with other tests. + +promise_test(async (t) => { + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + const reader = rs.getReader({mode: 'byob'}); + + const length = 0x4000; + const buffer = new ArrayBuffer(length); + const bigArray = new BigUint64Array(buffer, length - 8, 1); + + const read1 = reader.read(new Uint8Array(new ArrayBuffer(0x100))); + const read2 = reader.read(bigArray); + + let flag = false; + Object.defineProperty(Object.prototype, 'then', { + get: t.step_func(() => { + if (!flag) { + flag = true; + assert_equals(controller.byobRequest, null, 'byobRequest should be null after filling both views'); + } + }), + configurable: true + }); + t.add_cleanup(() => { + delete Object.prototype.then; + }); + + controller.enqueue(new Uint8Array(0x110).fill(0x42)); + assert_true(flag, 'patched then() should be called'); + + // The first read() is filled entirely with 0x100 bytes + const result1 = await read1; + assert_false(result1.done, 'result1.done'); + assert_typed_array_equals(result1.value, new Uint8Array(0x100).fill(0x42), 'result1.value'); + + // The second read() is filled with the remaining 0x10 bytes + const result2 = await read2; + assert_false(result2.done, 'result2.done'); + assert_equals(result2.value.constructor, BigUint64Array, 'result2.value constructor'); + assert_equals(result2.value.byteOffset, length - 8, 'result2.value byteOffset'); + assert_equals(result2.value.length, 1, 'result2.value length'); + assert_array_equals([...result2.value], [0x42424242_42424242n], 'result2.value contents'); +}, 'Patched then() sees byobRequest after filling all pending pull-into descriptors'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/read-min.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/read-min.any.js new file mode 100644 index 000000000000..a5d6ad944be5 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/read-min.any.js @@ -0,0 +1,774 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +'use strict'; + +// View buffers are detached after pull() returns, so record the information at the time that pull() was called. +function extractViewInfo(view) { + return { + constructor: view.constructor, + bufferByteLength: view.buffer.byteLength, + byteOffset: view.byteOffset, + byteLength: view.byteLength + }; +} + +promise_test(async t => { + const rs = new ReadableStream({ + type: 'bytes', + pull: t.unreached_func('pull() should not be called'), + }); + const reader = rs.getReader({ mode: 'byob' }); + await promise_rejects_js(t, TypeError, reader.read(new Uint8Array(1), { min: 0 })); +}, 'ReadableStream with byte source: read({ min }) rejects if min is 0'); + +promise_test(async t => { + const rs = new ReadableStream({ + type: 'bytes', + pull: t.unreached_func('pull() should not be called'), + }); + const reader = rs.getReader({ mode: 'byob' }); + await promise_rejects_js(t, TypeError, reader.read(new Uint8Array(1), { min: -1 })); +}, 'ReadableStream with byte source: read({ min }) rejects if min is negative'); + +promise_test(async t => { + const rs = new ReadableStream({ + type: 'bytes', + pull: t.unreached_func('pull() should not be called'), + }); + const reader = rs.getReader({ mode: 'byob' }); + await promise_rejects_js(t, RangeError, reader.read(new Uint8Array(1), { min: 2 })); +}, 'ReadableStream with byte source: read({ min }) rejects if min is larger than view\'s length (Uint8Array)'); + +promise_test(async t => { + const rs = new ReadableStream({ + type: 'bytes', + pull: t.unreached_func('pull() should not be called'), + }); + const reader = rs.getReader({ mode: 'byob' }); + await promise_rejects_js(t, RangeError, reader.read(new Uint16Array(1), { min: 2 })); +}, 'ReadableStream with byte source: read({ min }) rejects if min is larger than view\'s length (Uint16Array)'); + +promise_test(async t => { + const rs = new ReadableStream({ + type: 'bytes', + pull: t.unreached_func('pull() should not be called'), + }); + const reader = rs.getReader({ mode: 'byob' }); + await promise_rejects_js(t, RangeError, reader.read(new DataView(new ArrayBuffer(1)), { min: 2 })); +}, 'ReadableStream with byte source: read({ min }) rejects if min is larger than view\'s length (DataView)'); + +promise_test(async t => { + let pullCount = 0; + const byobRequests = []; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + const byobRequest = c.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + view[0] = 0x01; + view[1] = 0x02; + byobRequest.respond(2); + } else if (pullCount === 1) { + view[0] = 0x03; + byobRequest.respond(1); + } else if (pullCount === 2) { + view[0] = 0x04; + byobRequest.respond(1); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + const read1 = reader.read(new Uint8Array(3), { min: 3 }); + const read2 = reader.read(new Uint8Array(1)); + + const result1 = await read1; + assert_false(result1.done, 'first result should not be done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x01, 0x02, 0x03]), 'first result value'); + + const result2 = await read2; + assert_false(result2.done, 'second result should not be done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x04]), 'second result value'); + + assert_equals(pullCount, 3, 'pull() must have been called 3 times'); + + { + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3'); + assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 3, 'first view.byteLength should be 3'); + } + + { + const byobRequest = byobRequests[1]; + assert_true(byobRequest.nonNull, 'second byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 3, 'second view.buffer.byteLength should be 3'); + assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2'); + assert_equals(viewInfo.byteLength, 1, 'second view.byteLength should be 1'); + } + + { + const byobRequest = byobRequests[2]; + assert_true(byobRequest.nonNull, 'third byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'third byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'third view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 1, 'third view.buffer.byteLength should be 1'); + assert_equals(viewInfo.byteOffset, 0, 'third view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 1, 'third view.byteLength should be 1'); + } + +}, 'ReadableStream with byte source: read({ min }), then read()'); + +promise_test(async t => { + let pullCount = 0; + const byobRequests = []; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + const byobRequest = c.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + view[0] = 0x01; + view[1] = 0x02; + byobRequest.respond(2); + } else if (pullCount === 1) { + view[0] = 0x03; + byobRequest.respond(1); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + + const result = await reader.read(new DataView(new ArrayBuffer(3)), { min: 3 }); + assert_false(result.done, 'result should not be done'); + assert_equals(result.value.constructor, DataView, 'result.value must be a DataView'); + assert_equals(result.value.byteOffset, 0, 'result.value.byteOffset'); + assert_equals(result.value.byteLength, 3, 'result.value.byteLength'); + assert_equals(result.value.buffer.byteLength, 3, 'result.value.buffer.byteLength'); + assert_array_equals([...new Uint8Array(result.value.buffer)], [0x01, 0x02, 0x03], `result.value.buffer contents`); + + assert_equals(pullCount, 2, 'pull() must have been called 2 times'); + + { + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3'); + assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 3, 'first view.byteLength should be 3'); + } + + { + const byobRequest = byobRequests[1]; + assert_true(byobRequest.nonNull, 'second byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 3, 'second view.buffer.byteLength should be 3'); + assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2'); + assert_equals(viewInfo.byteLength, 1, 'second view.byteLength should be 1'); + } + +}, 'ReadableStream with byte source: read({ min }) with a DataView'); + +promise_test(async t => { + let pullCount = 0; + const byobRequests = []; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + c.enqueue(new Uint8Array([0x01])); + }), + pull: t.step_func((c) => { + const byobRequest = c.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + view[0] = 0x02; + view[1] = 0x03; + byobRequest.respond(2); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array(3), { min: 3 }); + assert_false(result.done, 'first result should not be done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03]), 'first result value'); + + assert_equals(pullCount, 1, 'pull() must have only been called once'); + + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3'); + assert_equals(viewInfo.byteOffset, 1, 'first view.byteOffset should be 1'); + assert_equals(viewInfo.byteLength, 2, 'first view.byteLength should be 2'); + +}, 'ReadableStream with byte source: enqueue(), then read({ min })'); + +promise_test(async t => { + let pullCount = 0; + const byobRequests = []; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + const byobRequest = c.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + c.enqueue(new Uint8Array([0x01, 0x02])); + } else if (pullCount === 1) { + c.enqueue(new Uint8Array([0x03])); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array(3), { min: 3 }); + assert_false(result.done, 'first result should not be done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03]), 'first result value'); + + assert_equals(pullCount, 2, 'pull() must have been called 2 times'); + + { + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 3, 'first view.buffer.byteLength should be 3'); + assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 3, 'first view.byteLength should be 3'); + } + + { + const byobRequest = byobRequests[1]; + assert_true(byobRequest.nonNull, 'second byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 3, 'second view.buffer.byteLength should be 3'); + assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2'); + assert_equals(viewInfo.byteLength, 1, 'second view.byteLength should be 1'); + } + +}, 'ReadableStream with byte source: read({ min: 3 }) on a 3-byte Uint8Array, then multiple enqueue() up to 3 bytes'); + +promise_test(async t => { + let pullCount = 0; + const byobRequests = []; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + const byobRequest = c.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + c.enqueue(new Uint8Array([0x01, 0x02])); + } else if (pullCount === 1) { + c.enqueue(new Uint8Array([0x03])); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array(5), { min: 3 }); + assert_false(result.done, 'first result should not be done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03, 0, 0]).subarray(0, 3), 'first result value'); + + assert_equals(pullCount, 2, 'pull() must have been called 2 times'); + + { + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 5, 'first view.buffer.byteLength should be 5'); + assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 5, 'first view.byteLength should be 5'); + } + + { + const byobRequest = byobRequests[1]; + assert_true(byobRequest.nonNull, 'second byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 5, 'second view.buffer.byteLength should be 5'); + assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2'); + assert_equals(viewInfo.byteLength, 3, 'second view.byteLength should be 3'); + } + +}, 'ReadableStream with byte source: read({ min: 3 }) on a 5-byte Uint8Array, then multiple enqueue() up to 3 bytes'); + +promise_test(async t => { + let pullCount = 0; + const byobRequests = []; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + const byobRequest = c.byobRequest; + const view = byobRequest.view; + byobRequests[pullCount] = { + nonNull: byobRequest !== null, + viewNonNull: view !== null, + viewInfo: extractViewInfo(view) + }; + if (pullCount === 0) { + c.enqueue(new Uint8Array([0x01, 0x02])); + } else if (pullCount === 1) { + c.enqueue(new Uint8Array([0x03, 0x04])); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array(5), { min: 3 }); + assert_false(result.done, 'first result should not be done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03, 0x04, 0]).subarray(0, 4), 'first result value'); + + assert_equals(pullCount, 2, 'pull() must have been called 2 times'); + + { + const byobRequest = byobRequests[0]; + assert_true(byobRequest.nonNull, 'first byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'first byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'first view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 5, 'first view.buffer.byteLength should be 5'); + assert_equals(viewInfo.byteOffset, 0, 'first view.byteOffset should be 0'); + assert_equals(viewInfo.byteLength, 5, 'first view.byteLength should be 5'); + } + + { + const byobRequest = byobRequests[1]; + assert_true(byobRequest.nonNull, 'second byobRequest must not be null'); + assert_true(byobRequest.viewNonNull, 'second byobRequest.view must not be null'); + const viewInfo = byobRequest.viewInfo; + assert_equals(viewInfo.constructor, Uint8Array, 'second view.constructor should be Uint8Array'); + assert_equals(viewInfo.bufferByteLength, 5, 'second view.buffer.byteLength should be 5'); + assert_equals(viewInfo.byteOffset, 2, 'second view.byteOffset should be 2'); + assert_equals(viewInfo.byteLength, 3, 'second view.byteLength should be 3'); + } + +}, 'ReadableStream with byte source: read({ min: 3 }) on a 5-byte Uint8Array, then multiple enqueue() up to 4 bytes'); + +promise_test(async t => { + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(16); + view[0] = 0x01; + view[8] = 0x02; + c.enqueue(view); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const byobReader = stream.getReader({ mode: 'byob' }); + const result1 = await byobReader.read(new Uint8Array(8), { min: 8 }); + assert_false(result1.done, 'result1.done'); + + const view1 = result1.value; + assert_equals(view1.constructor, Uint8Array, 'result1.value.constructor'); + assert_equals(view1.buffer.byteLength, 8, 'result1.value.buffer.byteLength'); + assert_equals(view1.byteOffset, 0, 'result1.value.byteOffset'); + assert_equals(view1.byteLength, 8, 'result1.value.byteLength'); + assert_equals(view1[0], 0x01, 'result1.value[0]'); + + byobReader.releaseLock(); + + const reader = stream.getReader(); + const result2 = await reader.read(); + assert_false(result2.done, 'result2.done'); + + const view2 = result2.value; + assert_equals(view2.constructor, Uint8Array, 'result2.value.constructor'); + assert_equals(view2.buffer.byteLength, 16, 'result2.value.buffer.byteLength'); + assert_equals(view2.byteOffset, 8, 'result2.value.byteOffset'); + assert_equals(view2.byteLength, 8, 'result2.value.byteLength'); + assert_equals(view2[0], 0x02, 'result2.value[0]'); +}, 'ReadableStream with byte source: enqueue(), read({ min }) partially, then read()'); + +promise_test(async () => { + let pullCount = 0; + const byobRequestDefined = []; + let byobRequestViewDefined; + + const stream = new ReadableStream({ + async pull(c) { + byobRequestDefined.push(c.byobRequest !== null); + const initialByobRequest = c.byobRequest; + + const transferredView = await transferArrayBufferView(c.byobRequest.view); + transferredView[0] = 0x01; + c.byobRequest.respondWithNewView(transferredView); + + byobRequestDefined.push(c.byobRequest !== null); + byobRequestViewDefined = initialByobRequest.view !== null; + + ++pullCount; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const result = await reader.read(new Uint8Array(1), { min: 1 }); + assert_false(result.done, 'result.done'); + assert_equals(result.value.byteLength, 1, 'result.value.byteLength'); + assert_equals(result.value[0], 0x01, 'result.value[0]'); + assert_equals(pullCount, 1, 'pull() should be called only once'); + assert_true(byobRequestDefined[0], 'byobRequest must not be null before respondWithNewView()'); + assert_false(byobRequestDefined[1], 'byobRequest must be null after respondWithNewView()'); + assert_false(byobRequestViewDefined, 'view of initial byobRequest must be null after respondWithNewView()'); +}, 'ReadableStream with byte source: read({ min }), then respondWithNewView() with a transferred ArrayBuffer'); + +promise_test(async t => { + const stream = new ReadableStream({ + start(c) { + c.close(); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array([0x01]), { min: 1 }); + assert_true(result.done, 'result.done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01]).subarray(0, 0), 'result.value'); + + await reader.closed; +}, 'ReadableStream with byte source: read({ min }) on a closed stream'); + +promise_test(async t => { + let pullCount = 0; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + if (pullCount === 0) { + c.byobRequest.view[0] = 0x01; + c.byobRequest.respond(1); + } else if (pullCount === 1) { + c.close(); + c.byobRequest.respond(0); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array(3), { min: 3 }); + assert_true(result.done, 'result.done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01, 0, 0]).subarray(0, 1), 'result.value'); + + assert_equals(pullCount, 2, 'pull() must have been called 2 times'); + + await reader.closed; +}, 'ReadableStream with byte source: read({ min }) when closed before view is filled'); + +promise_test(async t => { + let pullCount = 0; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + if (pullCount === 0) { + c.byobRequest.view[0] = 0x01; + c.byobRequest.view[1] = 0x02; + c.byobRequest.respond(2); + } else if (pullCount === 1) { + c.byobRequest.view[0] = 0x03; + c.byobRequest.respond(1); + c.close(); + } + ++pullCount; + }) + }); + const reader = rs.getReader({ mode: 'byob' }); + + const result = await reader.read(new Uint8Array(3), { min: 3 }); + assert_false(result.done, 'result.done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01, 0x02, 0x03]), 'result.value'); + + assert_equals(pullCount, 2, 'pull() must have been called 2 times'); + + await reader.closed; +}, 'ReadableStream with byte source: read({ min }) when closed immediately after view is filled'); + +promise_test(async t => { + const error1 = new Error('error1'); + const stream = new ReadableStream({ + start(c) { + c.error(error1); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const read = reader.read(new Uint8Array(1), { min: 1 }); + + await Promise.all([ + promise_rejects_exactly(t, error1, read, 'read() must fail'), + promise_rejects_exactly(t, error1, reader.closed, 'closed must fail') + ]); +}, 'ReadableStream with byte source: read({ min }) on an errored stream'); + +promise_test(async t => { + const error1 = new Error('error1'); + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const read = reader.read(new Uint8Array(1), { min: 1 }); + + controller.error(error1); + + await Promise.all([ + promise_rejects_exactly(t, error1, read, 'read() must fail'), + promise_rejects_exactly(t, error1, reader.closed, 'closed must fail') + ]); +}, 'ReadableStream with byte source: read({ min }), then error()'); + +promise_test(t => { + let cancelCount = 0; + let reason; + + const passedReason = new TypeError('foo'); + + const stream = new ReadableStream({ + pull: t.unreached_func('pull() should not be called'), + cancel(r) { + if (cancelCount === 0) { + reason = r; + } + + ++cancelCount; + + return 'bar'; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const readPromise = reader.read(new Uint8Array(1), { min: 1 }).then(result => { + assert_true(result.done, 'result.done'); + assert_equals(result.value, undefined, 'result.value'); + }); + + const cancelPromise = reader.cancel(passedReason).then(result => { + assert_equals(result, undefined, 'cancel() return value should be fulfilled with undefined'); + assert_equals(cancelCount, 1, 'cancel() should be called only once'); + assert_equals(reason, passedReason, 'reason should equal the passed reason'); + }); + + return Promise.all([readPromise, cancelPromise]); +}, 'ReadableStream with byte source: getReader(), read({ min }), then cancel()'); + +promise_test(async t => { + let pullCount = 0; + let byobRequest; + const viewInfos = []; + const rs = new ReadableStream({ + type: 'bytes', + pull: t.step_func((c) => { + byobRequest = c.byobRequest; + + viewInfos.push(extractViewInfo(c.byobRequest.view)); + c.byobRequest.view[0] = 0x01; + c.byobRequest.respond(1); + viewInfos.push(extractViewInfo(c.byobRequest.view)); + + ++pullCount; + }) + }); + + await Promise.resolve(); + assert_equals(pullCount, 0, 'pull() must not have been called yet'); + + const reader = rs.getReader({ mode: 'byob' }); + const read = reader.read(new Uint8Array(3), { min: 3 }); + assert_equals(pullCount, 1, 'pull() must have been called once'); + assert_not_equals(byobRequest, null, 'byobRequest should not be null'); + assert_equals(viewInfos[0].byteLength, 3, 'byteLength before respond() should be 3'); + assert_equals(viewInfos[1].byteLength, 2, 'byteLength after respond() should be 2'); + + reader.cancel().catch(t.unreached_func('cancel() should not reject')); + + const result = await read; + assert_true(result.done, 'result.done'); + assert_equals(result.value, undefined, 'result.value'); + + assert_equals(pullCount, 1, 'pull() must only be called once'); + + await reader.closed; +}, 'ReadableStream with byte source: cancel() with partially filled pending read({ min }) request'); + +promise_test(async () => { + let pullCalled = false; + + const stream = new ReadableStream({ + start(c) { + const view = new Uint8Array(16); + view[7] = 0x01; + view[15] = 0x02; + c.enqueue(view); + }, + pull() { + pullCalled = true; + }, + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + const result1 = await reader.read(new Uint8Array(8), { min: 8 }); + assert_false(result1.done, 'result1.done'); + + const view1 = result1.value; + assert_equals(view1.byteOffset, 0, 'result1.value.byteOffset'); + assert_equals(view1.byteLength, 8, 'result1.value.byteLength'); + assert_equals(view1[7], 0x01, 'result1.value[7]'); + + const result2 = await reader.read(new Uint8Array(8), { min: 8 }); + assert_false(pullCalled, 'pull() must not have been called'); + assert_false(result2.done, 'result2.done'); + + const view2 = result2.value; + assert_equals(view2.byteOffset, 0, 'result2.value.byteOffset'); + assert_equals(view2.byteLength, 8, 'result2.value.byteLength'); + assert_equals(view2[7], 0x02, 'result2.value[7]'); +}, 'ReadableStream with byte source: enqueue(), then read({ min }) with smaller views'); + +promise_test(async t => { + const stream = new ReadableStream({ + start(c) { + c.enqueue(new Uint8Array([0xaa, 0xbb, 0xcc])); + c.close(); + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + + await promise_rejects_js(t, TypeError, reader.read(new Uint16Array(2), { min: 2 }), 'read() must fail'); + await promise_rejects_js(t, TypeError, reader.closed, 'reader.closed should reject'); +}, 'ReadableStream with byte source: 3 byte enqueue(), then close(), then read({ min }) with 2-element Uint16Array must fail'); + +promise_test(async t => { + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + }, + pull: t.unreached_func('pull() should not be called'), + type: 'bytes' + }); + + const reader = stream.getReader({ mode: 'byob' }); + const readPromise = reader.read(new Uint16Array(2), { min: 2 }); + + controller.enqueue(new Uint8Array([0xaa, 0xbb, 0xcc])); + assert_throws_js(TypeError, () => controller.close(), 'controller.close() must throw'); + + await promise_rejects_js(t, TypeError, readPromise, 'read() must fail'); + await promise_rejects_js(t, TypeError, reader.closed, 'reader.closed must reject'); +}, 'ReadableStream with byte source: read({ min }) with 2-element Uint16Array, then 3 byte enqueue(), then close() must fail'); + +promise_test(async t => { + let pullCount = 0; + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start: t.step_func((c) => { + controller = c; + }), + pull: t.step_func((c) => { + ++pullCount; + }) + }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + await Promise.resolve(); + assert_equals(pullCount, 0, 'pull() must not have been called yet'); + + const read1 = reader1.read(new Uint8Array(3), { min: 3 }); + const read2 = reader2.read(new Uint8Array(1)); + + assert_equals(pullCount, 1, 'pull() must have been called once'); + const byobRequest1 = controller.byobRequest; + assert_equals(byobRequest1.view.byteLength, 3, 'first byobRequest.view.byteLength should be 3'); + byobRequest1.view[0] = 0x01; + byobRequest1.respond(1); + + const result2 = await read2; + assert_false(result2.done, 'branch2 first read() should not be done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x01]), 'branch2 first read() value'); + + assert_equals(pullCount, 2, 'pull() must have been called 2 times'); + const byobRequest2 = controller.byobRequest; + assert_equals(byobRequest2.view.byteLength, 2, 'second byobRequest.view.byteLength should be 2'); + byobRequest2.view[0] = 0x02; + byobRequest2.view[1] = 0x03; + byobRequest2.respond(2); + + const result1 = await read1; + assert_false(result1.done, 'branch1 read() should not be done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x01, 0x02, 0x03]), 'branch1 read() value'); + + const result3 = await reader2.read(new Uint8Array(2)); + assert_equals(pullCount, 2, 'pull() must only be called 2 times'); + assert_false(result3.done, 'branch2 second read() should not be done'); + assert_typed_array_equals(result3.value, new Uint8Array([0x02, 0x03]), 'branch2 second read() value'); +}, 'ReadableStream with byte source: tee() with read({ min }) from branch1 and read() from branch2'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/respond-after-enqueue.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/respond-after-enqueue.any.js new file mode 100644 index 000000000000..b93cec97391e --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/respond-after-enqueue.any.js @@ -0,0 +1,55 @@ +// META: global=window,worker + +'use strict'; + +// Repro for Blink bug https://crbug.com/1255762. +promise_test(async () => { + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 10, + pull(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])); + controller.byobRequest.respond(10); + } + }); + + const reader = rs.getReader(); + const {value, done} = await reader.read(); + assert_false(done, 'done should not be true'); + assert_array_equals(value, [1, 2, 3], 'value should be 3 bytes'); +}, 'byobRequest.respond() after enqueue() should not crash'); + +promise_test(async () => { + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 10, + pull(controller) { + const byobRequest = controller.byobRequest; + controller.enqueue(new Uint8Array([1, 2, 3])); + byobRequest.respond(10); + } + }); + + const reader = rs.getReader(); + const {value, done} = await reader.read(); + assert_false(done, 'done should not be true'); + assert_array_equals(value, [1, 2, 3], 'value should be 3 bytes'); +}, 'byobRequest.respond() with cached byobRequest after enqueue() should not crash'); + +promise_test(async () => { + const rs = new ReadableStream({ + type: 'bytes', + autoAllocateChunkSize: 10, + pull(controller) { + controller.enqueue(new Uint8Array([1, 2, 3])); + controller.byobRequest.respond(2); + } + }); + + const reader = rs.getReader(); + const [read1, read2] = await Promise.all([reader.read(), reader.read()]); + assert_false(read1.done, 'read1.done should not be true'); + assert_array_equals(read1.value, [1, 2, 3], 'read1.value should be 3 bytes'); + assert_false(read2.done, 'read2.done should not be true'); + assert_array_equals(read2.value, [0, 0], 'read2.value should be 2 bytes'); +}, 'byobRequest.respond() after enqueue() with double read should not crash'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/tee.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/tee.any.js new file mode 100644 index 000000000000..9fac6a18a25b --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/tee.any.js @@ -0,0 +1,969 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +// META: script=../resources/rs-test-templates.js +'use strict'; + +test(() => { + + const rs = new ReadableStream({ type: 'bytes' }); + const result = rs.tee(); + + assert_true(Array.isArray(result), 'return value should be an array'); + assert_equals(result.length, 2, 'array should have length 2'); + assert_equals(result[0].constructor, ReadableStream, '0th element should be a ReadableStream'); + assert_equals(result[1].constructor, ReadableStream, '1st element should be a ReadableStream'); + +}, 'ReadableStream teeing with byte source: rs.tee() returns an array of two ReadableStreams'); + +promise_test(async t => { + + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + c.enqueue(new Uint8Array([0x01])); + c.enqueue(new Uint8Array([0x02])); + c.close(); + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader({ mode: 'byob' }); + + reader2.closed.then(t.unreached_func('branch2 should not be closed')); + + { + const result = await reader1.read(new Uint8Array(1)); + assert_equals(result.done, false, 'done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01]), 'value'); + } + + { + const result = await reader1.read(new Uint8Array(1)); + assert_equals(result.done, false, 'done'); + assert_typed_array_equals(result.value, new Uint8Array([0x02]), 'value'); + } + + { + const result = await reader1.read(new Uint8Array(1)); + assert_equals(result.done, true, 'done'); + assert_typed_array_equals(result.value, new Uint8Array([0]).subarray(0, 0), 'value'); + } + + { + const result = await reader2.read(new Uint8Array(1)); + assert_equals(result.done, false, 'done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01]), 'value'); + } + + await reader1.closed; + +}, 'ReadableStream teeing with byte source: should be able to read one branch to the end without affecting the other'); + +promise_test(async () => { + + let pullCount = 0; + const enqueuedChunk = new Uint8Array([0x01]); + const rs = new ReadableStream({ + type: 'bytes', + pull(c) { + ++pullCount; + if (pullCount === 1) { + c.enqueue(enqueuedChunk); + } + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + const [result1, result2] = await Promise.all([reader1.read(), reader2.read()]); + assert_equals(result1.done, false, 'reader1 done'); + assert_equals(result2.done, false, 'reader2 done'); + + const view1 = result1.value; + const view2 = result2.value; + assert_typed_array_equals(view1, new Uint8Array([0x01]), 'reader1 value'); + assert_typed_array_equals(view2, new Uint8Array([0x01]), 'reader2 value'); + + assert_not_equals(view1.buffer, view2.buffer, 'chunks should have different buffers'); + assert_not_equals(enqueuedChunk.buffer, view1.buffer, 'enqueued chunk and branch1\'s chunk should have different buffers'); + assert_not_equals(enqueuedChunk.buffer, view2.buffer, 'enqueued chunk and branch2\'s chunk should have different buffers'); + +}, 'ReadableStream teeing with byte source: chunks should be cloned for each branch'); + +promise_test(async () => { + + let pullCount = 0; + const rs = new ReadableStream({ + type: 'bytes', + pull(c) { + ++pullCount; + if (pullCount === 1) { + c.byobRequest.view[0] = 0x01; + c.byobRequest.respond(1); + } + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader(); + const buffer = new Uint8Array([42, 42, 42]).buffer; + + { + const result = await reader1.read(new Uint8Array(buffer, 0, 1)); + assert_equals(result.done, false, 'done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01, 42, 42]).subarray(0, 1), 'value'); + } + + { + const result = await reader2.read(); + assert_equals(result.done, false, 'done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01]), 'value'); + } + +}, 'ReadableStream teeing with byte source: chunks for BYOB requests from branch 1 should be cloned to branch 2'); + +promise_test(async t => { + + const theError = { name: 'boo!' }; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + c.enqueue(new Uint8Array([0x01])); + c.enqueue(new Uint8Array([0x02])); + }, + pull() { + throw theError; + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader({ mode: 'byob' }); + + { + const result = await reader1.read(new Uint8Array(1)); + assert_equals(result.done, false, 'first read from branch1 should not be done'); + assert_typed_array_equals(result.value, new Uint8Array([0x01]), 'first read from branch1'); + } + + { + const result = await reader1.read(new Uint8Array(1)); + assert_equals(result.done, false, 'second read from branch1 should not be done'); + assert_typed_array_equals(result.value, new Uint8Array([0x02]), 'second read from branch1'); + } + + await promise_rejects_exactly(t, theError, reader1.read(new Uint8Array(1))); + await promise_rejects_exactly(t, theError, reader2.read(new Uint8Array(1))); + + await Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + +}, 'ReadableStream teeing with byte source: errors in the source should propagate to both branches'); + +promise_test(async () => { + + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + c.enqueue(new Uint8Array([0x01])); + c.enqueue(new Uint8Array([0x02])); + c.close(); + } + }); + + const [branch1, branch2] = rs.tee(); + branch1.cancel(); + + const [chunks1, chunks2] = await Promise.all([readableStreamToArray(branch1), readableStreamToArray(branch2)]); + assert_array_equals(chunks1, [], 'branch1 should have no chunks'); + assert_equals(chunks2.length, 2, 'branch2 should have two chunks'); + assert_typed_array_equals(chunks2[0], new Uint8Array([0x01]), 'first chunk from branch2'); + assert_typed_array_equals(chunks2[1], new Uint8Array([0x02]), 'second chunk from branch2'); + +}, 'ReadableStream teeing with byte source: canceling branch1 should not impact branch2'); + +promise_test(async () => { + + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + c.enqueue(new Uint8Array([0x01])); + c.enqueue(new Uint8Array([0x02])); + c.close(); + } + }); + + const [branch1, branch2] = rs.tee(); + branch2.cancel(); + + const [chunks1, chunks2] = await Promise.all([readableStreamToArray(branch1), readableStreamToArray(branch2)]); + assert_equals(chunks1.length, 2, 'branch1 should have two chunks'); + assert_typed_array_equals(chunks1[0], new Uint8Array([0x01]), 'first chunk from branch1'); + assert_typed_array_equals(chunks1[1], new Uint8Array([0x02]), 'second chunk from branch1'); + assert_array_equals(chunks2, [], 'branch2 should have no chunks'); + +}, 'ReadableStream teeing with byte source: canceling branch2 should not impact branch1'); + +templatedRSTeeCancel('ReadableStream teeing with byte source', (extras) => { + return new ReadableStream({ type: 'bytes', ...extras }); +}); + +promise_test(async () => { + + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader({ mode: 'byob' }); + + const promise = Promise.all([reader1.closed, reader2.closed]); + + controller.close(); + + // The branches are created with HWM 0, so we need to read from at least one of them + // to observe the stream becoming closed. + const read1 = await reader1.read(new Uint8Array(1)); + assert_equals(read1.done, true, 'first read from branch1 should be done'); + + await promise; + +}, 'ReadableStream teeing with byte source: closing the original should close the branches'); + +promise_test(async t => { + + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader({ mode: 'byob' }); + + const theError = { name: 'boo!' }; + const promise = Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + + controller.error(theError); + await promise; + +}, 'ReadableStream teeing with byte source: erroring the original should immediately error the branches'); + +promise_test(async t => { + + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + const theError = { name: 'boo!' }; + const promise = Promise.all([ + promise_rejects_exactly(t, theError, reader1.read()), + promise_rejects_exactly(t, theError, reader2.read()) + ]); + + controller.error(theError); + await promise; + +}, 'ReadableStream teeing with byte source: erroring the original should error pending reads from default reader'); + +promise_test(async t => { + + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader({ mode: 'byob' }); + + const theError = { name: 'boo!' }; + const promise = Promise.all([ + promise_rejects_exactly(t, theError, reader1.read(new Uint8Array(1))), + promise_rejects_exactly(t, theError, reader2.read(new Uint8Array(1))) + ]); + + controller.error(theError); + await promise; + +}, 'ReadableStream teeing with byte source: erroring the original should error pending reads from BYOB reader'); + +promise_test(async () => { + + let controller; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader({ mode: 'byob' }); + const cancelPromise = reader2.cancel(); + + controller.enqueue(new Uint8Array([0x01])); + + const read1 = await reader1.read(new Uint8Array(1)); + assert_equals(read1.done, false, 'first read() from branch1 should not be done'); + assert_typed_array_equals(read1.value, new Uint8Array([0x01]), 'first read() from branch1'); + + controller.close(); + + const read2 = await reader1.read(new Uint8Array(1)); + assert_equals(read2.done, true, 'second read() from branch1 should be done'); + + await Promise.all([ + reader1.closed, + cancelPromise + ]); + +}, 'ReadableStream teeing with byte source: canceling branch1 should finish when branch2 reads until end of stream'); + +promise_test(async t => { + + let controller; + const theError = { name: 'boo!' }; + const rs = new ReadableStream({ + type: 'bytes', + start(c) { + controller = c; + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader({ mode: 'byob' }); + const cancelPromise = reader2.cancel(); + + controller.error(theError); + + await Promise.all([ + promise_rejects_exactly(t, theError, reader1.read(new Uint8Array(1))), + cancelPromise + ]); + +}, 'ReadableStream teeing with byte source: canceling branch1 should finish when original stream errors'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + + // Create two branches, each with a HWM of 0. This should result in no chunks being pulled. + rs.tee(); + + await flushAsyncEvents(); + assert_array_equals(rs.events, [], 'pull should not be called'); + +}, 'ReadableStream teeing with byte source: should not pull any chunks if no branches are reading'); + +promise_test(async () => { + + const rs = recordingReadableStream({ + type: 'bytes', + pull(controller) { + controller.enqueue(new Uint8Array([0x01])); + } + }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + await Promise.all([ + reader1.read(new Uint8Array(1)), + reader2.read(new Uint8Array(1)) + ]); + assert_array_equals(rs.events, ['pull'], 'pull should be called once'); + +}, 'ReadableStream teeing with byte source: should only pull enough to fill the emptiest queue'); + +promise_test(async t => { + + const rs = recordingReadableStream({ type: 'bytes' }); + const theError = { name: 'boo!' }; + + rs.controller.error(theError); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + await flushAsyncEvents(); + assert_array_equals(rs.events, [], 'pull should not be called'); + + await Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + +}, 'ReadableStream teeing with byte source: should not pull when original is already errored'); + +for (const branch of [1, 2]) { + promise_test(async t => { + + const rs = recordingReadableStream({ type: 'bytes' }); + const theError = { name: 'boo!' }; + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + await flushAsyncEvents(); + assert_array_equals(rs.events, [], 'pull should not be called'); + + const reader = (branch === 1) ? reader1 : reader2; + const read1 = reader.read(new Uint8Array(1)); + + await flushAsyncEvents(); + assert_array_equals(rs.events, ['pull'], 'pull should be called once'); + + rs.controller.error(theError); + + await Promise.all([ + promise_rejects_exactly(t, theError, read1), + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + + await flushAsyncEvents(); + assert_array_equals(rs.events, ['pull'], 'pull should be called once'); + + }, `ReadableStream teeing with byte source: stops pulling when original stream errors while branch ${branch} is reading`); +} + +promise_test(async t => { + + const rs = recordingReadableStream({ type: 'bytes' }); + const theError = { name: 'boo!' }; + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + await flushAsyncEvents(); + assert_array_equals(rs.events, [], 'pull should not be called'); + + const read1 = reader1.read(new Uint8Array(1)); + const read2 = reader2.read(new Uint8Array(1)); + + await flushAsyncEvents(); + assert_array_equals(rs.events, ['pull'], 'pull should be called once'); + + rs.controller.error(theError); + + await Promise.all([ + promise_rejects_exactly(t, theError, read1), + promise_rejects_exactly(t, theError, read2), + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + + await flushAsyncEvents(); + assert_array_equals(rs.events, ['pull'], 'pull should be called once'); + +}, 'ReadableStream teeing with byte source: stops pulling when original stream errors while both branches are reading'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + const read1 = reader1.read(new Uint8Array([0x11])); + const read2 = reader2.read(new Uint8Array([0x22])); + + const cancel1 = reader1.cancel(); + await flushAsyncEvents(); + const cancel2 = reader2.cancel(); + + const result1 = await read1; + assert_object_equals(result1, { value: undefined, done: true }); + const result2 = await read2; + assert_object_equals(result2, { value: undefined, done: true }); + + await Promise.all([cancel1, cancel2]); + +}, 'ReadableStream teeing with byte source: canceling both branches in sequence with delay'); + +promise_test(async t => { + + const theError = { name: 'boo!' }; + const rs = new ReadableStream({ + type: 'bytes', + cancel() { + throw theError; + } + }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + const read1 = reader1.read(new Uint8Array([0x11])); + const read2 = reader2.read(new Uint8Array([0x22])); + + const cancel1 = reader1.cancel(); + await flushAsyncEvents(); + const cancel2 = reader2.cancel(); + + const result1 = await read1; + assert_object_equals(result1, { value: undefined, done: true }); + const result2 = await read2; + assert_object_equals(result2, { value: undefined, done: true }); + + await Promise.all([ + promise_rejects_exactly(t, theError, cancel1), + promise_rejects_exactly(t, theError, cancel2) + ]); + +}, 'ReadableStream teeing with byte source: failing to cancel when canceling both branches in sequence with delay'); + +promise_test(async () => { + + let cancelResolve; + const cancelCalled = new Promise((resolve) => { + cancelResolve = resolve; + }); + const rs = recordingReadableStream({ + type: 'bytes', + cancel() { + cancelResolve(); + } + }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + const read1 = reader1.read(new Uint8Array([0x11])); + await flushAsyncEvents(); + const read2 = reader2.read(new Uint8Array([0x22])); + await flushAsyncEvents(); + + // We are reading into branch1's buffer. + const byobRequest1 = rs.controller.byobRequest; + assert_not_equals(byobRequest1, null); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([0x11]), 'byobRequest1.view'); + + // Cancelling branch1 should not affect the BYOB request. + const cancel1 = reader1.cancel(); + const result1 = await read1; + assert_equals(result1.done, true); + assert_equals(result1.value, undefined); + await flushAsyncEvents(); + const byobRequest2 = rs.controller.byobRequest; + assert_typed_array_equals(byobRequest2.view, new Uint8Array([0x11]), 'byobRequest2.view'); + + // Cancelling branch1 should invalidate the BYOB request. + const cancel2 = reader2.cancel(); + await cancelCalled; + const byobRequest3 = rs.controller.byobRequest; + assert_equals(byobRequest3, null); + const result2 = await read2; + assert_equals(result2.done, true); + assert_equals(result2.value, undefined); + + await Promise.all([cancel1, cancel2]); + +}, 'ReadableStream teeing with byte source: read from branch1 and branch2, cancel branch1, cancel branch2'); + +promise_test(async () => { + + let cancelResolve; + const cancelCalled = new Promise((resolve) => { + cancelResolve = resolve; + }); + const rs = recordingReadableStream({ + type: 'bytes', + cancel() { + cancelResolve(); + } + }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + const read1 = reader1.read(new Uint8Array([0x11])); + await flushAsyncEvents(); + const read2 = reader2.read(new Uint8Array([0x22])); + await flushAsyncEvents(); + + // We are reading into branch1's buffer. + const byobRequest1 = rs.controller.byobRequest; + assert_not_equals(byobRequest1, null); + assert_typed_array_equals(byobRequest1.view, new Uint8Array([0x11]), 'byobRequest1.view'); + + // Cancelling branch2 should not affect the BYOB request. + const cancel2 = reader2.cancel(); + const result2 = await read2; + assert_equals(result2.done, true); + assert_equals(result2.value, undefined); + await flushAsyncEvents(); + const byobRequest2 = rs.controller.byobRequest; + assert_typed_array_equals(byobRequest2.view, new Uint8Array([0x11]), 'byobRequest2.view'); + + // Cancelling branch1 should invalidate the BYOB request. + const cancel1 = reader1.cancel(); + await cancelCalled; + const byobRequest3 = rs.controller.byobRequest; + assert_equals(byobRequest3, null); + const result1 = await read1; + assert_equals(result1.done, true); + assert_equals(result1.value, undefined); + + await Promise.all([cancel1, cancel2]); + +}, 'ReadableStream teeing with byte source: read from branch1 and branch2, cancel branch2, cancel branch1'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + const read1 = reader1.read(new Uint8Array([0x11])); + await flushAsyncEvents(); + const read2 = reader2.read(new Uint8Array([0x22])); + await flushAsyncEvents(); + + // We are reading into branch1's buffer. + assert_typed_array_equals(rs.controller.byobRequest.view, new Uint8Array([0x11]), 'first byobRequest.view'); + + // Cancelling branch2 should not affect the BYOB request. + reader2.cancel(); + const result2 = await read2; + assert_equals(result2.done, true); + assert_equals(result2.value, undefined); + await flushAsyncEvents(); + assert_typed_array_equals(rs.controller.byobRequest.view, new Uint8Array([0x11]), 'second byobRequest.view'); + + // Respond to the BYOB request. + rs.controller.byobRequest.view[0] = 0x33; + rs.controller.byobRequest.respond(1); + + // branch1 should receive the read chunk. + const result1 = await read1; + assert_equals(result1.done, false); + assert_typed_array_equals(result1.value, new Uint8Array([0x33]), 'first read() from branch1'); + +}, 'ReadableStream teeing with byte source: read from branch1 and branch2, cancel branch2, enqueue to branch1'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + const read1 = reader1.read(new Uint8Array([0x11])); + await flushAsyncEvents(); + const read2 = reader2.read(new Uint8Array([0x22])); + await flushAsyncEvents(); + + // We are reading into branch1's buffer. + assert_typed_array_equals(rs.controller.byobRequest.view, new Uint8Array([0x11]), 'first byobRequest.view'); + + // Cancelling branch1 should not affect the BYOB request. + reader1.cancel(); + const result1 = await read1; + assert_equals(result1.done, true); + assert_equals(result1.value, undefined); + await flushAsyncEvents(); + assert_typed_array_equals(rs.controller.byobRequest.view, new Uint8Array([0x11]), 'second byobRequest.view'); + + // Respond to the BYOB request. + rs.controller.byobRequest.view[0] = 0x33; + rs.controller.byobRequest.respond(1); + + // branch2 should receive the read chunk. + const result2 = await read2; + assert_equals(result2.done, false); + assert_typed_array_equals(result2.value, new Uint8Array([0x33]), 'first read() from branch2'); + +}, 'ReadableStream teeing with byte source: read from branch1 and branch2, cancel branch1, respond to branch2'); + +promise_test(async () => { + + let pullCount = 0; + const byobRequestDefined = []; + const rs = new ReadableStream({ + type: 'bytes', + pull(c) { + ++pullCount; + byobRequestDefined.push(c.byobRequest !== null); + c.enqueue(new Uint8Array([pullCount])); + } + }); + + const [branch1, _] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + + const result1 = await reader1.read(new Uint8Array([0x11])); + assert_equals(result1.done, false, 'first read should not be done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x1]), 'first read'); + assert_equals(pullCount, 1, 'pull() should be called once'); + assert_equals(byobRequestDefined[0], true, 'should have created a BYOB request for first read'); + + reader1.releaseLock(); + const reader2 = branch1.getReader(); + + const result2 = await reader2.read(); + assert_equals(result2.done, false, 'second read should not be done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x2]), 'second read'); + assert_equals(pullCount, 2, 'pull() should be called twice'); + assert_equals(byobRequestDefined[1], false, 'should not have created a BYOB request for second read'); + +}, 'ReadableStream teeing with byte source: pull with BYOB reader, then pull with default reader'); + +promise_test(async () => { + + let pullCount = 0; + const byobRequestDefined = []; + const rs = new ReadableStream({ + type: 'bytes', + pull(c) { + ++pullCount; + byobRequestDefined.push(c.byobRequest !== null); + c.enqueue(new Uint8Array([pullCount])); + } + }); + + const [branch1, _] = rs.tee(); + const reader1 = branch1.getReader(); + + const result1 = await reader1.read(); + assert_equals(result1.done, false, 'first read should not be done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x1]), 'first read'); + assert_equals(pullCount, 1, 'pull() should be called once'); + assert_equals(byobRequestDefined[0], false, 'should not have created a BYOB request for first read'); + + reader1.releaseLock(); + const reader2 = branch1.getReader({ mode: 'byob' }); + + const result2 = await reader2.read(new Uint8Array([0x22])); + assert_equals(result2.done, false, 'second read should not be done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x2]), 'second read'); + assert_equals(pullCount, 2, 'pull() should be called twice'); + assert_equals(byobRequestDefined[1], true, 'should have created a BYOB request for second read'); + +}, 'ReadableStream teeing with byte source: pull with default reader, then pull with BYOB reader'); + +promise_test(async () => { + + const rs = recordingReadableStream({ + type: 'bytes' + }); + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + + // Wait for each branch's start() promise to resolve. + await flushAsyncEvents(); + + const read2 = reader2.read(new Uint8Array([0x22])); + const read1 = reader1.read(new Uint8Array([0x11])); + await flushAsyncEvents(); + + // branch2 should provide the BYOB request. + const byobRequest = rs.controller.byobRequest; + assert_typed_array_equals(byobRequest.view, new Uint8Array([0x22]), 'first BYOB request'); + byobRequest.view[0] = 0x01; + byobRequest.respond(1); + + const result1 = await read1; + assert_equals(result1.done, false, 'first read should not be done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x1]), 'first read'); + + const result2 = await read2; + assert_equals(result2.done, false, 'second read should not be done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x1]), 'second read'); + +}, 'ReadableStream teeing with byte source: read from branch2, then read from branch1'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader({ mode: 'byob' }); + await flushAsyncEvents(); + + const read1 = reader1.read(); + const read2 = reader2.read(new Uint8Array([0x22])); + await flushAsyncEvents(); + + // There should be no BYOB request. + assert_equals(rs.controller.byobRequest, null, 'first BYOB request'); + + // Close the stream. + rs.controller.close(); + + const result1 = await read1; + assert_equals(result1.done, true, 'read from branch1 should be done'); + assert_equals(result1.value, undefined, 'read from branch1'); + + // branch2 should get its buffer back. + const result2 = await read2; + assert_equals(result2.done, true, 'read from branch2 should be done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x22]).subarray(0, 0), 'read from branch2'); + +}, 'ReadableStream teeing with byte source: read from branch1 with default reader, then close while branch2 has pending BYOB read'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader({ mode: 'byob' }); + const reader2 = branch2.getReader(); + await flushAsyncEvents(); + + const read2 = reader2.read(); + const read1 = reader1.read(new Uint8Array([0x11])); + await flushAsyncEvents(); + + // There should be no BYOB request. + assert_equals(rs.controller.byobRequest, null, 'first BYOB request'); + + // Close the stream. + rs.controller.close(); + + const result2 = await read2; + assert_equals(result2.done, true, 'read from branch2 should be done'); + assert_equals(result2.value, undefined, 'read from branch2'); + + // branch1 should get its buffer back. + const result1 = await read1; + assert_equals(result1.done, true, 'read from branch1 should be done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x11]).subarray(0, 0), 'read from branch1'); + +}, 'ReadableStream teeing with byte source: read from branch2 with default reader, then close while branch1 has pending BYOB read'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + await flushAsyncEvents(); + + const read1 = reader1.read(new Uint8Array([0x11])); + const read2 = reader2.read(new Uint8Array([0x22])); + await flushAsyncEvents(); + + // branch1 should provide the BYOB request. + const byobRequest = rs.controller.byobRequest; + assert_typed_array_equals(byobRequest.view, new Uint8Array([0x11]), 'first BYOB request'); + + // Close the stream. + rs.controller.close(); + byobRequest.respond(0); + + // Both branches should get their buffers back. + const result1 = await read1; + assert_equals(result1.done, true, 'first read should be done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x11]).subarray(0, 0), 'first read'); + + const result2 = await read2; + assert_equals(result2.done, true, 'second read should be done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x22]).subarray(0, 0), 'second read'); + +}, 'ReadableStream teeing with byte source: close when both branches have pending BYOB reads'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + const branch1Reads = [reader1.read(), reader1.read()]; + const branch2Reads = [reader2.read(), reader2.read()]; + + await flushAsyncEvents(); + rs.controller.enqueue(new Uint8Array([0x11])); + rs.controller.close(); + + const result1 = await branch1Reads[0]; + assert_equals(result1.done, false, 'first read() from branch1 should be not done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x11]), 'first chunk from branch1 should be correct'); + const result2 = await branch2Reads[0]; + assert_equals(result2.done, false, 'first read() from branch2 should be not done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x11]), 'first chunk from branch2 should be correct'); + + assert_object_equals(await branch1Reads[1], { value: undefined, done: true }, 'second read() from branch1 should be done'); + assert_object_equals(await branch2Reads[1], { value: undefined, done: true }, 'second read() from branch2 should be done'); + +}, 'ReadableStream teeing with byte source: enqueue() and close() while both branches are pulling'); + +promise_test(async () => { + + const rs = recordingReadableStream({ type: 'bytes' }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader({ mode: 'byob' })); + const branch1Reads = [reader1.read(new Uint8Array(1)), reader1.read(new Uint8Array(1))]; + const branch2Reads = [reader2.read(new Uint8Array(1)), reader2.read(new Uint8Array(1))]; + + await flushAsyncEvents(); + rs.controller.byobRequest.view[0] = 0x11; + rs.controller.byobRequest.respond(1); + rs.controller.close(); + + const result1 = await branch1Reads[0]; + assert_equals(result1.done, false, 'first read() from branch1 should be not done'); + assert_typed_array_equals(result1.value, new Uint8Array([0x11]), 'first chunk from branch1 should be correct'); + const result2 = await branch2Reads[0]; + assert_equals(result2.done, false, 'first read() from branch2 should be not done'); + assert_typed_array_equals(result2.value, new Uint8Array([0x11]), 'first chunk from branch2 should be correct'); + + const result3 = await branch1Reads[1]; + assert_equals(result3.done, true, 'second read() from branch1 should be done'); + assert_typed_array_equals(result3.value, new Uint8Array([0]).subarray(0, 0), 'second chunk from branch1 should be correct'); + const result4 = await branch2Reads[1]; + assert_equals(result4.done, true, 'second read() from branch2 should be done'); + assert_typed_array_equals(result4.value, new Uint8Array([0]).subarray(0, 0), 'second chunk from branch2 should be correct'); + +}, 'ReadableStream teeing with byte source: respond() and close() while both branches are pulling'); + +promise_test(async t => { + let pullCount = 0; + const arrayBuffer = new Uint8Array([0x01, 0x02, 0x03]).buffer; + const enqueuedChunk = new Uint8Array(arrayBuffer, 2); + assert_equals(enqueuedChunk.length, 1); + assert_equals(enqueuedChunk.byteOffset, 2); + const rs = new ReadableStream({ + type: 'bytes', + pull(c) { + ++pullCount; + if (pullCount === 1) { + c.enqueue(enqueuedChunk); + } + } + }); + + const [branch1, branch2] = rs.tee(); + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + const [result1, result2] = await Promise.all([reader1.read(), reader2.read()]); + assert_equals(result1.done, false, 'reader1 done'); + assert_equals(result2.done, false, 'reader2 done'); + + const view1 = result1.value; + const view2 = result2.value; + // The first stream has the transferred buffer, but the second stream has the + // cloned buffer. + const underlying = new Uint8Array([0x01, 0x02, 0x03]).buffer; + assert_typed_array_equals(view1, new Uint8Array(underlying, 2), 'reader1 value'); + assert_typed_array_equals(view2, new Uint8Array([0x03]), 'reader2 value'); +}, 'ReadableStream teeing with byte source: reading an array with a byte offset should clone correctly'); diff --git a/test/js/third_party/wpt-streams/streams/readable-byte-streams/templated.any.js b/test/js/third_party/wpt-streams/streams/readable-byte-streams/templated.any.js new file mode 100644 index 000000000000..8438db50e9e6 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-byte-streams/templated.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-test-templates.js +'use strict'; + +templatedRSEmpty('ReadableStream with byte source (empty)', () => { + return new ReadableStream({ type: 'bytes' }); +}); + +templatedRSEmptyReader('ReadableStream with byte source (empty) default reader', () => { + const stream = new ReadableStream({ type: 'bytes' }); + const reader = stream.getReader(); + return { stream, reader, read: () => reader.read() }; +}); + +templatedRSEmptyReader('ReadableStream with byte source (empty) BYOB reader', () => { + const stream = new ReadableStream({ type: 'bytes' }); + const reader = stream.getReader({ mode: 'byob' }); + return { stream, reader, read: () => reader.read(new Uint8Array([0])) }; +}); + +templatedRSThrowAfterCloseOrError('ReadableStream with byte source', (extras) => { + return new ReadableStream({ type: 'bytes', ...extras }); +}); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/async-iterator.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/async-iterator.any.js new file mode 100644 index 000000000000..d815e9d1a16b --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/async-iterator.any.js @@ -0,0 +1,732 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1'); + +function assert_iter_result(iterResult, value, done, message) { + const prefix = message === undefined ? '' : `${message} `; + assert_equals(typeof iterResult, 'object', `${prefix}type is object`); + assert_equals(Object.getPrototypeOf(iterResult), Object.prototype, `${prefix}[[Prototype]]`); + assert_array_equals(Object.getOwnPropertyNames(iterResult).sort(), ['done', 'value'], `${prefix}property names`); + assert_equals(iterResult.value, value, `${prefix}value`); + assert_equals(iterResult.done, done, `${prefix}done`); +} + +test(() => { + const s = new ReadableStream(); + const it = s.values(); + const proto = Object.getPrototypeOf(it); + + const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype); + assert_equals(Object.getPrototypeOf(proto), AsyncIteratorPrototype, 'prototype should extend AsyncIteratorPrototype'); + + const methods = ['next', 'return'].sort(); + assert_array_equals(Object.getOwnPropertyNames(proto).sort(), methods, 'should have all the correct methods'); + + for (const m of methods) { + const propDesc = Object.getOwnPropertyDescriptor(proto, m); + assert_true(propDesc.enumerable, 'method should be enumerable'); + assert_true(propDesc.configurable, 'method should be configurable'); + assert_true(propDesc.writable, 'method should be writable'); + assert_equals(typeof it[m], 'function', 'method should be a function'); + assert_equals(it[m].name, m, 'method should have the correct name'); + } + + assert_equals(it.next.length, 0, 'next should have no parameters'); + assert_equals(it.return.length, 1, 'return should have 1 parameter'); + assert_equals(typeof it.throw, 'undefined', 'throw should not exist'); +}, 'Async iterator instances should have the correct list of properties'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.enqueue(1); + c.enqueue(2); + c.enqueue(3); + c.close(); + } + }); + + const chunks = []; + for await (const chunk of s) { + chunks.push(chunk); + } + assert_array_equals(chunks, [1, 2, 3]); +}, 'Async-iterating a push source'); + +promise_test(async () => { + let i = 1; + const s = new ReadableStream({ + pull(c) { + c.enqueue(i); + if (i >= 3) { + c.close(); + } + i += 1; + } + }); + + const chunks = []; + for await (const chunk of s) { + chunks.push(chunk); + } + assert_array_equals(chunks, [1, 2, 3]); +}, 'Async-iterating a pull source'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.enqueue(undefined); + c.enqueue(undefined); + c.enqueue(undefined); + c.close(); + } + }); + + const chunks = []; + for await (const chunk of s) { + chunks.push(chunk); + } + assert_array_equals(chunks, [undefined, undefined, undefined]); +}, 'Async-iterating a push source with undefined values'); + +promise_test(async () => { + let i = 1; + const s = new ReadableStream({ + pull(c) { + c.enqueue(undefined); + if (i >= 3) { + c.close(); + } + i += 1; + } + }); + + const chunks = []; + for await (const chunk of s) { + chunks.push(chunk); + } + assert_array_equals(chunks, [undefined, undefined, undefined]); +}, 'Async-iterating a pull source with undefined values'); + +promise_test(async () => { + let i = 1; + const s = recordingReadableStream({ + pull(c) { + c.enqueue(i); + if (i >= 3) { + c.close(); + } + i += 1; + }, + }, new CountQueuingStrategy({ highWaterMark: 0 })); + + const it = s.values(); + assert_array_equals(s.events, []); + + const read1 = await it.next(); + assert_iter_result(read1, 1, false); + assert_array_equals(s.events, ['pull']); + + const read2 = await it.next(); + assert_iter_result(read2, 2, false); + assert_array_equals(s.events, ['pull', 'pull']); + + const read3 = await it.next(); + assert_iter_result(read3, 3, false); + assert_array_equals(s.events, ['pull', 'pull', 'pull']); + + const read4 = await it.next(); + assert_iter_result(read4, undefined, true); + assert_array_equals(s.events, ['pull', 'pull', 'pull']); +}, 'Async-iterating a pull source manually'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.error('e'); + }, + }); + + try { + for await (const chunk of s) {} + assert_unreached(); + } catch (e) { + assert_equals(e, 'e'); + } +}, 'Async-iterating an errored stream throws'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.close(); + } + }); + + for await (const chunk of s) { + assert_unreached(); + } +}, 'Async-iterating a closed stream never executes the loop body, but works fine'); + +promise_test(async () => { + const s = new ReadableStream(); + + const loop = async () => { + for await (const chunk of s) { + assert_unreached(); + } + assert_unreached(); + }; + + await Promise.race([ + loop(), + flushAsyncEvents() + ]); +}, 'Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.enqueue(1); + c.enqueue(2); + c.enqueue(3); + c.close(); + }, + }); + + const reader = s.getReader(); + const readResult = await reader.read(); + assert_iter_result(readResult, 1, false); + reader.releaseLock(); + + const chunks = []; + for await (const chunk of s) { + chunks.push(chunk); + } + assert_array_equals(chunks, [2, 3]); +}, 'Async-iterating a partially consumed stream'); + +for (const type of ['throw', 'break', 'return']) { + for (const preventCancel of [false, true]) { + promise_test(async () => { + const s = recordingReadableStream({ + start(c) { + c.enqueue(0); + } + }); + + // use a separate function for the loop body so return does not stop the test + const loop = async () => { + for await (const c of s.values({ preventCancel })) { + if (type === 'throw') { + throw new Error(); + } else if (type === 'break') { + break; + } else if (type === 'return') { + return; + } + } + }; + + try { + await loop(); + } catch (e) {} + + if (preventCancel) { + assert_array_equals(s.events, ['pull'], `cancel() should not be called`); + } else { + assert_array_equals(s.events, ['pull', 'cancel', undefined], `cancel() should be called`); + } + }, `Cancellation behavior when ${type}ing inside loop body; preventCancel = ${preventCancel}`); + } +} + +for (const preventCancel of [false, true]) { + promise_test(async () => { + const s = recordingReadableStream({ + start(c) { + c.enqueue(0); + } + }); + + const it = s.values({ preventCancel }); + await it.return(); + + if (preventCancel) { + assert_array_equals(s.events, [], `cancel() should not be called`); + } else { + assert_array_equals(s.events, ['cancel', undefined], `cancel() should be called`); + } + }, `Cancellation behavior when manually calling return(); preventCancel = ${preventCancel}`); +} + +promise_test(async t => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + if (timesPulled === 0) { + c.enqueue(0); + ++timesPulled; + } else { + c.error(error1); + } + } + }); + + const it = s[Symbol.asyncIterator](); + + const iterResult1 = await it.next(); + assert_iter_result(iterResult1, 0, false, '1st next()'); + + await promise_rejects_exactly(t, error1, it.next(), '2nd next()'); +}, 'next() rejects if the stream errors'); + +promise_test(async () => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + if (timesPulled === 0) { + c.enqueue(0); + ++timesPulled; + } else { + c.error(error1); + } + } + }); + + const it = s[Symbol.asyncIterator](); + + const iterResult = await it.return('return value'); + assert_iter_result(iterResult, 'return value', true); +}, 'return() does not rejects if the stream has not errored yet'); + +promise_test(async t => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + // Do not error in start() because doing so would prevent acquiring a reader/async iterator. + c.error(error1); + } + }); + + const it = s[Symbol.asyncIterator](); + + await flushAsyncEvents(); + await promise_rejects_exactly(t, error1, it.return('return value')); +}, 'return() rejects if the stream has errored'); + +promise_test(async t => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + if (timesPulled === 0) { + c.enqueue(0); + ++timesPulled; + } else { + c.error(error1); + } + } + }); + + const it = s[Symbol.asyncIterator](); + + const iterResult1 = await it.next(); + assert_iter_result(iterResult1, 0, false, '1st next()'); + + await promise_rejects_exactly(t, error1, it.next(), '2nd next()'); + + const iterResult3 = await it.next(); + assert_iter_result(iterResult3, undefined, true, '3rd next()'); +}, 'next() that succeeds; next() that reports an error; next()'); + +promise_test(async () => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + if (timesPulled === 0) { + c.enqueue(0); + ++timesPulled; + } else { + c.error(error1); + } + } + }); + + const it = s[Symbol.asyncIterator](); + + const iterResults = await Promise.allSettled([it.next(), it.next(), it.next()]); + + assert_equals(iterResults[0].status, 'fulfilled', '1st next() promise status'); + assert_iter_result(iterResults[0].value, 0, false, '1st next()'); + + assert_equals(iterResults[1].status, 'rejected', '2nd next() promise status'); + assert_equals(iterResults[1].reason, error1, '2nd next() rejection reason'); + + assert_equals(iterResults[2].status, 'fulfilled', '3rd next() promise status'); + assert_iter_result(iterResults[2].value, undefined, true, '3rd next()'); +}, 'next() that succeeds; next() that reports an error(); next() [no awaiting]'); + +promise_test(async t => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + if (timesPulled === 0) { + c.enqueue(0); + ++timesPulled; + } else { + c.error(error1); + } + } + }); + + const it = s[Symbol.asyncIterator](); + + const iterResult1 = await it.next(); + assert_iter_result(iterResult1, 0, false, '1st next()'); + + await promise_rejects_exactly(t, error1, it.next(), '2nd next()'); + + const iterResult3 = await it.return('return value'); + assert_iter_result(iterResult3, 'return value', true, 'return()'); +}, 'next() that succeeds; next() that reports an error(); return()'); + +promise_test(async () => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + if (timesPulled === 0) { + c.enqueue(0); + ++timesPulled; + } else { + c.error(error1); + } + } + }); + + const it = s[Symbol.asyncIterator](); + + const iterResults = await Promise.allSettled([it.next(), it.next(), it.return('return value')]); + + assert_equals(iterResults[0].status, 'fulfilled', '1st next() promise status'); + assert_iter_result(iterResults[0].value, 0, false, '1st next()'); + + assert_equals(iterResults[1].status, 'rejected', '2nd next() promise status'); + assert_equals(iterResults[1].reason, error1, '2nd next() rejection reason'); + + assert_equals(iterResults[2].status, 'fulfilled', 'return() promise status'); + assert_iter_result(iterResults[2].value, 'return value', true, 'return()'); +}, 'next() that succeeds; next() that reports an error(); return() [no awaiting]'); + +promise_test(async () => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + c.enqueue(timesPulled); + ++timesPulled; + } + }); + const it = s[Symbol.asyncIterator](); + + const iterResult1 = await it.next(); + assert_iter_result(iterResult1, 0, false, 'next()'); + + const iterResult2 = await it.return('return value'); + assert_iter_result(iterResult2, 'return value', true, 'return()'); + + assert_equals(timesPulled, 2); +}, 'next() that succeeds; return()'); + +promise_test(async () => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + c.enqueue(timesPulled); + ++timesPulled; + } + }); + const it = s[Symbol.asyncIterator](); + + const iterResults = await Promise.allSettled([it.next(), it.return('return value')]); + + assert_equals(iterResults[0].status, 'fulfilled', 'next() promise status'); + assert_iter_result(iterResults[0].value, 0, false, 'next()'); + + assert_equals(iterResults[1].status, 'fulfilled', 'return() promise status'); + assert_iter_result(iterResults[1].value, 'return value', true, 'return()'); + + assert_equals(timesPulled, 2); +}, 'next() that succeeds; return() [no awaiting]'); + +promise_test(async () => { + const rs = new ReadableStream(); + const it = rs.values(); + + const iterResult1 = await it.return('return value'); + assert_iter_result(iterResult1, 'return value', true, 'return()'); + + const iterResult2 = await it.next(); + assert_iter_result(iterResult2, undefined, true, 'next()'); +}, 'return(); next()'); + +promise_test(async () => { + const rs = new ReadableStream(); + const it = rs.values(); + + const resolveOrder = []; + const iterResults = await Promise.allSettled([ + it.return('return value').then(result => { + resolveOrder.push('return'); + return result; + }), + it.next().then(result => { + resolveOrder.push('next'); + return result; + }) + ]); + + assert_equals(iterResults[0].status, 'fulfilled', 'return() promise status'); + assert_iter_result(iterResults[0].value, 'return value', true, 'return()'); + + assert_equals(iterResults[1].status, 'fulfilled', 'next() promise status'); + assert_iter_result(iterResults[1].value, undefined, true, 'next()'); + + assert_array_equals(resolveOrder, ['return', 'next'], 'next() resolves after return()'); +}, 'return(); next() [no awaiting]'); + +promise_test(async () => { + let resolveCancelPromise; + const rs = recordingReadableStream({ + cancel(reason) { + return new Promise(r => resolveCancelPromise = r); + } + }); + const it = rs.values(); + + let returnResolved = false; + const returnPromise = it.return('return value').then(result => { + returnResolved = true; + return result; + }); + await flushAsyncEvents(); + assert_false(returnResolved, 'return() should not resolve while cancel() promise is pending'); + + resolveCancelPromise(); + const iterResult1 = await returnPromise; + assert_iter_result(iterResult1, 'return value', true, 'return()'); + + const iterResult2 = await it.next(); + assert_iter_result(iterResult2, undefined, true, 'next()'); +}, 'return(); next() with delayed cancel()'); + +promise_test(async () => { + let resolveCancelPromise; + const rs = recordingReadableStream({ + cancel(reason) { + return new Promise(r => resolveCancelPromise = r); + } + }); + const it = rs.values(); + + const resolveOrder = []; + const returnPromise = it.return('return value').then(result => { + resolveOrder.push('return'); + return result; + }); + const nextPromise = it.next().then(result => { + resolveOrder.push('next'); + return result; + }); + + assert_array_equals(rs.events, ['cancel', 'return value'], 'return() should call cancel()'); + assert_array_equals(resolveOrder, [], 'return() should not resolve before cancel() resolves'); + + resolveCancelPromise(); + const iterResult1 = await returnPromise; + assert_iter_result(iterResult1, 'return value', true, 'return() should resolve with original reason'); + const iterResult2 = await nextPromise; + assert_iter_result(iterResult2, undefined, true, 'next() should resolve with done result'); + + assert_array_equals(rs.events, ['cancel', 'return value'], 'no pull() after cancel()'); + assert_array_equals(resolveOrder, ['return', 'next'], 'next() should resolve after return() resolves'); + +}, 'return(); next() with delayed cancel() [no awaiting]'); + +promise_test(async () => { + const rs = new ReadableStream(); + const it = rs.values(); + + const iterResult1 = await it.return('return value 1'); + assert_iter_result(iterResult1, 'return value 1', true, '1st return()'); + + const iterResult2 = await it.return('return value 2'); + assert_iter_result(iterResult2, 'return value 2', true, '1st return()'); +}, 'return(); return()'); + +promise_test(async () => { + const rs = new ReadableStream(); + const it = rs.values(); + + const resolveOrder = []; + const iterResults = await Promise.allSettled([ + it.return('return value 1').then(result => { + resolveOrder.push('return 1'); + return result; + }), + it.return('return value 2').then(result => { + resolveOrder.push('return 2'); + return result; + }) + ]); + + assert_equals(iterResults[0].status, 'fulfilled', '1st return() promise status'); + assert_iter_result(iterResults[0].value, 'return value 1', true, '1st return()'); + + assert_equals(iterResults[1].status, 'fulfilled', '2nd return() promise status'); + assert_iter_result(iterResults[1].value, 'return value 2', true, '1st return()'); + + assert_array_equals(resolveOrder, ['return 1', 'return 2'], '2nd return() resolves after 1st return()'); +}, 'return(); return() [no awaiting]'); + +test(() => { + const s = new ReadableStream({ + start(c) { + c.enqueue(0); + c.close(); + }, + }); + s.values(); + assert_throws_js(TypeError, () => s.values(), 'values() should throw'); +}, 'values() throws if there\'s already a lock'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.enqueue(1); + c.enqueue(2); + c.enqueue(3); + c.close(); + } + }); + + const chunks = []; + for await (const chunk of s) { + chunks.push(chunk); + } + assert_array_equals(chunks, [1, 2, 3]); + + const reader = s.getReader(); + await reader.closed; +}, 'Acquiring a reader after exhaustively async-iterating a stream'); + +promise_test(async t => { + let timesPulled = 0; + const s = new ReadableStream({ + pull(c) { + if (timesPulled === 0) { + c.enqueue(0); + ++timesPulled; + } else { + c.error(error1); + } + } + }); + + const it = s[Symbol.asyncIterator]({ preventCancel: true }); + + const iterResult1 = await it.next(); + assert_iter_result(iterResult1, 0, false, '1st next()'); + + await promise_rejects_exactly(t, error1, it.next(), '2nd next()'); + + const iterResult2 = await it.return('return value'); + assert_iter_result(iterResult2, 'return value', true, 'return()'); + + // i.e. it should not reject with a generic "this stream is locked" TypeError. + const reader = s.getReader(); + await promise_rejects_exactly(t, error1, reader.closed, 'closed on the new reader should reject with the error'); +}, 'Acquiring a reader after return()ing from a stream that errors'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.enqueue(1); + c.enqueue(2); + c.enqueue(3); + c.close(); + }, + }); + + // read the first two chunks, then cancel + const chunks = []; + for await (const chunk of s) { + chunks.push(chunk); + if (chunk >= 2) { + break; + } + } + assert_array_equals(chunks, [1, 2]); + + const reader = s.getReader(); + await reader.closed; +}, 'Acquiring a reader after partially async-iterating a stream'); + +promise_test(async () => { + const s = new ReadableStream({ + start(c) { + c.enqueue(1); + c.enqueue(2); + c.enqueue(3); + c.close(); + }, + }); + + // read the first two chunks, then release lock + const chunks = []; + for await (const chunk of s.values({preventCancel: true})) { + chunks.push(chunk); + if (chunk >= 2) { + break; + } + } + assert_array_equals(chunks, [1, 2]); + + const reader = s.getReader(); + const readResult = await reader.read(); + assert_iter_result(readResult, 3, false); + await reader.closed; +}, 'Acquiring a reader and reading the remaining chunks after partially async-iterating a stream with preventCancel = true'); + +for (const preventCancel of [false, true]) { + test(() => { + const rs = new ReadableStream(); + rs.values({ preventCancel }).return(); + // The test passes if this line doesn't throw. + rs.getReader(); + }, `return() should unlock the stream synchronously when preventCancel = ${preventCancel}`); +} + +promise_test(async () => { + const rs = new ReadableStream({ + async start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.enqueue('c'); + await flushAsyncEvents(); + // At this point, the async iterator has a read request in the stream's queue for its pending next() promise. + // Closing the stream now causes two things to happen *synchronously*: + // 1. ReadableStreamClose resolves reader.[[closedPromise]] with undefined. + // 2. ReadableStreamClose calls the read request's close steps, which calls ReadableStreamReaderGenericRelease, + // which replaces reader.[[closedPromise]] with a rejected promise. + c.close(); + } + }); + + const chunks = []; + for await (const chunk of rs) { + chunks.push(chunk); + } + assert_array_equals(chunks, ['a', 'b', 'c']); +}, 'close() while next() is pending'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/bad-strategies.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/bad-strategies.any.js new file mode 100644 index 000000000000..409c63b8177e --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/bad-strategies.any.js @@ -0,0 +1,198 @@ +// META: global=window,worker +'use strict'; + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws_exactly(theError, () => { + new ReadableStream({}, { + get size() { + throw theError; + }, + highWaterMark: 5 + }); + }, 'construction should re-throw the error'); + +}, 'Readable stream: throwing strategy.size getter'); + +promise_test(t => { + + const controllerError = { name: 'controller error' }; + const thrownError = { name: 'thrown error' }; + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + { + size() { + controller.error(controllerError); + throw thrownError; + }, + highWaterMark: 5 + } + ); + + assert_throws_exactly(thrownError, () => controller.enqueue('a'), 'enqueue should re-throw the error'); + + return promise_rejects_exactly(t, controllerError, rs.getReader().closed); + +}, 'Readable stream: strategy.size errors the stream and then throws'); + +promise_test(t => { + + const theError = { name: 'my error' }; + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + { + size() { + controller.error(theError); + return Infinity; + }, + highWaterMark: 5 + } + ); + + assert_throws_js(RangeError, () => controller.enqueue('a'), 'enqueue should throw a RangeError'); + + return promise_rejects_exactly(t, theError, rs.getReader().closed, 'closed should reject with the error'); + +}, 'Readable stream: strategy.size errors the stream and then returns Infinity'); + +promise_test(() => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream( + { + start(c) { + assert_throws_exactly(theError, () => c.enqueue('a'), 'enqueue should throw the error'); + } + }, + { + size() { + throw theError; + }, + highWaterMark: 5 + } + ); + + return rs.getReader().closed.catch(e => { + assert_equals(e, theError, 'closed should reject with the error'); + }); + +}, 'Readable stream: throwing strategy.size method'); + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws_exactly(theError, () => { + new ReadableStream({}, { + size() { + return 1; + }, + get highWaterMark() { + throw theError; + } + }); + }, 'construction should re-throw the error'); + +}, 'Readable stream: throwing strategy.highWaterMark getter'); + +test(() => { + + for (const highWaterMark of [-1, -Infinity, NaN, 'foo', {}]) { + assert_throws_js(RangeError, () => { + new ReadableStream({}, { + size() { + return 1; + }, + highWaterMark + }); + }, 'construction should throw a RangeError for ' + highWaterMark); + } + +}, 'Readable stream: invalid strategy.highWaterMark'); + +promise_test(() => { + + const promises = []; + for (const size of [NaN, -Infinity, Infinity, -1]) { + let theError; + const rs = new ReadableStream( + { + start(c) { + try { + c.enqueue('hi'); + assert_unreached('enqueue didn\'t throw'); + } catch (error) { + assert_equals(error.name, 'RangeError', 'enqueue should throw a RangeError for ' + size); + theError = error; + } + } + }, + { + size() { + return size; + }, + highWaterMark: 5 + } + ); + + promises.push(rs.getReader().closed.then(() => { + assert_unreached('closed didn\'t throw'); + }, e => { + assert_equals(e, theError, 'closed should reject with the error for ' + size); + })); + } + + return Promise.all(promises); + +}, 'Readable stream: invalid strategy.size return value'); + +promise_test(() => { + + const promises = []; + for (const size of [NaN, -Infinity, Infinity, -1]) { + let theError; + const rs = new ReadableStream( + { + pull(c) { + try { + c.enqueue('hi'); + assert_unreached('enqueue didn\'t throw'); + } catch (error) { + assert_equals(error.name, 'RangeError', 'enqueue should throw a RangeError for ' + size); + theError = error; + } + } + }, + { + size() { + return size; + }, + highWaterMark: 5 + } + ); + + promises.push(rs.getReader().closed.then(() => { + assert_unreached('closed didn\'t throw'); + }, e => { + assert_equals(e, theError, 'closed should reject with the error for ' + size); + })); + } + + return Promise.all(promises); + +}, 'Readable stream: invalid strategy.size return value when pulling'); + diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/bad-underlying-sources.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/bad-underlying-sources.any.js new file mode 100644 index 000000000000..e9cf4c924930 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/bad-underlying-sources.any.js @@ -0,0 +1,400 @@ +// META: global=window,worker +'use strict'; + + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws_exactly(theError, () => { + new ReadableStream({ + get start() { + throw theError; + } + }); + }, 'constructing the stream should re-throw the error'); + +}, 'Underlying source start: throwing getter'); + + +test(() => { + + const theError = new Error('a unique string'); + + assert_throws_exactly(theError, () => { + new ReadableStream({ + start() { + throw theError; + } + }); + }, 'constructing the stream should re-throw the error'); + +}, 'Underlying source start: throwing method'); + + +test(() => { + + const theError = new Error('a unique string'); + assert_throws_exactly(theError, () => new ReadableStream({ + get pull() { + throw theError; + } + }), 'constructor should throw'); + +}, 'Underlying source: throwing pull getter (initial pull)'); + + +promise_test(t => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream({ + pull() { + throw theError; + } + }); + + return promise_rejects_exactly(t, theError, rs.getReader().closed); + +}, 'Underlying source: throwing pull method (initial pull)'); + + +promise_test(t => { + + const theError = new Error('a unique string'); + + let counter = 0; + const rs = new ReadableStream({ + get pull() { + ++counter; + if (counter === 1) { + return c => c.enqueue('a'); + } + + throw theError; + } + }); + const reader = rs.getReader(); + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'the first chunk read should be correct'); + }), + reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'the second chunk read should be correct'); + assert_equals(counter, 1, 'counter should be 1'); + }) + ]); + +}, 'Underlying source pull: throwing getter (second pull does not result in a second get)'); + +promise_test(t => { + + const theError = new Error('a unique string'); + + let counter = 0; + const rs = new ReadableStream({ + pull(c) { + ++counter; + if (counter === 1) { + c.enqueue('a'); + return; + } + + throw theError; + } + }); + const reader = rs.getReader(); + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'the chunk read should be correct'); + }), + promise_rejects_exactly(t, theError, reader.closed) + ]); + +}, 'Underlying source pull: throwing method (second pull)'); + +test(() => { + + const theError = new Error('a unique string'); + assert_throws_exactly(theError, () => new ReadableStream({ + get cancel() { + throw theError; + } + }), 'constructor should throw'); + +}, 'Underlying source cancel: throwing getter'); + +promise_test(t => { + + const theError = new Error('a unique string'); + const rs = new ReadableStream({ + cancel() { + throw theError; + } + }); + + return promise_rejects_exactly(t, theError, rs.cancel()); + +}, 'Underlying source cancel: throwing method'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + rs.cancel(); + assert_throws_js(TypeError, () => controller.enqueue('a'), 'Calling enqueue after canceling should throw'); + + return rs.getReader().closed; + +}, 'Underlying source: calling enqueue on an empty canceled stream should throw'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + controller = c; + } + }); + + rs.cancel(); + assert_throws_js(TypeError, () => controller.enqueue('c'), 'Calling enqueue after canceling should throw'); + + return rs.getReader().closed; + +}, 'Underlying source: calling enqueue on a non-empty canceled stream should throw'); + +promise_test(() => { + + return new ReadableStream({ + start(c) { + c.close(); + assert_throws_js(TypeError, () => c.enqueue('a'), 'call to enqueue should throw a TypeError'); + } + }).getReader().closed; + +}, 'Underlying source: calling enqueue on a closed stream should throw'); + +promise_test(t => { + + const theError = new Error('boo'); + const closed = new ReadableStream({ + start(c) { + c.error(theError); + assert_throws_js(TypeError, () => c.enqueue('a'), 'call to enqueue should throw the error'); + } + }).getReader().closed; + + return promise_rejects_exactly(t, theError, closed); + +}, 'Underlying source: calling enqueue on an errored stream should throw'); + +promise_test(() => { + + return new ReadableStream({ + start(c) { + c.close(); + assert_throws_js(TypeError, () => c.close(), 'second call to close should throw a TypeError'); + } + }).getReader().closed; + +}, 'Underlying source: calling close twice on an empty stream should throw the second time'); + +promise_test(() => { + + let startCalled = false; + let readCalled = false; + const reader = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.close(); + assert_throws_js(TypeError, () => c.close(), 'second call to close should throw a TypeError'); + startCalled = true; + } + }).getReader(); + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'read() should read the enqueued chunk'); + readCalled = true; + }), + reader.closed.then(() => { + assert_true(startCalled); + assert_true(readCalled); + }) + ]); + +}, 'Underlying source: calling close twice on a non-empty stream should throw the second time'); + +promise_test(() => { + + let controller; + let startCalled = false; + const rs = new ReadableStream({ + start(c) { + controller = c; + startCalled = true; + } + }); + + rs.cancel(); + assert_throws_js(TypeError, () => controller.close(), 'Calling close after canceling should throw'); + + return rs.getReader().closed.then(() => { + assert_true(startCalled); + }); + +}, 'Underlying source: calling close on an empty canceled stream should throw'); + +promise_test(() => { + + let controller; + let startCalled = false; + const rs = new ReadableStream({ + start(c) { + controller = c; + c.enqueue('a'); + startCalled = true; + } + }); + + rs.cancel(); + assert_throws_js(TypeError, () => controller.close(), 'Calling close after canceling should throw'); + + return rs.getReader().closed.then(() => { + assert_true(startCalled); + }); + +}, 'Underlying source: calling close on a non-empty canceled stream should throw'); + +promise_test(() => { + + const theError = new Error('boo'); + let startCalled = false; + + const closed = new ReadableStream({ + start(c) { + c.error(theError); + assert_throws_js(TypeError, () => c.close(), 'call to close should throw a TypeError'); + startCalled = true; + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, theError, 'closed should reject with the error'); + }); + +}, 'Underlying source: calling close after error should throw'); + +promise_test(() => { + + const theError = new Error('boo'); + let startCalled = false; + + const closed = new ReadableStream({ + start(c) { + c.error(theError); + c.error(); + startCalled = true; + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, theError, 'closed should reject with the error'); + }); + +}, 'Underlying source: calling error twice should not throw'); + +promise_test(() => { + + let startCalled = false; + + const closed = new ReadableStream({ + start(c) { + c.close(); + c.error(); + startCalled = true; + } + }).getReader().closed; + + return closed.then(() => assert_true(startCalled)); + +}, 'Underlying source: calling error after close should not throw'); + +promise_test(() => { + + let startCalled = false; + const firstError = new Error('1'); + const secondError = new Error('2'); + + const closed = new ReadableStream({ + start(c) { + c.error(firstError); + startCalled = true; + return Promise.reject(secondError); + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, firstError, 'closed should reject with the first error'); + }); + +}, 'Underlying source: calling error and returning a rejected promise from start should cause the stream to error ' + + 'with the first error'); + +promise_test(() => { + + let startCalled = false; + const firstError = new Error('1'); + const secondError = new Error('2'); + + const closed = new ReadableStream({ + pull(c) { + c.error(firstError); + startCalled = true; + return Promise.reject(secondError); + } + }).getReader().closed; + + return closed.catch(e => { + assert_true(startCalled); + assert_equals(e, firstError, 'closed should reject with the first error'); + }); + +}, 'Underlying source: calling error and returning a rejected promise from pull should cause the stream to error ' + + 'with the first error'); + +const error1 = { name: 'error1' }; + +promise_test(t => { + + let pullShouldThrow = false; + const rs = new ReadableStream({ + pull(controller) { + if (pullShouldThrow) { + throw error1; + } + controller.enqueue(0); + } + }, new CountQueuingStrategy({highWaterMark: 1})); + const reader = rs.getReader(); + return Promise.resolve().then(() => { + pullShouldThrow = true; + return Promise.all([ + reader.read(), + promise_rejects_exactly(t, error1, reader.closed, '.closed promise should reject') + ]); + }); + +}, 'read should not error if it dequeues and pull() throws'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/cancel.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/cancel.any.js new file mode 100644 index 000000000000..8e186be586c1 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/cancel.any.js @@ -0,0 +1,261 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +'use strict'; + +promise_test(t => { + + const randomSource = new RandomPushSource(); + + let cancellationFinished = false; + const rs = new ReadableStream({ + start(c) { + randomSource.ondata = c.enqueue.bind(c); + randomSource.onend = c.close.bind(c); + randomSource.onerror = c.error.bind(c); + }, + + pull() { + randomSource.readStart(); + }, + + cancel() { + randomSource.readStop(); + + return new Promise(resolve => { + t.step_timeout(() => { + cancellationFinished = true; + resolve(); + }, 1); + }); + } + }); + + const reader = rs.getReader(); + + // We call delay multiple times to avoid cancelling too early for the + // source to enqueue at least one chunk. + const cancel = delay(5).then(() => delay(5)).then(() => delay(5)).then(() => { + const cancelPromise = reader.cancel(); + assert_false(cancellationFinished, 'cancellation in source should happen later'); + return cancelPromise; + }); + + return readableStreamToArray(rs, reader).then(chunks => { + assert_greater_than(chunks.length, 0, 'at least one chunk should be read'); + for (let i = 0; i < chunks.length; i++) { + assert_equals(chunks[i].length, 128, 'chunk ' + i + ' should have 128 bytes'); + } + return cancel; + }).then(() => { + assert_true(cancellationFinished, 'it returns a promise that is fulfilled when the cancellation finishes'); + }); + +}, 'ReadableStream cancellation: integration test on an infinite stream derived from a random push source'); + +test(() => { + + let recordedReason; + const rs = new ReadableStream({ + cancel(reason) { + recordedReason = reason; + } + }); + + const passedReason = new Error('Sorry, it just wasn\'t meant to be.'); + rs.cancel(passedReason); + + assert_equals(recordedReason, passedReason, + 'the error passed to the underlying source\'s cancel method should equal the one passed to the stream\'s cancel'); + +}, 'ReadableStream cancellation: cancel(reason) should pass through the given reason to the underlying source'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.close(); + }, + cancel() { + assert_unreached('underlying source cancel() should not have been called'); + } + }); + + const reader = rs.getReader(); + + return rs.cancel().then(() => { + assert_unreached('cancel() should be rejected'); + }, e => { + assert_equals(e.name, 'TypeError', 'cancel() should be rejected with a TypeError'); + }).then(() => { + return reader.read(); + }).then(result => { + assert_object_equals(result, { value: 'a', done: false }, 'read() should still work after the attempted cancel'); + return reader.closed; + }); + +}, 'ReadableStream cancellation: cancel() on a locked stream should fail and not call the underlying source cancel'); + +promise_test(() => { + + let cancelReceived = false; + const cancelReason = new Error('I am tired of this stream, I prefer to cancel it'); + const rs = new ReadableStream({ + cancel(reason) { + cancelReceived = true; + assert_equals(reason, cancelReason, 'cancellation reason given to the underlying source should be equal to the one passed'); + } + }); + + return rs.cancel(cancelReason).then(() => { + assert_true(cancelReceived); + }); + +}, 'ReadableStream cancellation: should fulfill promise when cancel callback went fine'); + +promise_test(() => { + + const rs = new ReadableStream({ + cancel() { + return 'Hello'; + } + }); + + return rs.cancel().then(v => { + assert_equals(v, undefined, 'cancel() return value should be fulfilled with undefined'); + }); + +}, 'ReadableStream cancellation: returning a value from the underlying source\'s cancel should not affect the fulfillment value of the promise returned by the stream\'s cancel'); + +promise_test(() => { + + const thrownError = new Error('test'); + let cancelCalled = false; + + const rs = new ReadableStream({ + cancel() { + cancelCalled = true; + throw thrownError; + } + }); + + return rs.cancel('test').then(() => { + assert_unreached('cancel should reject'); + }, e => { + assert_true(cancelCalled); + assert_equals(e, thrownError); + }); + +}, 'ReadableStream cancellation: should reject promise when cancel callback raises an exception'); + +promise_test(() => { + + const cancelReason = new Error('test'); + + const rs = new ReadableStream({ + cancel(error) { + assert_equals(error, cancelReason); + return delay(1); + } + }); + + return rs.cancel(cancelReason); + +}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (1)'); + +promise_test(t => { + + let resolveSourceCancelPromise; + let sourceCancelPromiseHasFulfilled = false; + + const rs = new ReadableStream({ + cancel() { + const sourceCancelPromise = new Promise(resolve => resolveSourceCancelPromise = resolve); + + sourceCancelPromise.then(() => { + sourceCancelPromiseHasFulfilled = true; + }); + + return sourceCancelPromise; + } + }); + + t.step_timeout(() => resolveSourceCancelPromise('Hello'), 1); + + return rs.cancel().then(value => { + assert_true(sourceCancelPromiseHasFulfilled, 'cancel() return value should be fulfilled only after the promise returned by the underlying source\'s cancel'); + assert_equals(value, undefined, 'cancel() return value should be fulfilled with undefined'); + }); + +}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (2)'); + +promise_test(t => { + + let rejectSourceCancelPromise; + let sourceCancelPromiseHasRejected = false; + + const rs = new ReadableStream({ + cancel() { + const sourceCancelPromise = new Promise((resolve, reject) => rejectSourceCancelPromise = reject); + + sourceCancelPromise.catch(() => { + sourceCancelPromiseHasRejected = true; + }); + + return sourceCancelPromise; + } + }); + + const errorInCancel = new Error('Sorry, it just wasn\'t meant to be.'); + + t.step_timeout(() => rejectSourceCancelPromise(errorInCancel), 1); + + return rs.cancel().then(() => { + assert_unreached('cancel() return value should be rejected'); + }, r => { + assert_true(sourceCancelPromiseHasRejected, 'cancel() return value should be rejected only after the promise returned by the underlying source\'s cancel'); + assert_equals(r, errorInCancel, 'cancel() return value should be rejected with the underlying source\'s rejection reason'); + }); + +}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should reject when that one does'); + +promise_test(() => { + + const rs = new ReadableStream({ + start() { + return new Promise(() => {}); + }, + pull() { + assert_unreached('pull should not have been called'); + } + }); + + return Promise.all([rs.cancel(), rs.getReader().closed]); + +}, 'ReadableStream cancellation: cancelling before start finishes should prevent pull() from being called'); + +promise_test(async () => { + + const events = []; + + const pendingPromise = new Promise(() => {}); + + const rs = new ReadableStream({ + pull() { + events.push('pull'); + return pendingPromise; + }, + cancel() { + events.push('cancel'); + } + }); + + const reader = rs.getReader(); + reader.read().catch(() => {}); // No await. + await delay(0); + await Promise.all([reader.cancel(), reader.closed]); + + assert_array_equals(events, ['pull', 'cancel'], 'cancel should have been called'); + +}, 'ReadableStream cancellation: underlyingSource.cancel() should called, even with pending pull'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/constructor.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/constructor.any.js new file mode 100644 index 000000000000..608dc48cfa39 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/constructor.any.js @@ -0,0 +1,17 @@ +// META: global=window,worker +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +const error2 = new Error('error2'); +error2.name = 'error2'; + +test(() => { + const underlyingSource = { get start() { throw error1; } }; + const queuingStrategy = { highWaterMark: 0, get size() { throw error2; } }; + + // underlyingSource is converted in prose in the method body, whereas queuingStrategy is done at the IDL layer. + // So the queuingStrategy exception should be encountered first. + assert_throws_exactly(error2, () => new ReadableStream(underlyingSource, queuingStrategy)); +}, 'underlyingSource argument should be converted after queuingStrategy argument'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/count-queuing-strategy-integration.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/count-queuing-strategy-integration.any.js new file mode 100644 index 000000000000..02ac5bae5c2f --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/count-queuing-strategy-integration.any.js @@ -0,0 +1,208 @@ +// META: global=window,worker +'use strict'; + +test(() => { + + new ReadableStream({}, new CountQueuingStrategy({ highWaterMark: 4 })); + +}, 'Can construct a readable stream with a valid CountQueuingStrategy'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + new CountQueuingStrategy({ highWaterMark: 0 }) + ); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 0, '0 reads, 0 enqueues: desiredSize should be 0'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, -1, '0 reads, 1 enqueue: desiredSize should be -1'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, -2, '0 reads, 2 enqueues: desiredSize should be -2'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, -3, '0 reads, 3 enqueues: desiredSize should be -3'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, -4, '0 reads, 4 enqueues: desiredSize should be -4'); + + return reader.read() + .then(result => { + assert_object_equals(result, { value: 'a', done: false }, + '1st read gives back the 1st chunk enqueued (queue now contains 3 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'b', done: false }, + '2nd read gives back the 2nd chunk enqueued (queue now contains 2 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'c', done: false }, + '3rd read gives back the 3rd chunk enqueued (queue now contains 1 chunk)'); + + assert_equals(controller.desiredSize, -1, '3 reads, 4 enqueues: desiredSize should be -1'); + controller.enqueue('e'); + assert_equals(controller.desiredSize, -2, '3 reads, 5 enqueues: desiredSize should be -2'); + + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'd', done: false }, + '4th read gives back the 4th chunk enqueued (queue now contains 1 chunks)'); + return reader.read(); + + }).then(result => { + assert_object_equals(result, { value: 'e', done: false }, + '5th read gives back the 5th chunk enqueued (queue now contains 0 chunks)'); + + assert_equals(controller.desiredSize, 0, '5 reads, 5 enqueues: desiredSize should be 0'); + controller.enqueue('f'); + assert_equals(controller.desiredSize, -1, '5 reads, 6 enqueues: desiredSize should be -1'); + controller.enqueue('g'); + assert_equals(controller.desiredSize, -2, '5 reads, 7 enqueues: desiredSize should be -2'); + }); + +}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 0)'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + new CountQueuingStrategy({ highWaterMark: 1 }) + ); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 1, '0 reads, 0 enqueues: desiredSize should be 1'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, 0, '0 reads, 1 enqueue: desiredSize should be 0'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, -1, '0 reads, 2 enqueues: desiredSize should be -1'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, -2, '0 reads, 3 enqueues: desiredSize should be -2'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, -3, '0 reads, 4 enqueues: desiredSize should be -3'); + + return reader.read() + .then(result => { + assert_object_equals(result, { value: 'a', done: false }, + '1st read gives back the 1st chunk enqueued (queue now contains 3 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'b', done: false }, + '2nd read gives back the 2nd chunk enqueued (queue now contains 2 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'c', done: false }, + '3rd read gives back the 3rd chunk enqueued (queue now contains 1 chunk)'); + + assert_equals(controller.desiredSize, 0, '3 reads, 4 enqueues: desiredSize should be 0'); + controller.enqueue('e'); + assert_equals(controller.desiredSize, -1, '3 reads, 5 enqueues: desiredSize should be -1'); + + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'd', done: false }, + '4th read gives back the 4th chunk enqueued (queue now contains 1 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'e', done: false }, + '5th read gives back the 5th chunk enqueued (queue now contains 0 chunks)'); + + assert_equals(controller.desiredSize, 1, '5 reads, 5 enqueues: desiredSize should be 1'); + controller.enqueue('f'); + assert_equals(controller.desiredSize, 0, '5 reads, 6 enqueues: desiredSize should be 0'); + controller.enqueue('g'); + assert_equals(controller.desiredSize, -1, '5 reads, 7 enqueues: desiredSize should be -1'); + }); + +}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 1)'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream( + { + start(c) { + controller = c; + } + }, + new CountQueuingStrategy({ highWaterMark: 4 }) + ); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 4, '0 reads, 0 enqueues: desiredSize should be 4'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, 3, '0 reads, 1 enqueue: desiredSize should be 3'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, 2, '0 reads, 2 enqueues: desiredSize should be 2'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, 1, '0 reads, 3 enqueues: desiredSize should be 1'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, 0, '0 reads, 4 enqueues: desiredSize should be 0'); + controller.enqueue('e'); + assert_equals(controller.desiredSize, -1, '0 reads, 5 enqueues: desiredSize should be -1'); + controller.enqueue('f'); + assert_equals(controller.desiredSize, -2, '0 reads, 6 enqueues: desiredSize should be -2'); + + + return reader.read() + .then(result => { + assert_object_equals(result, { value: 'a', done: false }, + '1st read gives back the 1st chunk enqueued (queue now contains 5 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'b', done: false }, + '2nd read gives back the 2nd chunk enqueued (queue now contains 4 chunks)'); + + assert_equals(controller.desiredSize, 0, '2 reads, 6 enqueues: desiredSize should be 0'); + controller.enqueue('g'); + assert_equals(controller.desiredSize, -1, '2 reads, 7 enqueues: desiredSize should be -1'); + + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'c', done: false }, + '3rd read gives back the 3rd chunk enqueued (queue now contains 4 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'd', done: false }, + '4th read gives back the 4th chunk enqueued (queue now contains 3 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'e', done: false }, + '5th read gives back the 5th chunk enqueued (queue now contains 2 chunks)'); + return reader.read(); + }) + .then(result => { + assert_object_equals(result, { value: 'f', done: false }, + '6th read gives back the 6th chunk enqueued (queue now contains 0 chunks)'); + + assert_equals(controller.desiredSize, 3, '6 reads, 7 enqueues: desiredSize should be 3'); + controller.enqueue('h'); + assert_equals(controller.desiredSize, 2, '6 reads, 8 enqueues: desiredSize should be 2'); + controller.enqueue('i'); + assert_equals(controller.desiredSize, 1, '6 reads, 9 enqueues: desiredSize should be 1'); + controller.enqueue('j'); + assert_equals(controller.desiredSize, 0, '6 reads, 10 enqueues: desiredSize should be 0'); + controller.enqueue('k'); + assert_equals(controller.desiredSize, -1, '6 reads, 11 enqueues: desiredSize should be -1'); + }); + +}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 4)'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/crashtests/garbage-collection.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/crashtests/garbage-collection.any.js new file mode 100644 index 000000000000..6e9d80c41425 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/crashtests/garbage-collection.any.js @@ -0,0 +1,38 @@ +// META: global=window,worker +// META: script=/common/gc.js +'use strict'; + +// See https://crbug.com/335506658 for details. +promise_test(async () => { + const closed = new ReadableStream({ + pull(controller) { + controller.enqueue('is there anybody in there?'); + } + }).getReader().closed; + // 3 GCs are actually required to trigger the bug at time of writing. + for (let i = 0; i < 5; ++i) + await garbageCollect(); +}, 'Garbage-collecting a stream along with its reader should not crash'); + +promise_test(async () => { + let reader = new ReadableStream({ + pull() { } + }).getReader(); + const promise = reader.read(); + reader = null; + for (let i = 0; i < 5; ++i) + await garbageCollect(); +}, 'Garbage-collecting a stream with a pending read should not crash'); + +promise_test(async () => { + let reader = new ReadableStream({ + type: "bytes", + pull() { return new Promise(resolve => {}); } + }).getReader({mode: "byob"}); + const promise = reader.read(new Uint8Array(42)); + reader = null; + for (let i = 0; i < 5; ++i) + await garbageCollect(); +}, 'Garbage-collecting a stream with a pending BYOB read should not crash'); + + diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/default-reader.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/default-reader.any.js new file mode 100644 index 000000000000..59d7ab2f74db --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/default-reader.any.js @@ -0,0 +1,539 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +'use strict'; + +test(() => { + + assert_throws_js(TypeError, () => new ReadableStreamDefaultReader('potato')); + assert_throws_js(TypeError, () => new ReadableStreamDefaultReader({})); + assert_throws_js(TypeError, () => new ReadableStreamDefaultReader()); + +}, 'ReadableStreamDefaultReader constructor should get a ReadableStream object as argument'); + +test(() => { + + const rsReader = new ReadableStreamDefaultReader(new ReadableStream()); + assert_equals(rsReader.closed, rsReader.closed, 'closed should return the same promise'); + +}, 'ReadableStreamDefaultReader closed should always return the same promise object'); + +test(() => { + + const rs = new ReadableStream(); + new ReadableStreamDefaultReader(rs); // Constructing directly the first time should be fine. + assert_throws_js(TypeError, () => new ReadableStreamDefaultReader(rs), + 'constructing directly the second time should fail'); + +}, 'Constructing a ReadableStreamDefaultReader directly should fail if the stream is already locked (via direct ' + + 'construction)'); + +test(() => { + + const rs = new ReadableStream(); + new ReadableStreamDefaultReader(rs); // Constructing directly should be fine. + assert_throws_js(TypeError, () => rs.getReader(), 'getReader() should fail'); + +}, 'Getting a ReadableStreamDefaultReader via getReader should fail if the stream is already locked (via direct ' + + 'construction)'); + +test(() => { + + const rs = new ReadableStream(); + rs.getReader(); // getReader() should be fine. + assert_throws_js(TypeError, () => new ReadableStreamDefaultReader(rs), 'constructing directly should fail'); + +}, 'Constructing a ReadableStreamDefaultReader directly should fail if the stream is already locked (via getReader)'); + +test(() => { + + const rs = new ReadableStream(); + rs.getReader(); // getReader() should be fine. + assert_throws_js(TypeError, () => rs.getReader(), 'getReader() should fail'); + +}, 'Getting a ReadableStreamDefaultReader via getReader should fail if the stream is already locked (via getReader)'); + +test(() => { + + const rs = new ReadableStream({ + start(c) { + c.close(); + } + }); + + new ReadableStreamDefaultReader(rs); // Constructing directly should not throw. + +}, 'Constructing a ReadableStreamDefaultReader directly should be OK if the stream is closed'); + +test(() => { + + const theError = new Error('don\'t say i didn\'t warn ya'); + const rs = new ReadableStream({ + start(c) { + c.error(theError); + } + }); + + new ReadableStreamDefaultReader(rs); // Constructing directly should not throw. + +}, 'Constructing a ReadableStreamDefaultReader directly should be OK if the stream is errored'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + const reader = rs.getReader(); + + const promise = reader.read().then(result => { + assert_object_equals(result, { value: 'a', done: false }, 'read() should fulfill with the enqueued chunk'); + }); + + controller.enqueue('a'); + return promise; + +}, 'Reading from a reader for an empty stream will wait until a chunk is available'); + +promise_test(() => { + + let cancelCalled = false; + const passedReason = new Error('it wasn\'t the right time, sorry'); + const rs = new ReadableStream({ + cancel(reason) { + assert_true(rs.locked, 'the stream should still be locked'); + assert_throws_js(TypeError, () => rs.getReader(), 'should not be able to get another reader'); + assert_equals(reason, passedReason, 'the cancellation reason is passed through to the underlying source'); + cancelCalled = true; + } + }); + + const reader = rs.getReader(); + return reader.cancel(passedReason).then(() => assert_true(cancelCalled)); + +}, 'cancel() on a reader does not release the reader'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader = rs.getReader(); + const promise = reader.closed; + + controller.close(); + return promise; + +}, 'closed should be fulfilled after stream is closed (.closed access before acquiring)'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader1 = rs.getReader(); + + reader1.releaseLock(); + + const reader2 = rs.getReader(); + controller.close(); + + return Promise.all([ + promise_rejects_js(t, TypeError, reader1.closed), + reader2.closed + ]); + +}, 'closed should be rejected after reader releases its lock (multiple stream locks)'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader = rs.getReader(); + const promise1 = reader.closed; + + controller.close(); + + reader.releaseLock(); + const promise2 = reader.closed; + + assert_not_equals(promise1, promise2, '.closed should be replaced'); + return Promise.all([ + promise1, + promise_rejects_js(t, TypeError, promise2, '.closed after releasing lock'), + ]); + +}, 'closed is replaced when stream closes and reader releases its lock'); + +promise_test(t => { + + const theError = { name: 'unique error' }; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader = rs.getReader(); + const promise1 = reader.closed; + + controller.error(theError); + + reader.releaseLock(); + const promise2 = reader.closed; + + assert_not_equals(promise1, promise2, '.closed should be replaced'); + return Promise.all([ + promise_rejects_exactly(t, theError, promise1, '.closed before releasing lock'), + promise_rejects_js(t, TypeError, promise2, '.closed after releasing lock') + ]); + +}, 'closed is replaced when stream errors and reader releases its lock'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const reader1 = rs.getReader(); + const promise1 = reader1.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'reading the first chunk from reader1 works'); + }); + reader1.releaseLock(); + + const reader2 = rs.getReader(); + const promise2 = reader2.read().then(r => { + assert_object_equals(r, { value: 'b', done: false }, 'reading the second chunk from reader2 works'); + }); + reader2.releaseLock(); + + return Promise.all([promise1, promise2]); + +}, 'Multiple readers can access the stream in sequence'); + +promise_test(() => { + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + } + }); + + const reader1 = rs.getReader(); + reader1.releaseLock(); + + const reader2 = rs.getReader(); + + // Should be a no-op + reader1.releaseLock(); + + return reader2.read().then(result => { + assert_object_equals(result, { value: 'a', done: false }, + 'read() should still work on reader2 even after reader1 is released'); + }); + +}, 'Cannot use an already-released reader to unlock a stream again'); + +promise_test(t => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + }, + cancel() { + assert_unreached('underlying source cancel should not be called'); + } + }); + + const reader = rs.getReader(); + reader.releaseLock(); + const cancelPromise = reader.cancel(); + + const reader2 = rs.getReader(); + const readPromise = reader2.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'a new reader should be able to read a chunk'); + }); + + return Promise.all([ + promise_rejects_js(t, TypeError, cancelPromise), + readPromise + ]); + +}, 'cancel() on a released reader is a no-op and does not pass through'); + +promise_test(t => { + + const promiseAsserts = []; + + let controller; + const theError = { name: 'unique error' }; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader1 = rs.getReader(); + + promiseAsserts.push( + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader1.read()) + ); + + assert_throws_js(TypeError, () => rs.getReader(), 'trying to get another reader before erroring should throw'); + + controller.error(theError); + + reader1.releaseLock(); + + const reader2 = rs.getReader(); + + promiseAsserts.push( + promise_rejects_exactly(t, theError, reader2.closed), + promise_rejects_exactly(t, theError, reader2.read()) + ); + + return Promise.all(promiseAsserts); + +}, 'Getting a second reader after erroring the stream and releasing the reader should succeed'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const promise = rs.getReader().closed.then( + t.unreached_func('closed promise should not be fulfilled when stream is errored'), + err => { + assert_equals(err, undefined, 'passed error should be undefined as it was'); + } + ); + + controller.error(); + return promise; + +}, 'ReadableStreamDefaultReader closed promise should be rejected with undefined if that is the error'); + + +promise_test(t => { + + const rs = new ReadableStream({ + start() { + return Promise.reject(); + } + }); + + return rs.getReader().read().then( + t.unreached_func('read promise should not be fulfilled when stream is errored'), + err => { + assert_equals(err, undefined, 'passed error should be undefined as it was'); + } + ); + +}, 'ReadableStreamDefaultReader: if start rejects with no parameter, it should error the stream with an undefined ' + + 'error'); + +promise_test(t => { + + const theError = { name: 'unique string' }; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const promise = promise_rejects_exactly(t, theError, rs.getReader().closed); + + controller.error(theError); + return promise; + +}, 'Erroring a ReadableStream after checking closed should reject ReadableStreamDefaultReader closed promise'); + +promise_test(t => { + + const theError = { name: 'unique string' }; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + controller.error(theError); + + // Let's call getReader twice for extra test coverage of this code path. + rs.getReader().releaseLock(); + + return promise_rejects_exactly(t, theError, rs.getReader().closed); + +}, 'Erroring a ReadableStream before checking closed should reject ReadableStreamDefaultReader closed promise'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + const reader = rs.getReader(); + + const promise = Promise.all([ + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (1)'); + }), + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (2)'); + }), + reader.closed + ]); + + controller.close(); + return promise; + +}, 'Reading twice on a stream that gets closed'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + controller.close(); + const reader = rs.getReader(); + + return Promise.all([ + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (1)'); + }), + reader.read().then(result => { + assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (2)'); + }), + reader.closed + ]); + +}, 'Reading twice on a closed stream'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const myError = { name: 'mashed potatoes' }; + controller.error(myError); + + const reader = rs.getReader(); + + return Promise.all([ + promise_rejects_exactly(t, myError, reader.read()), + promise_rejects_exactly(t, myError, reader.read()), + promise_rejects_exactly(t, myError, reader.closed) + ]); + +}, 'Reading twice on an errored stream'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const myError = { name: 'mashed potatoes' }; + const reader = rs.getReader(); + + const promise = Promise.all([ + promise_rejects_exactly(t, myError, reader.read()), + promise_rejects_exactly(t, myError, reader.read()), + promise_rejects_exactly(t, myError, reader.closed) + ]); + + controller.error(myError); + return promise; + +}, 'Reading twice on a stream that gets errored'); + +test(() => { + const rs = new ReadableStream(); + let toStringCalled = false; + const mode = { + toString() { + toStringCalled = true; + return ''; + } + }; + assert_throws_js(TypeError, () => rs.getReader({ mode }), 'getReader() should throw'); + assert_true(toStringCalled, 'toString() should be called'); +}, 'getReader() should call ToString() on mode'); + +promise_test(() => { + const rs = new ReadableStream({ + pull(controller) { + controller.close(); + } + }); + + const reader = rs.getReader(); + return reader.read().then(() => { + // The test passes if releaseLock() does not throw. + reader.releaseLock(); + }); +}, 'controller.close() should clear the list of pending read requests'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const reader1 = rs.getReader(); + const promise1 = promise_rejects_js(t, TypeError, reader1.read(), 'read() from reader1 should reject when reader1 is released'); + reader1.releaseLock(); + + controller.enqueue('a'); + + const reader2 = rs.getReader(); + const promise2 = reader2.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'read() from reader2 should resolve with enqueued chunk'); + }) + reader2.releaseLock(); + + return Promise.all([promise1, promise2]); + +}, 'Second reader can read chunks after first reader was released with pending read requests'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/floating-point-total-queue-size.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/floating-point-total-queue-size.any.js new file mode 100644 index 000000000000..50cca3d951a9 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/floating-point-total-queue-size.any.js @@ -0,0 +1,116 @@ +// META: global=window,worker +'use strict'; + +// Due to the limitations of floating-point precision, the calculation of desiredSize sometimes gives different answers +// than adding up the items in the queue would. It is important that implementations give the same result in these edge +// cases so that developers do not come to depend on non-standard behaviour. See +// https://github.com/whatwg/streams/issues/582 and linked issues for further discussion. + +promise_test(() => { + const { reader, controller } = setupTestStream(); + + controller.enqueue(2); + assert_equals(controller.desiredSize, 0 - 2, 'desiredSize must be -2 after enqueueing such a chunk'); + + controller.enqueue(Number.MAX_SAFE_INTEGER); + assert_equals(controller.desiredSize, 0 - Number.MAX_SAFE_INTEGER - 2, + 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)'); + + return reader.read().then(() => { + assert_equals(controller.desiredSize, 0 - Number.MAX_SAFE_INTEGER - 2 + 2, + 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)'); + + return reader.read(); + }).then(() => { + assert_equals(controller.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative'); + }); +}, 'Floating point arithmetic must manifest near NUMBER.MAX_SAFE_INTEGER (total ends up positive)'); + +promise_test(() => { + const { reader, controller } = setupTestStream(); + + controller.enqueue(1e-16); + assert_equals(controller.desiredSize, 0 - 1e-16, 'desiredSize must be -1e16 after enqueueing such a chunk'); + + controller.enqueue(1); + assert_equals(controller.desiredSize, 0 - 1e-16 - 1, + 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)'); + + return reader.read().then(() => { + assert_equals(controller.desiredSize, 0 - 1e-16 - 1 + 1e-16, + 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)'); + + return reader.read(); + }).then(() => { + assert_equals(controller.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative'); + }); +}, 'Floating point arithmetic must manifest near 0 (total ends up positive, but clamped)'); + +promise_test(() => { + const { reader, controller } = setupTestStream(); + + controller.enqueue(1e-16); + assert_equals(controller.desiredSize, 0 - 1e-16, 'desiredSize must be -2e16 after enqueueing such a chunk'); + + controller.enqueue(1); + assert_equals(controller.desiredSize, 0 - 1e-16 - 1, + 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)'); + + controller.enqueue(2e-16); + assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16, + 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a third chunk)'); + + return reader.read().then(() => { + assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16, + 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)'); + + return reader.read(); + }).then(() => { + assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1, + 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a second chunk)'); + + return reader.read(); + }).then(() => { + assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1 + 2e-16, + 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a third chunk)'); + }); +}, 'Floating point arithmetic must manifest near 0 (total ends up positive, and not clamped)'); + +promise_test(() => { + const { reader, controller } = setupTestStream(); + + controller.enqueue(2e-16); + assert_equals(controller.desiredSize, 0 - 2e-16, 'desiredSize must be -2e16 after enqueueing such a chunk'); + + controller.enqueue(1); + assert_equals(controller.desiredSize, 0 - 2e-16 - 1, + 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)'); + + return reader.read().then(() => { + assert_equals(controller.desiredSize, 0 - 2e-16 - 1 + 2e-16, + 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)'); + + return reader.read(); + }).then(() => { + assert_equals(controller.desiredSize, 0, + 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a second chunk)'); + }); +}, 'Floating point arithmetic must manifest near 0 (total ends up zero)'); + +function setupTestStream() { + const strategy = { + size(x) { + return x; + }, + highWaterMark: 0 + }; + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, strategy); + + return { reader: rs.getReader(), controller }; +} diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/from.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/from.any.js new file mode 100644 index 000000000000..b38d54b9a062 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/from.any.js @@ -0,0 +1,669 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +'use strict'; + +const iterableFactories = [ + ['an array of values', () => { + return ['a', 'b']; + }], + + ['an array of promises', () => { + return [ + Promise.resolve('a'), + Promise.resolve('b') + ]; + }], + + ['an array iterator', () => { + return ['a', 'b'][Symbol.iterator](); + }], + + ['a string', () => { + // This iterates over the code points of the string. + return 'ab'; + }], + + ['a Set', () => { + return new Set(['a', 'b']); + }], + + ['a Set iterator', () => { + return new Set(['a', 'b'])[Symbol.iterator](); + }], + + ['a sync generator', () => { + function* syncGenerator() { + yield 'a'; + yield 'b'; + } + + return syncGenerator(); + }], + + ['an async generator', () => { + async function* asyncGenerator() { + yield 'a'; + yield 'b'; + } + + return asyncGenerator(); + }], + + ['a sync iterable of values', () => { + const chunks = ['a', 'b']; + const iterator = { + next() { + return { + done: chunks.length === 0, + value: chunks.shift() + }; + } + }; + const iterable = { + [Symbol.iterator]: () => iterator + }; + return iterable; + }], + + ['a sync iterable of promises', () => { + const chunks = ['a', 'b']; + const iterator = { + next() { + return chunks.length === 0 ? { done: true } : { + done: false, + value: Promise.resolve(chunks.shift()) + }; + } + }; + const iterable = { + [Symbol.iterator]: () => iterator + }; + return iterable; + }], + + ['a sync iterable with a function iterator', () => { + const chunks = ['a', 'b']; + function functionIterator() {} + functionIterator.next = () => ({ + done: chunks.length === 0, + value: chunks.shift() + }); + const iterable = { + [Symbol.iterator]: () => functionIterator + }; + return iterable; + }], + + ['an async iterable', () => { + const chunks = ['a', 'b']; + const asyncIterator = { + next() { + return Promise.resolve({ + done: chunks.length === 0, + value: chunks.shift() + }) + } + }; + const asyncIterable = { + [Symbol.asyncIterator]: () => asyncIterator + }; + return asyncIterable; + }], + + ['an async iterable with a function iterator', () => { + const chunks = ['a', 'b']; + function functionAsyncIterator() {} + functionAsyncIterator.next = () => Promise.resolve({ + done: chunks.length === 0, + value: chunks.shift() + }); + const asyncIterable = { + [Symbol.asyncIterator]: () => functionAsyncIterator + }; + return asyncIterable; + }], + + ['a ReadableStream', () => { + return new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + }], + + ['a ReadableStream async iterator', () => { + return new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + })[Symbol.asyncIterator](); + }] +]; + +for (const [label, factory] of iterableFactories) { + promise_test(async () => { + + const iterable = factory(); + const rs = ReadableStream.from(iterable); + assert_equals(rs.constructor, ReadableStream, 'from() should return a ReadableStream'); + + const reader = rs.getReader(); + assert_object_equals(await reader.read(), { value: 'a', done: false }, 'first read should be correct'); + assert_object_equals(await reader.read(), { value: 'b', done: false }, 'second read should be correct'); + assert_object_equals(await reader.read(), { value: undefined, done: true }, 'third read should be done'); + await reader.closed; + + }, `ReadableStream.from accepts ${label}`); +} + +const badIterables = [ + ['null', null], + ['undefined', undefined], + ['0', 0], + ['NaN', NaN], + ['true', true], + ['{}', {}], + ['Object.create(null)', Object.create(null)], + ['a function', () => 42], + ['a symbol', Symbol()], + ['an object with a non-callable @@iterator method', { + [Symbol.iterator]: 42 + }], + ['an object with a non-callable @@asyncIterator method', { + [Symbol.asyncIterator]: 42 + }], + ['an object with an @@iterator method returning a non-object', { + [Symbol.iterator]: () => 42 + }], + ['an object with an @@asyncIterator method returning a non-object', { + [Symbol.asyncIterator]: () => 42 + }], +]; + +for (const [label, iterable] of badIterables) { + test(() => { + assert_throws_js(TypeError, () => ReadableStream.from(iterable), 'from() should throw a TypeError') + }, `ReadableStream.from throws on invalid iterables; specifically ${label}`); +} + +test(() => { + const theError = new Error('a unique string'); + const iterable = { + [Symbol.iterator]() { + throw theError; + } + }; + + assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error'); +}, `ReadableStream.from re-throws errors from calling the @@iterator method`); + +test(() => { + const theError = new Error('a unique string'); + const iterable = { + [Symbol.asyncIterator]() { + throw theError; + } + }; + + assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error'); +}, `ReadableStream.from re-throws errors from calling the @@asyncIterator method`); + +test(t => { + const theError = new Error('a unique string'); + const iterable = { + [Symbol.iterator]: t.unreached_func('@@iterator should not be called'), + [Symbol.asyncIterator]() { + throw theError; + } + }; + + assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error'); +}, `ReadableStream.from ignores @@iterator if @@asyncIterator exists`); + +test(() => { + const theError = new Error('a unique string'); + const iterable = { + [Symbol.asyncIterator]: null, + [Symbol.iterator]() { + throw theError + } + }; + + assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error'); +}, `ReadableStream.from ignores a null @@asyncIterator`); + +promise_test(async () => { + + const iterable = { + async next() { + return { value: undefined, done: true }; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + const read = await reader.read(); + assert_object_equals(read, { value: undefined, done: true }, 'first read should be done'); + + await reader.closed; + +}, `ReadableStream.from accepts an empty iterable`); + +promise_test(async t => { + + const theError = new Error('a unique string'); + + const iterable = { + async next() { + throw theError; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await Promise.all([ + promise_rejects_exactly(t, theError, reader.read()), + promise_rejects_exactly(t, theError, reader.closed) + ]); + +}, `ReadableStream.from: stream errors when next() rejects`); + +promise_test(async t => { + const theError = new Error('a unique string'); + + const iterable = { + next() { + throw theError; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await Promise.all([ + promise_rejects_exactly(t, theError, reader.read()), + promise_rejects_exactly(t, theError, reader.closed) + ]); + +}, 'ReadableStream.from: stream errors when next() throws synchronously'); + +promise_test(async t => { + + const iterable = { + next() { + return 42; // not a promise or an iterator result + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await Promise.all([ + promise_rejects_js(t, TypeError, reader.read()), + promise_rejects_js(t, TypeError, reader.closed) + ]); + +}, 'ReadableStream.from: stream errors when next() returns a non-object'); + +promise_test(async t => { + + const iterable = { + next() { + return Promise.resolve(42); // not an iterator result + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await Promise.all([ + promise_rejects_js(t, TypeError, reader.read()), + promise_rejects_js(t, TypeError, reader.closed) + ]); + +}, 'ReadableStream.from: stream errors when next() fulfills with a non-object'); + +promise_test(async t => { + + const iterable = { + next() { + return new Promise(() => {}); + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await Promise.race([ + reader.read().then(t.unreached_func('read() should not resolve'), t.unreached_func('read() should not reject')), + reader.closed.then(t.unreached_func('closed should not resolve'), t.unreached_func('closed should not reject')), + flushAsyncEvents() + ]); + +}, 'ReadableStream.from: stream stalls when next() never settles'); + +promise_test(async () => { + + let nextCalls = 0; + let nextArgs; + const iterable = { + async next(...args) { + nextCalls += 1; + nextArgs = args; + return { value: 'a', done: false }; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await flushAsyncEvents(); + assert_equals(nextCalls, 0, 'next() should not be called yet'); + + const read = await reader.read(); + assert_object_equals(read, { value: 'a', done: false }, 'first read should be correct'); + assert_equals(nextCalls, 1, 'next() should be called after first read()'); + assert_array_equals(nextArgs, [], 'next() should be called with no arguments'); + +}, `ReadableStream.from: calls next() after first read()`); + +promise_test(async t => { + + const theError = new Error('a unique string'); + + let returnCalls = 0; + let returnArgs; + let resolveReturn; + const iterable = { + next: t.unreached_func('next() should not be called'), + throw: t.unreached_func('throw() should not be called'), + async return(...args) { + returnCalls += 1; + returnArgs = args; + await new Promise(r => resolveReturn = r); + return { done: true }; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + assert_equals(returnCalls, 0, 'return() should not be called yet'); + + let cancelResolved = false; + const cancelPromise = reader.cancel(theError).then(() => { + cancelResolved = true; + }); + + await flushAsyncEvents(); + assert_equals(returnCalls, 1, 'return() should be called'); + assert_array_equals(returnArgs, [theError], 'return() should be called with cancel reason'); + assert_false(cancelResolved, 'cancel() should not resolve while promise from return() is pending'); + + resolveReturn(); + await Promise.all([ + cancelPromise, + reader.closed + ]); + +}, `ReadableStream.from: cancelling the returned stream calls and awaits return()`); + +promise_test(async t => { + + let nextCalls = 0; + let returnCalls = 0; + + const iterable = { + async next() { + nextCalls += 1; + return { value: undefined, done: true }; + }, + throw: t.unreached_func('throw() should not be called'), + async return() { + returnCalls += 1; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + const read = await reader.read(); + assert_object_equals(read, { value: undefined, done: true }, 'first read should be done'); + assert_equals(nextCalls, 1, 'next() should be called once'); + + await reader.closed; + assert_equals(returnCalls, 0, 'return() should not be called'); + +}, `ReadableStream.from: return() is not called when iterator completes normally`); + +promise_test(async t => { + + const theError = new Error('a unique string'); + + const iterable = { + next: t.unreached_func('next() should not be called'), + throw: t.unreached_func('throw() should not be called'), + // no return method + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await Promise.all([ + reader.cancel(theError), + reader.closed + ]); + +}, `ReadableStream.from: cancel() resolves when return() method is missing`); + +promise_test(async t => { + + const theError = new Error('a unique string'); + + const iterable = { + next: t.unreached_func('next() should not be called'), + throw: t.unreached_func('throw() should not be called'), + return: 42, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await promise_rejects_js(t, TypeError, reader.cancel(theError), 'cancel() should reject with a TypeError'); + + await reader.closed; + +}, `ReadableStream.from: cancel() rejects when return() is not a method`); + +promise_test(async t => { + + const cancelReason = new Error('cancel reason'); + const rejectError = new Error('reject error'); + + const iterable = { + next: t.unreached_func('next() should not be called'), + throw: t.unreached_func('throw() should not be called'), + async return() { + throw rejectError; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await promise_rejects_exactly(t, rejectError, reader.cancel(cancelReason), 'cancel() should reject with error from return()'); + + await reader.closed; + +}, `ReadableStream.from: cancel() rejects when return() rejects`); + +promise_test(async t => { + + const cancelReason = new Error('cancel reason'); + const rejectError = new Error('reject error'); + + const iterable = { + next: t.unreached_func('next() should not be called'), + throw: t.unreached_func('throw() should not be called'), + return() { + throw rejectError; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await promise_rejects_exactly(t, rejectError, reader.cancel(cancelReason), 'cancel() should reject with error from return()'); + + await reader.closed; + +}, `ReadableStream.from: cancel() rejects when return() throws synchronously`); + +promise_test(async t => { + + const theError = new Error('a unique string'); + + const iterable = { + next: t.unreached_func('next() should not be called'), + throw: t.unreached_func('throw() should not be called'), + async return() { + return 42; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + const reader = rs.getReader(); + + await promise_rejects_js(t, TypeError, reader.cancel(theError), 'cancel() should reject with a TypeError'); + + await reader.closed; + +}, `ReadableStream.from: cancel() rejects when return() fulfills with a non-object`); + +promise_test(async () => { + + let nextCalls = 0; + let reader; + let values = ['a', 'b', 'c']; + + const iterable = { + async next() { + nextCalls += 1; + if (nextCalls === 1) { + reader.read(); + } + return { value: values.shift(), done: false }; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + reader = rs.getReader(); + + const read1 = await reader.read(); + assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct'); + await flushAsyncEvents(); + assert_equals(nextCalls, 2, 'next() should be called two times'); + + const read2 = await reader.read(); + assert_object_equals(read2, { value: 'c', done: false }, 'second read should be correct'); + assert_equals(nextCalls, 3, 'next() should be called three times'); + +}, `ReadableStream.from: reader.read() inside next()`); + +promise_test(async () => { + + let nextCalls = 0; + let returnCalls = 0; + let reader; + + const iterable = { + async next() { + nextCalls++; + await reader.cancel(); + assert_equals(returnCalls, 1, 'return() should be called once'); + return { value: 'something else', done: false }; + }, + async return() { + returnCalls++; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + reader = rs.getReader(); + + const read = await reader.read(); + assert_object_equals(read, { value: undefined, done: true }, 'first read should be done'); + assert_equals(nextCalls, 1, 'next() should be called once'); + + await reader.closed; + +}, `ReadableStream.from: reader.cancel() inside next()`); + +promise_test(async t => { + + let returnCalls = 0; + let reader; + + const iterable = { + next: t.unreached_func('next() should not be called'), + async return() { + returnCalls++; + await reader.cancel(); + return { done: true }; + }, + [Symbol.asyncIterator]: () => iterable + }; + + const rs = ReadableStream.from(iterable); + reader = rs.getReader(); + + await reader.cancel(); + assert_equals(returnCalls, 1, 'return() should be called once'); + + await reader.closed; + +}, `ReadableStream.from: reader.cancel() inside return()`); + +promise_test(async t => { + + let array = ['a', 'b']; + + const rs = ReadableStream.from(array); + const reader = rs.getReader(); + + const read1 = await reader.read(); + assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct'); + const read2 = await reader.read(); + assert_object_equals(read2, { value: 'b', done: false }, 'second read should be correct'); + + array.push('c'); + + const read3 = await reader.read(); + assert_object_equals(read3, { value: 'c', done: false }, 'third read after push() should be correct'); + const read4 = await reader.read(); + assert_object_equals(read4, { value: undefined, done: true }, 'fourth read should be done'); + + await reader.closed; + +}, `ReadableStream.from(array), push() to array while reading`); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/garbage-collection.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/garbage-collection.any.js new file mode 100644 index 000000000000..907eb6006822 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/garbage-collection.any.js @@ -0,0 +1,90 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=/common/gc.js +'use strict'; + +promise_test(async () => { + + let controller; + new ReadableStream({ + start(c) { + controller = c; + } + }); + + await garbageCollect(); + + return delay(50).then(() => { + controller.close(); + assert_throws_js(TypeError, () => controller.close(), 'close should throw a TypeError the second time'); + controller.error(); + }); + +}, 'ReadableStreamController methods should continue working properly when scripts lose their reference to the ' + + 'readable stream'); + +promise_test(async () => { + + let controller; + + const closedPromise = new ReadableStream({ + start(c) { + controller = c; + } + }).getReader().closed; + + await garbageCollect(); + + return delay(50).then(() => controller.close()).then(() => closedPromise); + +}, 'ReadableStream closed promise should fulfill even if the stream and reader JS references are lost'); + +promise_test(async t => { + + const theError = new Error('boo'); + let controller; + + const closedPromise = new ReadableStream({ + start(c) { + controller = c; + } + }).getReader().closed; + + await garbageCollect(); + + return delay(50).then(() => controller.error(theError)) + .then(() => promise_rejects_exactly(t, theError, closedPromise)); + +}, 'ReadableStream closed promise should reject even if stream and reader JS references are lost'); + +promise_test(async () => { + + const rs = new ReadableStream({}); + + rs.getReader(); + + await garbageCollect(); + + return delay(50).then(() => assert_throws_js(TypeError, () => rs.getReader(), + 'old reader should still be locking the stream even after garbage collection')); + +}, 'Garbage-collecting a ReadableStreamDefaultReader should not unlock its stream'); + +promise_test(async () => { + + const promise = (() => { + const rs = new ReadableStream({ + pull(controller) { + controller.enqueue('words'); + } + }); + const reader = rs.getReader(); + return reader.read(); + })(); + await garbageCollect(); + const {value, done} = await promise; + // If we get here, the test passed. + assert_equals(value, 'words', 'value should be words'); + assert_false(done, 'we should not be done'); + +}, 'A ReadableStream and its reader should not be garbage collected while there is a read promise pending'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/general.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/general.any.js new file mode 100644 index 000000000000..2a32b27943c8 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/general.any.js @@ -0,0 +1,840 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +test(() => { + + new ReadableStream(); // ReadableStream constructed with no parameters + new ReadableStream({ }); // ReadableStream constructed with an empty object as parameter + new ReadableStream({ type: undefined }); // ReadableStream constructed with undefined type + new ReadableStream(undefined); // ReadableStream constructed with undefined as parameter + + let x; + new ReadableStream(x); // ReadableStream constructed with an undefined variable as parameter + +}, 'ReadableStream can be constructed with no errors'); + +test(() => { + + assert_throws_js(TypeError, () => new ReadableStream(null), 'constructor should throw when the source is null'); + +}, 'ReadableStream can\'t be constructed with garbage'); + +test(() => { + + assert_throws_js(TypeError, () => new ReadableStream({ type: null }), + 'constructor should throw when the type is null'); + assert_throws_js(TypeError, () => new ReadableStream({ type: '' }), + 'constructor should throw when the type is empty string'); + assert_throws_js(TypeError, () => new ReadableStream({ type: 'asdf' }), + 'constructor should throw when the type is asdf'); + assert_throws_exactly( + error1, + () => new ReadableStream({ type: { get toString() { throw error1; } } }), + 'constructor should throw when ToString() throws' + ); + assert_throws_exactly( + error1, + () => new ReadableStream({ type: { toString() { throw error1; } } }), + 'constructor should throw when ToString() throws' + ); + +}, 'ReadableStream can\'t be constructed with an invalid type'); + +test(() => { + + assert_throws_js(TypeError, () => { + new ReadableStream({ start: 'potato' }); + }, 'constructor should throw when start is not a function'); + +}, 'ReadableStream constructor should throw for non-function start arguments'); + +test(() => { + + assert_throws_js(TypeError, () => new ReadableStream({ cancel: '2' }), 'constructor should throw'); + +}, 'ReadableStream constructor will not tolerate initial garbage as cancel argument'); + +test(() => { + + assert_throws_js(TypeError, () => new ReadableStream({ pull: { } }), 'constructor should throw'); + +}, 'ReadableStream constructor will not tolerate initial garbage as pull argument'); + +test(() => { + + let startCalled = false; + + const source = { + start() { + assert_equals(this, source, 'source is this during start'); + startCalled = true; + } + }; + + new ReadableStream(source); + assert_true(startCalled); + +}, 'ReadableStream start should be called with the proper thisArg'); + +test(() => { + + let startCalled = false; + const source = { + start(controller) { + const properties = ['close', 'constructor', 'desiredSize', 'enqueue', 'error']; + assert_array_equals(Object.getOwnPropertyNames(Object.getPrototypeOf(controller)).sort(), properties, + 'prototype should have the right properties'); + + controller.test = ''; + assert_array_equals(Object.getOwnPropertyNames(Object.getPrototypeOf(controller)).sort(), properties, + 'prototype should still have the right properties'); + assert_not_equals(Object.getOwnPropertyNames(controller).indexOf('test'), -1, + '"test" should be a property of the controller'); + + startCalled = true; + } + }; + + new ReadableStream(source); + assert_true(startCalled); + +}, 'ReadableStream start controller parameter should be extensible'); + +test(() => { + (new ReadableStream()).getReader(undefined); + (new ReadableStream()).getReader({}); + (new ReadableStream()).getReader({ mode: undefined, notmode: 'ignored' }); + assert_throws_js(TypeError, () => (new ReadableStream()).getReader({ mode: 'potato' })); +}, 'default ReadableStream getReader() should only accept mode:undefined'); + +promise_test(() => { + + function SimpleStreamSource() {} + let resolve; + const promise = new Promise(r => resolve = r); + SimpleStreamSource.prototype = { + start: resolve + }; + + new ReadableStream(new SimpleStreamSource()); + return promise; + +}, 'ReadableStream should be able to call start method within prototype chain of its source'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + return delay(5).then(() => { + c.enqueue('a'); + c.close(); + }); + } + }); + + const reader = rs.getReader(); + return reader.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'value read should be the one enqueued'); + return reader.closed; + }); + +}, 'ReadableStream start should be able to return a promise'); + +promise_test(() => { + + const theError = new Error('rejected!'); + const rs = new ReadableStream({ + start() { + return delay(1).then(() => { + throw theError; + }); + } + }); + + return rs.getReader().closed.then(() => { + assert_unreached('closed promise should be rejected'); + }, e => { + assert_equals(e, theError, 'promise should be rejected with the same error'); + }); + +}, 'ReadableStream start should be able to return a promise and reject it'); + +promise_test(() => { + + const objects = [ + { potato: 'Give me more!' }, + 'test', + 1 + ]; + + const rs = new ReadableStream({ + start(c) { + for (const o of objects) { + c.enqueue(o); + } + c.close(); + } + }); + + const reader = rs.getReader(); + + return Promise.all([reader.read(), reader.read(), reader.read(), reader.closed]).then(r => { + assert_object_equals(r[0], { value: objects[0], done: false }, 'value read should be the one enqueued'); + assert_object_equals(r[1], { value: objects[1], done: false }, 'value read should be the one enqueued'); + assert_object_equals(r[2], { value: objects[2], done: false }, 'value read should be the one enqueued'); + }); + +}, 'ReadableStream should be able to enqueue different objects.'); + +promise_test(() => { + + const error = new Error('pull failure'); + const rs = new ReadableStream({ + pull() { + return Promise.reject(error); + } + }); + + const reader = rs.getReader(); + + let closed = false; + let read = false; + + return Promise.all([ + reader.closed.then(() => { + assert_unreached('closed should be rejected'); + }, e => { + closed = true; + assert_false(read); + assert_equals(e, error, 'closed should be rejected with the thrown error'); + }), + reader.read().then(() => { + assert_unreached('read() should be rejected'); + }, e => { + read = true; + assert_true(closed); + assert_equals(e, error, 'read() should be rejected with the thrown error'); + }) + ]); + +}, 'ReadableStream: if pull rejects, it should error the stream'); + +promise_test(() => { + + let pullCount = 0; + + new ReadableStream({ + pull() { + pullCount++; + } + }); + + return flushAsyncEvents().then(() => { + assert_equals(pullCount, 1, 'pull should be called once start finishes'); + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should only call pull once upon starting the stream'); + +promise_test(() => { + + let pullCount = 0; + + const rs = new ReadableStream({ + pull(c) { + // Don't enqueue immediately after start. We want the stream to be empty when we call .read() on it. + if (pullCount > 0) { + c.enqueue(pullCount); + } + ++pullCount; + } + }); + + return flushAsyncEvents().then(() => { + assert_equals(pullCount, 1, 'pull should be called once start finishes'); + }).then(() => { + const reader = rs.getReader(); + const read = reader.read(); + assert_equals(pullCount, 2, 'pull should be called when read is called'); + return read; + }).then(result => { + assert_equals(pullCount, 3, 'pull should be called again in reaction to calling read'); + assert_object_equals(result, { value: 1, done: false }, 'the result read should be the one enqueued'); + }); + +}, 'ReadableStream: should call pull when trying to read from a started, empty stream'); + +promise_test(() => { + + let pullCount = 0; + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + }, + pull() { + pullCount++; + } + }); + + const read = rs.getReader().read(); + assert_equals(pullCount, 0, 'calling read() should not cause pull to be called yet'); + + return flushAsyncEvents().then(() => { + assert_equals(pullCount, 1, 'pull should be called once start finishes'); + return read; + }).then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first read() should return first chunk'); + assert_equals(pullCount, 1, 'pull should not have been called again'); + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should only call pull once on a non-empty stream read from before start fulfills'); + +promise_test(() => { + + let pullCount = 0; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + }, + pull() { + pullCount++; + } + }); + + return flushAsyncEvents().then(() => { + assert_equals(pullCount, 0, 'pull should not be called once start finishes, since the queue is full'); + + const read = rs.getReader().read(); + assert_equals(pullCount, 1, 'calling read() should cause pull to be called immediately'); + return read; + }).then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first read() should return first chunk'); + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should only call pull once on a non-empty stream read from after start fulfills'); + +promise_test(() => { + + let pullCount = 0; + let controller; + + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + ++pullCount; + } + }); + + const reader = rs.getReader(); + return flushAsyncEvents().then(() => { + assert_equals(pullCount, 1, 'pull should have been called once by the time the stream starts'); + + controller.enqueue('a'); + assert_equals(pullCount, 1, 'pull should not have been called again after enqueue'); + + return reader.read(); + }).then(() => { + assert_equals(pullCount, 2, 'pull should have been called again after read'); + + return delay(10); + }).then(() => { + assert_equals(pullCount, 2, 'pull should be called exactly twice'); + }); +}, 'ReadableStream: should call pull in reaction to read()ing the last chunk, if not draining'); + +promise_test(() => { + + let pullCount = 0; + let controller; + + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + ++pullCount; + } + }); + + const reader = rs.getReader(); + + return flushAsyncEvents().then(() => { + assert_equals(pullCount, 1, 'pull should have been called once by the time the stream starts'); + + controller.enqueue('a'); + assert_equals(pullCount, 1, 'pull should not have been called again after enqueue'); + + controller.close(); + + return reader.read(); + }).then(() => { + assert_equals(pullCount, 1, 'pull should not have been called a second time after read'); + + return delay(10); + }).then(() => { + assert_equals(pullCount, 1, 'pull should be called exactly once'); + }); + +}, 'ReadableStream: should not call pull() in reaction to read()ing the last chunk, if draining'); + +promise_test(() => { + + let resolve; + let returnedPromise; + let timesCalled = 0; + + const rs = new ReadableStream({ + pull(c) { + c.enqueue(++timesCalled); + returnedPromise = new Promise(r => resolve = r); + return returnedPromise; + } + }); + const reader = rs.getReader(); + + return reader.read() + .then(result1 => { + assert_equals(timesCalled, 1, + 'pull should have been called once after start, but not yet have been called a second time'); + assert_object_equals(result1, { value: 1, done: false }, 'read() should fulfill with the enqueued value'); + + return delay(10); + }).then(() => { + assert_equals(timesCalled, 1, 'after 10 ms, pull should still only have been called once'); + + resolve(); + return returnedPromise; + }).then(() => { + assert_equals(timesCalled, 2, + 'after the promise returned by pull is fulfilled, pull should be called a second time'); + }); + +}, 'ReadableStream: should not call pull until the previous pull call\'s promise fulfills'); + +promise_test(() => { + + let timesCalled = 0; + + const rs = new ReadableStream( + { + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.enqueue('c'); + }, + pull() { + ++timesCalled; + } + }, + { + size() { + return 1; + }, + highWaterMark: Infinity + } + ); + const reader = rs.getReader(); + + return flushAsyncEvents().then(() => { + return reader.read(); + }).then(result1 => { + assert_object_equals(result1, { value: 'a', done: false }, 'first chunk should be as expected'); + + return reader.read(); + }).then(result2 => { + assert_object_equals(result2, { value: 'b', done: false }, 'second chunk should be as expected'); + + return reader.read(); + }).then(result3 => { + assert_object_equals(result3, { value: 'c', done: false }, 'third chunk should be as expected'); + + return delay(10); + }).then(() => { + // Once for after start, and once for every read. + assert_equals(timesCalled, 4, 'pull() should be called exactly four times'); + }); + +}, 'ReadableStream: should pull after start, and after every read'); + +promise_test(() => { + + let timesCalled = 0; + const startPromise = Promise.resolve(); + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.close(); + return startPromise; + }, + pull() { + ++timesCalled; + } + }); + + const reader = rs.getReader(); + return startPromise.then(() => { + assert_equals(timesCalled, 0, 'after start finishes, pull should not have been called'); + + return reader.read(); + }).then(() => { + assert_equals(timesCalled, 0, 'reading should not have triggered a pull call'); + + return reader.closed; + }).then(() => { + assert_equals(timesCalled, 0, 'stream should have closed with still no calls to pull'); + }); + +}, 'ReadableStream: should not call pull after start if the stream is now closed'); + +promise_test(() => { + + let timesCalled = 0; + let resolve; + const ready = new Promise(r => resolve = r); + + new ReadableStream( + { + start() {}, + pull(c) { + c.enqueue(++timesCalled); + + if (timesCalled === 4) { + resolve(); + } + } + }, + { + size() { + return 1; + }, + highWaterMark: 4 + } + ); + + return ready.then(() => { + // after start: size = 0, pull() + // after enqueue(1): size = 1, pull() + // after enqueue(2): size = 2, pull() + // after enqueue(3): size = 3, pull() + // after enqueue(4): size = 4, do not pull + assert_equals(timesCalled, 4, 'pull() should have been called four times'); + }); + +}, 'ReadableStream: should call pull after enqueueing from inside pull (with no read requests), if strategy allows'); + +promise_test(() => { + + let pullCalled = false; + + const rs = new ReadableStream({ + pull(c) { + pullCalled = true; + c.close(); + } + }); + + const reader = rs.getReader(); + return reader.closed.then(() => { + assert_true(pullCalled); + }); + +}, 'ReadableStream pull should be able to close a stream.'); + +promise_test(t => { + + const controllerError = { name: 'controller error' }; + + const rs = new ReadableStream({ + pull(c) { + c.error(controllerError); + } + }); + + return promise_rejects_exactly(t, controllerError, rs.getReader().closed); + +}, 'ReadableStream pull should be able to error a stream.'); + +promise_test(t => { + + const controllerError = { name: 'controller error' }; + const thrownError = { name: 'thrown error' }; + + const rs = new ReadableStream({ + pull(c) { + c.error(controllerError); + throw thrownError; + } + }); + + return promise_rejects_exactly(t, controllerError, rs.getReader().closed); + +}, 'ReadableStream pull should be able to error a stream and throw.'); + +test(() => { + + let startCalled = false; + + new ReadableStream({ + start(c) { + assert_equals(c.enqueue('a'), undefined, 'the first enqueue should return undefined'); + c.close(); + + assert_throws_js(TypeError, () => c.enqueue('b'), 'enqueue after close should throw a TypeError'); + startCalled = true; + } + }); + + assert_true(startCalled); + +}, 'ReadableStream: enqueue should throw when the stream is readable but draining'); + +test(() => { + + let startCalled = false; + + new ReadableStream({ + start(c) { + c.close(); + + assert_throws_js(TypeError, () => c.enqueue('a'), 'enqueue after close should throw a TypeError'); + startCalled = true; + } + }); + + assert_true(startCalled); + +}, 'ReadableStream: enqueue should throw when the stream is closed'); + +promise_test(() => { + + let startCalled = 0; + let pullCalled = 0; + let cancelCalled = 0; + + /* eslint-disable no-use-before-define */ + class Source { + start(c) { + startCalled++; + assert_equals(this, theSource, 'start() should be called with the correct this'); + c.enqueue('a'); + } + + pull() { + pullCalled++; + assert_equals(this, theSource, 'pull() should be called with the correct this'); + } + + cancel() { + cancelCalled++; + assert_equals(this, theSource, 'cancel() should be called with the correct this'); + } + } + /* eslint-enable no-use-before-define */ + + const theSource = new Source(); + theSource.debugName = 'the source object passed to the constructor'; // makes test failures easier to diagnose + + const rs = new ReadableStream(theSource); + const reader = rs.getReader(); + + return reader.read().then(() => { + reader.releaseLock(); + rs.cancel(); + assert_equals(startCalled, 1); + assert_equals(pullCalled, 1); + assert_equals(cancelCalled, 1); + return rs.getReader().closed; + }); + +}, 'ReadableStream: should call underlying source methods as methods'); + +test(() => { + new ReadableStream({ + start(c) { + assert_equals(c.desiredSize, 10, 'desiredSize must start at highWaterMark'); + c.close(); + assert_equals(c.desiredSize, 0, 'after closing, desiredSize must be 0'); + } + }, { + highWaterMark: 10 + }); +}, 'ReadableStream: desiredSize when closed'); + +test(() => { + new ReadableStream({ + start(c) { + assert_equals(c.desiredSize, 10, 'desiredSize must start at highWaterMark'); + c.error(); + assert_equals(c.desiredSize, null, 'after erroring, desiredSize must be null'); + } + }, { + highWaterMark: 10 + }); +}, 'ReadableStream: desiredSize when errored'); + +test(() => { + class Subclass extends ReadableStream { + extraFunction() { + return true; + } + } + assert_equals( + Object.getPrototypeOf(Subclass.prototype), ReadableStream.prototype, + 'Subclass.prototype\'s prototype should be ReadableStream.prototype'); + assert_equals(Object.getPrototypeOf(Subclass), ReadableStream, + 'Subclass\'s prototype should be ReadableStream'); + const sub = new Subclass(); + assert_true(sub instanceof ReadableStream, + 'Subclass object should be an instance of ReadableStream'); + assert_true(sub instanceof Subclass, + 'Subclass object should be an instance of Subclass'); + const lockedGetter = Object.getOwnPropertyDescriptor( + ReadableStream.prototype, 'locked').get; + assert_equals(lockedGetter.call(sub), sub.locked, + 'Subclass object should pass brand check'); + assert_true(sub.extraFunction(), + 'extraFunction() should be present on Subclass object'); +}, 'Subclassing ReadableStream should work'); + +test(() => { + + let startCalled = false; + new ReadableStream({ + start(c) { + assert_equals(c.desiredSize, 1); + c.enqueue('a'); + assert_equals(c.desiredSize, 0); + c.enqueue('b'); + assert_equals(c.desiredSize, -1); + c.enqueue('c'); + assert_equals(c.desiredSize, -2); + c.enqueue('d'); + assert_equals(c.desiredSize, -3); + c.enqueue('e'); + startCalled = true; + } + }); + + assert_true(startCalled); + +}, 'ReadableStream strategies: the default strategy should give desiredSize of 1 to start, decreasing by 1 per enqueue'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + const reader = rs.getReader(); + + assert_equals(controller.desiredSize, 1, 'desiredSize should start at 1'); + controller.enqueue('a'); + assert_equals(controller.desiredSize, 0, 'desiredSize should decrease to 0 after first enqueue'); + + return reader.read().then(result1 => { + assert_object_equals(result1, { value: 'a', done: false }, 'first chunk read should be correct'); + + assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the first read'); + controller.enqueue('b'); + assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the second enqueue'); + + return reader.read(); + }).then(result2 => { + assert_object_equals(result2, { value: 'b', done: false }, 'second chunk read should be correct'); + + assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the second read'); + controller.enqueue('c'); + assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the third enqueue'); + + return reader.read(); + }).then(result3 => { + assert_object_equals(result3, { value: 'c', done: false }, 'third chunk read should be correct'); + + assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the third read'); + controller.enqueue('d'); + assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the fourth enqueue'); + }); + +}, 'ReadableStream strategies: the default strategy should continue giving desiredSize of 1 if the chunks are read immediately'); + +promise_test(t => { + + const randomSource = new RandomPushSource(8); + + const rs = new ReadableStream({ + start(c) { + assert_equals(typeof c, 'object', 'c should be an object in start'); + assert_equals(typeof c.enqueue, 'function', 'enqueue should be a function in start'); + assert_equals(typeof c.close, 'function', 'close should be a function in start'); + assert_equals(typeof c.error, 'function', 'error should be a function in start'); + + randomSource.ondata = t.step_func(chunk => { + if (!c.enqueue(chunk) <= 0) { + randomSource.readStop(); + } + }); + + randomSource.onend = c.close.bind(c); + randomSource.onerror = c.error.bind(c); + }, + + pull(c) { + assert_equals(typeof c, 'object', 'c should be an object in pull'); + assert_equals(typeof c.enqueue, 'function', 'enqueue should be a function in pull'); + assert_equals(typeof c.close, 'function', 'close should be a function in pull'); + + randomSource.readStart(); + } + }); + + return readableStreamToArray(rs).then(chunks => { + assert_equals(chunks.length, 8, '8 chunks should be read'); + for (const chunk of chunks) { + assert_equals(chunk.length, 128, 'chunk should have 128 bytes'); + } + }); + +}, 'ReadableStream integration test: adapting a random push source'); + +promise_test(() => { + + const rs = sequentialReadableStream(10); + + return readableStreamToArray(rs).then(chunks => { + assert_true(rs.source.closed, 'source should be closed after all chunks are read'); + assert_array_equals(chunks, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'the expected 10 chunks should be read'); + }); + +}, 'ReadableStream integration test: adapting a sync pull source'); + +promise_test(() => { + + const rs = sequentialReadableStream(10, { async: true }); + + return readableStreamToArray(rs).then(chunks => { + assert_true(rs.source.closed, 'source should be closed after all chunks are read'); + assert_array_equals(chunks, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'the expected 10 chunks should be read'); + }); + +}, 'ReadableStream integration test: adapting an async pull source'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/patched-global.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/patched-global.any.js new file mode 100644 index 000000000000..a64a054a97f1 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/patched-global.any.js @@ -0,0 +1,142 @@ +// META: global=window,worker +'use strict'; + +// Tests which patch the global environment are kept separate to avoid +// interfering with other tests. + +const ReadableStream_prototype_locked_get = + Object.getOwnPropertyDescriptor(ReadableStream.prototype, 'locked').get; + +// Verify that |rs| passes the brand check as a readable stream. +function isReadableStream(rs) { + try { + ReadableStream_prototype_locked_get.call(rs); + return true; + } catch (e) { + return false; + } +} + +test(t => { + const rs = new ReadableStream(); + + const trappedProperties = ['highWaterMark', 'size', 'start', 'type', 'mode']; + for (const property of trappedProperties) { + // eslint-disable-next-line no-extend-native, accessor-pairs + Object.defineProperty(Object.prototype, property, { + get() { throw new Error(`${property} getter called`); }, + configurable: true + }); + } + t.add_cleanup(() => { + for (const property of trappedProperties) { + delete Object.prototype[property]; + } + }); + + const [branch1, branch2] = rs.tee(); + assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream'); + assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream'); +}, 'ReadableStream tee() should not touch Object.prototype properties'); + +test(t => { + const rs = new ReadableStream(); + + const oldReadableStream = self.ReadableStream; + + self.ReadableStream = function() { + throw new Error('ReadableStream called on global object'); + }; + + t.add_cleanup(() => { + self.ReadableStream = oldReadableStream; + }); + + const [branch1, branch2] = rs.tee(); + + assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream'); + assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream'); +}, 'ReadableStream tee() should not call the global ReadableStream'); + +promise_test(async t => { + const rs = new ReadableStream({ + start(c) { + c.enqueue(1); + c.enqueue(2); + c.enqueue(3); + c.close(); + } + }); + + const oldReadableStreamGetReader = ReadableStream.prototype.getReader; + + const ReadableStreamDefaultReader = (new ReadableStream()).getReader().constructor; + const oldDefaultReaderRead = ReadableStreamDefaultReader.prototype.read; + const oldDefaultReaderCancel = ReadableStreamDefaultReader.prototype.cancel; + const oldDefaultReaderReleaseLock = ReadableStreamDefaultReader.prototype.releaseLock; + + self.ReadableStream.prototype.getReader = function() { + throw new Error('patched getReader() called'); + }; + + ReadableStreamDefaultReader.prototype.read = function() { + throw new Error('patched read() called'); + }; + ReadableStreamDefaultReader.prototype.cancel = function() { + throw new Error('patched cancel() called'); + }; + ReadableStreamDefaultReader.prototype.releaseLock = function() { + throw new Error('patched releaseLock() called'); + }; + + t.add_cleanup(() => { + self.ReadableStream.prototype.getReader = oldReadableStreamGetReader; + + ReadableStreamDefaultReader.prototype.read = oldDefaultReaderRead; + ReadableStreamDefaultReader.prototype.cancel = oldDefaultReaderCancel; + ReadableStreamDefaultReader.prototype.releaseLock = oldDefaultReaderReleaseLock; + }); + + // read the first chunk, then cancel + for await (const chunk of rs) { + break; + } + + // should be able to acquire a new reader + const reader = oldReadableStreamGetReader.call(rs); + // stream should be cancelled + await reader.closed; +}, 'ReadableStream async iterator should use the original values of getReader() and ReadableStreamDefaultReader ' + + 'methods'); + +test(t => { + const oldPromiseThen = Promise.prototype.then; + Promise.prototype.then = () => { + throw new Error('patched then() called'); + }; + t.add_cleanup(() => { + Promise.prototype.then = oldPromiseThen; + }); + const [branch1, branch2] = new ReadableStream().tee(); + assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream'); + assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream'); +}, 'tee() should not call Promise.prototype.then()'); + +test(t => { + const oldPromiseThen = Promise.prototype.then; + Promise.prototype.then = () => { + throw new Error('patched then() called'); + }; + t.add_cleanup(() => { + Promise.prototype.then = oldPromiseThen; + }); + let readableController; + const rs = new ReadableStream({ + start(c) { + readableController = c; + } + }); + const ws = new WritableStream(); + rs.pipeTo(ws); + readableController.close(); +}, 'pipeTo() should not call Promise.prototype.then()'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/reentrant-strategies.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/reentrant-strategies.any.js new file mode 100644 index 000000000000..b4988bc2433f --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/reentrant-strategies.any.js @@ -0,0 +1,264 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +'use strict'; + +// The size() function of the readable strategy can re-entrantly call back into the ReadableStream implementation. This +// makes it risky to cache state across the call to ReadableStreamDefaultControllerEnqueue. These tests attempt to catch +// such errors. They are separated from the other strategy tests because no real user code should ever do anything like +// this. + +const error1 = new Error('error1'); +error1.name = 'error1'; + +promise_test(() => { + let controller; + let calls = 0; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + ++calls; + if (calls < 2) { + controller.enqueue('b'); + } + return 1; + } + }); + controller.enqueue('a'); + controller.close(); + return readableStreamToArray(rs) + .then(array => assert_array_equals(array, ['b', 'a'], 'array should contain two chunks')); +}, 'enqueue() inside size() should work'); + +promise_test(() => { + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + // The queue is empty. + controller.close(); + // The state has gone from "readable" to "closed". + return 1; + // This chunk will be enqueued, but will be impossible to read because the state is already "closed". + } + }); + controller.enqueue('a'); + return readableStreamToArray(rs) + .then(array => assert_array_equals(array, [], 'array should contain no chunks')); + // The chunk 'a' is still in rs's queue. It is closed so 'a' cannot be read. +}, 'close() inside size() should not crash'); + +promise_test(() => { + let controller; + let calls = 0; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + ++calls; + if (calls === 2) { + // The queue contains one chunk. + controller.close(); + // The state is still "readable", but closeRequest is now true. + } + return 1; + } + }); + controller.enqueue('a'); + controller.enqueue('b'); + return readableStreamToArray(rs) + .then(array => assert_array_equals(array, ['a', 'b'], 'array should contain two chunks')); +}, 'close request inside size() should work'); + +promise_test(t => { + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + controller.error(error1); + return 1; + } + }); + controller.enqueue('a'); + return promise_rejects_exactly(t, error1, rs.getReader().read(), 'read() should reject'); +}, 'error() inside size() should work'); + +promise_test(() => { + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + assert_equals(controller.desiredSize, 1, 'desiredSize should be 1'); + return 1; + }, + highWaterMark: 1 + }); + controller.enqueue('a'); + controller.close(); + return readableStreamToArray(rs) + .then(array => assert_array_equals(array, ['a'], 'array should contain one chunk')); +}, 'desiredSize inside size() should work'); + +promise_test(t => { + let cancelPromise; + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + }, + cancel: t.step_func(reason => { + assert_equals(reason, error1, 'reason should be error1'); + assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue() should throw'); + }) + }, { + size() { + cancelPromise = rs.cancel(error1); + return 1; + }, + highWaterMark: Infinity + }); + controller.enqueue('a'); + const reader = rs.getReader(); + return Promise.all([ + reader.closed, + cancelPromise + ]); +}, 'cancel() inside size() should work'); + +promise_test(() => { + let controller; + let pipeToPromise; + const ws = recordingWritableStream(); + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + if (!pipeToPromise) { + pipeToPromise = rs.pipeTo(ws); + } + return 1; + }, + highWaterMark: 1 + }); + controller.enqueue('a'); + assert_not_equals(pipeToPromise, undefined); + + // Some pipeTo() implementations need an additional chunk enqueued in order for the first one to be processed. See + // https://github.com/whatwg/streams/issues/794 for background. + controller.enqueue('a'); + + // Give pipeTo() a chance to process the queued chunks. + return delay(0).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'a'], 'ws should contain two chunks'); + controller.close(); + return pipeToPromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'a', 'close'], 'target should have been closed'); + }); +}, 'pipeTo() inside size() should behave as expected'); + +promise_test(() => { + let controller; + let readPromise; + let calls = 0; + let readResolved = false; + let reader; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + // This is triggered by controller.enqueue(). The queue is empty and there are no pending reads. This read is + // added to the list of pending reads. + readPromise = reader.read(); + ++calls; + return 1; + }, + highWaterMark: 0 + }); + reader = rs.getReader(); + controller.enqueue('a'); + readPromise.then(() => { + readResolved = true; + }); + return flushAsyncEvents().then(() => { + assert_false(readResolved); + controller.enqueue('b'); + assert_equals(calls, 1, 'size() should have been called once'); + return delay(0); + }).then(() => { + assert_true(readResolved); + assert_equals(calls, 1, 'size() should only be called once'); + return readPromise; + }).then(({ value, done }) => { + assert_false(done, 'done should be false'); + // See https://github.com/whatwg/streams/issues/794 for why this chunk is not 'a'. + assert_equals(value, 'b', 'chunk should have been read'); + assert_equals(calls, 1, 'calls should still be 1'); + return reader.read(); + }).then(({ value, done }) => { + assert_false(done, 'done should be false again'); + assert_equals(value, 'a', 'chunk a should come after b'); + }); +}, 'read() inside of size() should behave as expected'); + +promise_test(() => { + let controller; + let reader; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + reader = rs.getReader(); + return 1; + } + }); + controller.enqueue('a'); + return reader.read().then(({ value, done }) => { + assert_false(done, 'done should be false'); + assert_equals(value, 'a', 'value should be a'); + }); +}, 'getReader() inside size() should work'); + +promise_test(() => { + let controller; + let branch1; + let branch2; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }, { + size() { + [branch1, branch2] = rs.tee(); + return 1; + } + }); + controller.enqueue('a'); + assert_true(rs.locked, 'rs should be locked'); + controller.close(); + return Promise.all([ + readableStreamToArray(branch1).then(array => assert_array_equals(array, ['a'], 'branch1 should have one chunk')), + readableStreamToArray(branch2).then(array => assert_array_equals(array, ['a'], 'branch2 should have one chunk')) + ]); +}, 'tee() inside size() should work'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/tee.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/tee.any.js new file mode 100644 index 000000000000..00397932f4b6 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/tee.any.js @@ -0,0 +1,479 @@ +// META: global=window,worker +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +// META: script=../resources/rs-test-templates.js +'use strict'; + +test(() => { + + const rs = new ReadableStream(); + const result = rs.tee(); + + assert_true(Array.isArray(result), 'return value should be an array'); + assert_equals(result.length, 2, 'array should have length 2'); + assert_equals(result[0].constructor, ReadableStream, '0th element should be a ReadableStream'); + assert_equals(result[1].constructor, ReadableStream, '1st element should be a ReadableStream'); + +}, 'ReadableStream teeing: rs.tee() returns an array of two ReadableStreams'); + +promise_test(t => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const branch = rs.tee(); + const branch1 = branch[0]; + const branch2 = branch[1]; + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + reader2.closed.then(t.unreached_func('branch2 should not be closed')); + + return Promise.all([ + reader1.closed, + reader1.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch1 should be correct'); + }), + reader1.read().then(r => { + assert_object_equals(r, { value: 'b', done: false }, 'second chunk from branch1 should be correct'); + }), + reader1.read().then(r => { + assert_object_equals(r, { value: undefined, done: true }, 'third read() from branch1 should be done'); + }), + reader2.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch2 should be correct'); + }) + ]); + +}, 'ReadableStream teeing: should be able to read one branch to the end without affecting the other'); + +promise_test(() => { + + const theObject = { the: 'test object' }; + const rs = new ReadableStream({ + start(c) { + c.enqueue(theObject); + } + }); + + const branch = rs.tee(); + const branch1 = branch[0]; + const branch2 = branch[1]; + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + return Promise.all([reader1.read(), reader2.read()]).then(values => { + assert_object_equals(values[0], values[1], 'the values should be equal'); + }); + +}, 'ReadableStream teeing: values should be equal across each branch'); + +promise_test(t => { + + const theError = { name: 'boo!' }; + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + }, + pull() { + throw theError; + } + }); + + const branches = rs.tee(); + const reader1 = branches[0].getReader(); + const reader2 = branches[1].getReader(); + + reader1.label = 'reader1'; + reader2.label = 'reader2'; + + return Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed), + reader1.read().then(r => { + assert_object_equals(r, { value: 'a', done: false }, 'should be able to read the first chunk in branch1'); + }), + reader1.read().then(r => { + assert_object_equals(r, { value: 'b', done: false }, 'should be able to read the second chunk in branch1'); + + return promise_rejects_exactly(t, theError, reader2.read()); + }) + .then(() => promise_rejects_exactly(t, theError, reader1.read())) + ]); + +}, 'ReadableStream teeing: errors in the source should propagate to both branches'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const branches = rs.tee(); + const branch1 = branches[0]; + const branch2 = branches[1]; + branch1.cancel(); + + return Promise.all([ + readableStreamToArray(branch1).then(chunks => { + assert_array_equals(chunks, [], 'branch1 should have no chunks'); + }), + readableStreamToArray(branch2).then(chunks => { + assert_array_equals(chunks, ['a', 'b'], 'branch2 should have two chunks'); + }) + ]); + +}, 'ReadableStream teeing: canceling branch1 should not impact branch2'); + +promise_test(() => { + + const rs = new ReadableStream({ + start(c) { + c.enqueue('a'); + c.enqueue('b'); + c.close(); + } + }); + + const branches = rs.tee(); + const branch1 = branches[0]; + const branch2 = branches[1]; + branch2.cancel(); + + return Promise.all([ + readableStreamToArray(branch1).then(chunks => { + assert_array_equals(chunks, ['a', 'b'], 'branch1 should have two chunks'); + }), + readableStreamToArray(branch2).then(chunks => { + assert_array_equals(chunks, [], 'branch2 should have no chunks'); + }) + ]); + +}, 'ReadableStream teeing: canceling branch2 should not impact branch1'); + +templatedRSTeeCancel('ReadableStream teeing', (extras) => { + return new ReadableStream({ ...extras }); +}); + +promise_test(t => { + + let controller; + const stream = new ReadableStream({ start(c) { controller = c; } }); + const [branch1, branch2] = stream.tee(); + + const error = new Error(); + error.name = 'distinctive'; + + // Ensure neither branch is waiting in ReadableStreamDefaultReaderRead(). + controller.enqueue(); + controller.enqueue(); + + return delay(0).then(() => { + // This error will have to be detected via [[closedPromise]]. + controller.error(error); + + const reader1 = branch1.getReader(); + const reader2 = branch2.getReader(); + + return Promise.all([ + promise_rejects_exactly(t, error, reader1.closed, 'reader1.closed should reject'), + promise_rejects_exactly(t, error, reader2.closed, 'reader2.closed should reject') + ]); + }); + +}, 'ReadableStream teeing: erroring a teed stream should error both branches'); + +promise_test(() => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const branches = rs.tee(); + const reader1 = branches[0].getReader(); + const reader2 = branches[1].getReader(); + + const promise = Promise.all([reader1.closed, reader2.closed]); + + controller.close(); + return promise; + +}, 'ReadableStream teeing: closing the original should immediately close the branches'); + +promise_test(t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const branches = rs.tee(); + const reader1 = branches[0].getReader(); + const reader2 = branches[1].getReader(); + + const theError = { name: 'boo!' }; + const promise = Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + + controller.error(theError); + return promise; + +}, 'ReadableStream teeing: erroring the original should immediately error the branches'); + +promise_test(async t => { + + let controller; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + const cancelPromise = reader2.cancel(); + + controller.enqueue('a'); + + const read1 = await reader1.read(); + assert_object_equals(read1, { value: 'a', done: false }, 'first read() from branch1 should fulfill with the chunk'); + + controller.close(); + + const read2 = await reader1.read(); + assert_object_equals(read2, { value: undefined, done: true }, 'second read() from branch1 should be done'); + + await Promise.all([ + reader1.closed, + cancelPromise + ]); + +}, 'ReadableStream teeing: canceling branch1 should finish when branch2 reads until end of stream'); + +promise_test(async t => { + + let controller; + const theError = { name: 'boo!' }; + const rs = new ReadableStream({ + start(c) { + controller = c; + } + }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + const cancelPromise = reader2.cancel(); + + controller.error(theError); + + await Promise.all([ + promise_rejects_exactly(t, theError, reader1.read()), + cancelPromise + ]); + +}, 'ReadableStream teeing: canceling branch1 should finish when original stream errors'); + +promise_test(async () => { + + const rs = new ReadableStream({}); + + const [branch1, branch2] = rs.tee(); + + const cancel1 = branch1.cancel(); + await flushAsyncEvents(); + const cancel2 = branch2.cancel(); + + await Promise.all([cancel1, cancel2]); + +}, 'ReadableStream teeing: canceling both branches in sequence with delay'); + +promise_test(async t => { + + const theError = { name: 'boo!' }; + const rs = new ReadableStream({ + cancel() { + throw theError; + } + }); + + const [branch1, branch2] = rs.tee(); + + const cancel1 = branch1.cancel(); + await flushAsyncEvents(); + const cancel2 = branch2.cancel(); + + await Promise.all([ + promise_rejects_exactly(t, theError, cancel1), + promise_rejects_exactly(t, theError, cancel2) + ]); + +}, 'ReadableStream teeing: failing to cancel when canceling both branches in sequence with delay'); + +test(t => { + + // Copy original global. + const oldReadableStream = ReadableStream; + const getReader = ReadableStream.prototype.getReader; + + const origRS = new ReadableStream(); + + // Replace the global ReadableStream constructor with one that doesn't work. + ReadableStream = function() { + throw new Error('global ReadableStream constructor called'); + }; + t.add_cleanup(() => { + ReadableStream = oldReadableStream; + }); + + // This will probably fail if the global ReadableStream constructor was used. + const [rs1, rs2] = origRS.tee(); + + // These will definitely fail if the global ReadableStream constructor was used. + assert_not_equals(getReader.call(rs1), undefined, 'getReader should work on rs1'); + assert_not_equals(getReader.call(rs2), undefined, 'getReader should work on rs2'); + +}, 'ReadableStreamTee should not use a modified ReadableStream constructor from the global object'); + +promise_test(t => { + + const rs = recordingReadableStream({}, { highWaterMark: 0 }); + + // Create two branches, each with a HWM of 1. This should result in one + // chunk being pulled, not two. + rs.tee(); + return flushAsyncEvents().then(() => { + assert_array_equals(rs.events, ['pull'], 'pull should only be called once'); + }); + +}, 'ReadableStreamTee should not pull more chunks than can fit in the branch queue'); + +promise_test(t => { + + const rs = recordingReadableStream({ + pull(controller) { + controller.enqueue('a'); + } + }, { highWaterMark: 0 }); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + return Promise.all([reader1.read(), reader2.read()]) + .then(() => { + assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice'); + }); + +}, 'ReadableStreamTee should only pull enough to fill the emptiest queue'); + +promise_test(t => { + + const rs = recordingReadableStream({}, { highWaterMark: 0 }); + const theError = { name: 'boo!' }; + + rs.controller.error(theError); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + + return flushAsyncEvents().then(() => { + assert_array_equals(rs.events, [], 'pull should not be called'); + + return Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + }); + +}, 'ReadableStreamTee should not pull when original is already errored'); + +for (const branch of [1, 2]) { + promise_test(t => { + + const rs = recordingReadableStream({}, { highWaterMark: 0 }); + const theError = { name: 'boo!' }; + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + + return flushAsyncEvents().then(() => { + assert_array_equals(rs.events, ['pull'], 'pull should be called once'); + + rs.controller.enqueue('a'); + + const reader = (branch === 1) ? reader1 : reader2; + return reader.read(); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice'); + + rs.controller.error(theError); + + return Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice'); + }); + + }, `ReadableStreamTee stops pulling when original stream errors while branch ${branch} is reading`); +} + +promise_test(t => { + + const rs = recordingReadableStream({}, { highWaterMark: 0 }); + const theError = { name: 'boo!' }; + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + + return flushAsyncEvents().then(() => { + assert_array_equals(rs.events, ['pull'], 'pull should be called once'); + + rs.controller.enqueue('a'); + + return Promise.all([reader1.read(), reader2.read()]); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice'); + + rs.controller.error(theError); + + return Promise.all([ + promise_rejects_exactly(t, theError, reader1.closed), + promise_rejects_exactly(t, theError, reader2.closed) + ]); + }).then(() => flushAsyncEvents()).then(() => { + assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice'); + }); + +}, 'ReadableStreamTee stops pulling when original stream errors while both branches are reading'); + +promise_test(async () => { + + const rs = recordingReadableStream(); + + const [reader1, reader2] = rs.tee().map(branch => branch.getReader()); + const branch1Reads = [reader1.read(), reader1.read()]; + const branch2Reads = [reader2.read(), reader2.read()]; + + await flushAsyncEvents(); + rs.controller.enqueue('a'); + rs.controller.close(); + + assert_object_equals(await branch1Reads[0], { value: 'a', done: false }, 'first chunk from branch1 should be correct'); + assert_object_equals(await branch2Reads[0], { value: 'a', done: false }, 'first chunk from branch2 should be correct'); + + assert_object_equals(await branch1Reads[1], { value: undefined, done: true }, 'second read() from branch1 should be done'); + assert_object_equals(await branch2Reads[1], { value: undefined, done: true }, 'second read() from branch2 should be done'); + +}, 'ReadableStream teeing: enqueue() and close() while both branches are pulling'); diff --git a/test/js/third_party/wpt-streams/streams/readable-streams/templated.any.js b/test/js/third_party/wpt-streams/streams/readable-streams/templated.any.js new file mode 100644 index 000000000000..8fdb0176ceff --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/readable-streams/templated.any.js @@ -0,0 +1,149 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-test-templates.js +'use strict'; + +// Run the readable stream test templates against readable streams created directly using the constructor + +const theError = { name: 'boo!' }; +const chunks = ['a', 'b']; + +templatedRSEmpty('ReadableStream (empty)', () => { + return new ReadableStream(); +}); + +templatedRSEmptyReader('ReadableStream (empty) reader', () => { + const stream = new ReadableStream(); + const reader = stream.getReader(); + return { stream, reader, read: () => reader.read() }; +}); + +templatedRSClosed('ReadableStream (closed via call in start)', () => { + return new ReadableStream({ + start(c) { + c.close(); + } + }); +}); + +templatedRSClosedReader('ReadableStream reader (closed before getting reader)', () => { + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + } + }); + controller.close(); + const result = streamAndDefaultReader(stream); + return result; +}); + +templatedRSClosedReader('ReadableStream reader (closed after getting reader)', () => { + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + } + }); + const result = streamAndDefaultReader(stream); + controller.close(); + return result; +}); + +templatedRSClosed('ReadableStream (closed via cancel)', () => { + const stream = new ReadableStream(); + stream.cancel(); + return stream; +}); + +templatedRSClosedReader('ReadableStream reader (closed via cancel after getting reader)', () => { + const stream = new ReadableStream(); + const result = streamAndDefaultReader(stream); + result.reader.cancel(); + return result; +}); + +templatedRSErrored('ReadableStream (errored via call in start)', () => { + return new ReadableStream({ + start(c) { + c.error(theError); + } + }); +}, theError); + +templatedRSErroredSyncOnly('ReadableStream (errored via call in start)', () => { + return new ReadableStream({ + start(c) { + c.error(theError); + } + }); +}, theError); + +templatedRSErrored('ReadableStream (errored via returning a rejected promise in start)', () => { + return new ReadableStream({ + start() { + return Promise.reject(theError); + } + }); +}, theError); + +templatedRSErroredReader('ReadableStream (errored via returning a rejected promise in start) reader', () => { + return streamAndDefaultReader(new ReadableStream({ + start() { + return Promise.reject(theError); + } + })); +}, theError); + +templatedRSErroredReader('ReadableStream reader (errored before getting reader)', () => { + let controller; + const stream = new ReadableStream({ + start(c) { + controller = c; + } + }); + controller.error(theError); + return streamAndDefaultReader(stream); +}, theError); + +templatedRSErroredReader('ReadableStream reader (errored after getting reader)', () => { + let controller; + const result = streamAndDefaultReader(new ReadableStream({ + start(c) { + controller = c; + } + })); + controller.error(theError); + return result; +}, theError); + +templatedRSTwoChunksOpenReader('ReadableStream (two chunks enqueued, still open) reader', () => { + return streamAndDefaultReader(new ReadableStream({ + start(c) { + c.enqueue(chunks[0]); + c.enqueue(chunks[1]); + } + })); +}, chunks); + +templatedRSTwoChunksClosedReader('ReadableStream (two chunks enqueued, then closed) reader', () => { + let doClose; + const stream = new ReadableStream({ + start(c) { + c.enqueue(chunks[0]); + c.enqueue(chunks[1]); + doClose = c.close.bind(c); + } + }); + const result = streamAndDefaultReader(stream); + doClose(); + return result; +}, chunks); + +templatedRSThrowAfterCloseOrError('ReadableStream', (extras) => { + return new ReadableStream({ ...extras }); +}); + +function streamAndDefaultReader(stream) { + return { stream, reader: stream.getReader() }; +} diff --git a/test/js/third_party/wpt-streams/streams/resources/recording-streams.js b/test/js/third_party/wpt-streams/streams/resources/recording-streams.js new file mode 100644 index 000000000000..661fe512f516 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/resources/recording-streams.js @@ -0,0 +1,131 @@ +'use strict'; + +self.recordingReadableStream = (extras = {}, strategy) => { + let controllerToCopyOver; + const stream = new ReadableStream({ + type: extras.type, + start(controller) { + controllerToCopyOver = controller; + + if (extras.start) { + return extras.start(controller); + } + + return undefined; + }, + pull(controller) { + stream.events.push('pull'); + + if (extras.pull) { + return extras.pull(controller); + } + + return undefined; + }, + cancel(reason) { + stream.events.push('cancel', reason); + stream.eventsWithoutPulls.push('cancel', reason); + + if (extras.cancel) { + return extras.cancel(reason); + } + + return undefined; + } + }, strategy); + + stream.controller = controllerToCopyOver; + stream.events = []; + stream.eventsWithoutPulls = []; + + return stream; +}; + +self.recordingWritableStream = (extras = {}, strategy) => { + let controllerToCopyOver; + const stream = new WritableStream({ + start(controller) { + controllerToCopyOver = controller; + + if (extras.start) { + return extras.start(controller); + } + + return undefined; + }, + write(chunk, controller) { + stream.events.push('write', chunk); + + if (extras.write) { + return extras.write(chunk, controller); + } + + return undefined; + }, + close() { + stream.events.push('close'); + + if (extras.close) { + return extras.close(); + } + + return undefined; + }, + abort(e) { + stream.events.push('abort', e); + + if (extras.abort) { + return extras.abort(e); + } + + return undefined; + } + }, strategy); + + stream.controller = controllerToCopyOver; + stream.events = []; + + return stream; +}; + +self.recordingTransformStream = (extras = {}, writableStrategy, readableStrategy) => { + let controllerToCopyOver; + const stream = new TransformStream({ + start(controller) { + controllerToCopyOver = controller; + + if (extras.start) { + return extras.start(controller); + } + + return undefined; + }, + + transform(chunk, controller) { + stream.events.push('transform', chunk); + + if (extras.transform) { + return extras.transform(chunk, controller); + } + + controller.enqueue(chunk); + + return undefined; + }, + + flush(controller) { + stream.events.push('flush'); + + if (extras.flush) { + return extras.flush(controller); + } + + return undefined; + } + }, writableStrategy, readableStrategy); + + stream.controller = controllerToCopyOver; + stream.events = []; + + return stream; +}; diff --git a/test/js/third_party/wpt-streams/streams/resources/rs-test-templates.js b/test/js/third_party/wpt-streams/streams/resources/rs-test-templates.js new file mode 100644 index 000000000000..73ef0463768d --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/resources/rs-test-templates.js @@ -0,0 +1,776 @@ +'use strict'; + +// These tests can be run against any readable stream produced by the web platform that meets the given descriptions. +// For readable stream tests, the factory should return the stream. For reader tests, the factory should return a +// { stream, reader } object. (You can use this to vary the time at which you acquire a reader.) + +self.templatedRSEmpty = (label, factory) => { + test(() => {}, 'Running templatedRSEmpty with ' + label); + + test(() => { + + const rs = factory(); + + assert_equals(typeof rs.locked, 'boolean', 'has a boolean locked getter'); + assert_equals(typeof rs.cancel, 'function', 'has a cancel method'); + assert_equals(typeof rs.getReader, 'function', 'has a getReader method'); + assert_equals(typeof rs.pipeThrough, 'function', 'has a pipeThrough method'); + assert_equals(typeof rs.pipeTo, 'function', 'has a pipeTo method'); + assert_equals(typeof rs.tee, 'function', 'has a tee method'); + + }, label + ': instances have the correct methods and properties'); + + test(() => { + const rs = factory(); + + assert_throws_js(TypeError, () => rs.getReader({ mode: '' }), 'empty string mode should throw'); + assert_throws_js(TypeError, () => rs.getReader({ mode: null }), 'null mode should throw'); + assert_throws_js(TypeError, () => rs.getReader({ mode: 'asdf' }), 'asdf mode should throw'); + assert_throws_js(TypeError, () => rs.getReader(5), '5 should throw'); + + // Should not throw + rs.getReader(null); + + }, label + ': calling getReader with invalid arguments should throw appropriate errors'); +}; + +self.templatedRSClosed = (label, factory) => { + test(() => {}, 'Running templatedRSClosed with ' + label); + + promise_test(() => { + + const rs = factory(); + const cancelPromise1 = rs.cancel(); + const cancelPromise2 = rs.cancel(); + + assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises'); + + return Promise.all([ + cancelPromise1.then(v => assert_equals(v, undefined, 'first cancel() call should fulfill with undefined')), + cancelPromise2.then(v => assert_equals(v, undefined, 'second cancel() call should fulfill with undefined')) + ]); + + }, label + ': cancel() should return a distinct fulfilled promise each time'); + + test(() => { + + const rs = factory(); + assert_false(rs.locked, 'locked getter should return false'); + + }, label + ': locked should be false'); + + test(() => { + + const rs = factory(); + rs.getReader(); // getReader() should not throw. + + }, label + ': getReader() should be OK'); + + test(() => { + + const rs = factory(); + + const reader = rs.getReader(); + reader.releaseLock(); + + const reader2 = rs.getReader(); // Getting a second reader should not throw. + reader2.releaseLock(); + + rs.getReader(); // Getting a third reader should not throw. + + }, label + ': should be able to acquire multiple readers if they are released in succession'); + + test(() => { + + const rs = factory(); + + rs.getReader(); + + assert_throws_js(TypeError, () => rs.getReader(), 'getting a second reader should throw'); + assert_throws_js(TypeError, () => rs.getReader(), 'getting a third reader should throw'); + + }, label + ': should not be able to acquire a second reader if we don\'t release the first one'); +}; + +self.templatedRSErrored = (label, factory, error) => { + test(() => {}, 'Running templatedRSErrored with ' + label); + + promise_test(t => { + + const rs = factory(); + const reader = rs.getReader(); + + return Promise.all([ + promise_rejects_exactly(t, error, reader.closed), + promise_rejects_exactly(t, error, reader.read()) + ]); + + }, label + ': getReader() should return a reader that acts errored'); + + promise_test(t => { + + const rs = factory(); + const reader = rs.getReader(); + + return Promise.all([ + promise_rejects_exactly(t, error, reader.read()), + promise_rejects_exactly(t, error, reader.read()), + promise_rejects_exactly(t, error, reader.closed) + ]); + + }, label + ': read() twice should give the error each time'); + + test(() => { + const rs = factory(); + + assert_false(rs.locked, 'locked getter should return false'); + }, label + ': locked should be false'); +}; + +self.templatedRSErroredSyncOnly = (label, factory, error) => { + test(() => {}, 'Running templatedRSErroredSyncOnly with ' + label); + + promise_test(t => { + + const rs = factory(); + rs.getReader().releaseLock(); + const reader = rs.getReader(); // Calling getReader() twice does not throw (the stream is not locked). + + return promise_rejects_exactly(t, error, reader.closed); + + }, label + ': should be able to obtain a second reader, with the correct closed promise'); + + test(() => { + + const rs = factory(); + rs.getReader(); + + assert_throws_js(TypeError, () => rs.getReader(), 'getting a second reader should throw a TypeError'); + assert_throws_js(TypeError, () => rs.getReader(), 'getting a third reader should throw a TypeError'); + + }, label + ': should not be able to obtain additional readers if we don\'t release the first lock'); + + promise_test(t => { + + const rs = factory(); + const cancelPromise1 = rs.cancel(); + const cancelPromise2 = rs.cancel(); + + assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises'); + + return Promise.all([ + promise_rejects_exactly(t, error, cancelPromise1), + promise_rejects_exactly(t, error, cancelPromise2) + ]); + + }, label + ': cancel() should return a distinct rejected promise each time'); + + promise_test(t => { + + const rs = factory(); + const reader = rs.getReader(); + const cancelPromise1 = reader.cancel(); + const cancelPromise2 = reader.cancel(); + + assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises'); + + return Promise.all([ + promise_rejects_exactly(t, error, cancelPromise1), + promise_rejects_exactly(t, error, cancelPromise2) + ]); + + }, label + ': reader cancel() should return a distinct rejected promise each time'); +}; + +self.templatedRSEmptyReader = (label, factory) => { + test(() => {}, 'Running templatedRSEmptyReader with ' + label); + + test(() => { + + const reader = factory().reader; + + assert_true('closed' in reader, 'has a closed property'); + assert_equals(typeof reader.closed.then, 'function', 'closed property is thenable'); + + assert_equals(typeof reader.cancel, 'function', 'has a cancel method'); + assert_equals(typeof reader.read, 'function', 'has a read method'); + assert_equals(typeof reader.releaseLock, 'function', 'has a releaseLock method'); + + }, label + ': instances have the correct methods and properties'); + + test(() => { + + const { stream } = factory(); + + assert_true(stream.locked, 'locked getter should return true'); + + }, label + ': locked should be true'); + + promise_test(t => { + + const { read } = factory(); + + read().then( + t.unreached_func('read() should not fulfill'), + t.unreached_func('read() should not reject') + ); + + return delay(500); + + }, label + ': read() should never settle'); + + promise_test(t => { + + const { read } = factory(); + + read().then( + t.unreached_func('read() should not fulfill'), + t.unreached_func('read() should not reject') + ); + + read().then( + t.unreached_func('read() should not fulfill'), + t.unreached_func('read() should not reject') + ); + + return delay(500); + + }, label + ': two read()s should both never settle'); + + test(() => { + + const { read } = factory(); + assert_not_equals(read(), read(), 'the promises returned should be distinct'); + + }, label + ': read() should return distinct promises each time'); + + test(() => { + + const { stream } = factory(); + assert_throws_js(TypeError, () => stream.getReader(), 'stream.getReader() should throw a TypeError'); + + }, label + ': getReader() again on the stream should fail'); + + promise_test(async t => { + + const { stream, reader, read } = factory(); + + const read1 = read(); + const read2 = read(); + const closed = reader.closed; + + reader.releaseLock(); + + assert_false(stream.locked, 'the stream should be unlocked'); + + await Promise.all([ + promise_rejects_js(t, TypeError, read1, 'first read should reject'), + promise_rejects_js(t, TypeError, read2, 'second read should reject'), + promise_rejects_js(t, TypeError, closed, 'closed should reject') + ]); + + }, label + ': releasing the lock should reject all pending read requests'); + + promise_test(t => { + + const { reader, read } = factory(); + reader.releaseLock(); + + return Promise.all([ + promise_rejects_js(t, TypeError, read()), + promise_rejects_js(t, TypeError, read()) + ]); + + }, label + ': releasing the lock should cause further read() calls to reject with a TypeError'); + + promise_test(t => { + + const { reader } = factory(); + + const closedBefore = reader.closed; + reader.releaseLock(); + const closedAfter = reader.closed; + + assert_equals(closedBefore, closedAfter, 'the closed promise should not change identity'); + + return promise_rejects_js(t, TypeError, closedBefore); + + }, label + ': releasing the lock should cause closed calls to reject with a TypeError'); + + test(() => { + + const { stream, reader } = factory(); + + reader.releaseLock(); + assert_false(stream.locked, 'locked getter should return false'); + + }, label + ': releasing the lock should cause locked to become false'); + + promise_test(() => { + + const { reader, read } = factory(); + reader.cancel(); + + return read().then(r => { + assert_object_equals(r, { value: undefined, done: true }, 'read()ing from the reader should give a done result'); + }); + + }, label + ': canceling via the reader should cause the reader to act closed'); + + promise_test(t => { + + const { stream } = factory(); + return promise_rejects_js(t, TypeError, stream.cancel()); + + }, label + ': canceling via the stream should fail'); +}; + +self.templatedRSClosedReader = (label, factory) => { + test(() => {}, 'Running templatedRSClosedReader with ' + label); + + promise_test(() => { + + const reader = factory().reader; + + return reader.read().then(v => { + assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly'); + }); + + }, label + ': read() should fulfill with { value: undefined, done: true }'); + + promise_test(() => { + + const reader = factory().reader; + + return Promise.all([ + reader.read().then(v => { + assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly'); + }), + reader.read().then(v => { + assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly'); + }) + ]); + + }, label + ': read() multiple times should fulfill with { value: undefined, done: true }'); + + promise_test(() => { + + const reader = factory().reader; + + return reader.read().then(() => reader.read()).then(v => { + assert_object_equals(v, { value: undefined, done: true }, 'read() should fulfill correctly'); + }); + + }, label + ': read() should work when used within another read() fulfill callback'); + + promise_test(() => { + + const reader = factory().reader; + + return reader.closed.then(v => assert_equals(v, undefined, 'reader closed should fulfill with undefined')); + + }, label + ': closed should fulfill with undefined'); + + promise_test(t => { + + const reader = factory().reader; + + const closedBefore = reader.closed; + reader.releaseLock(); + const closedAfter = reader.closed; + + assert_not_equals(closedBefore, closedAfter, 'the closed promise should change identity'); + + return Promise.all([ + closedBefore.then(v => assert_equals(v, undefined, 'reader.closed acquired before release should fulfill')), + promise_rejects_js(t, TypeError, closedAfter) + ]); + + }, label + ': releasing the lock should cause closed to reject and change identity'); + + promise_test(() => { + + const reader = factory().reader; + const cancelPromise1 = reader.cancel(); + const cancelPromise2 = reader.cancel(); + const closedReaderPromise = reader.closed; + + assert_not_equals(cancelPromise1, cancelPromise2, 'cancel() calls should return distinct promises'); + assert_not_equals(cancelPromise1, closedReaderPromise, 'cancel() promise 1 should be distinct from reader.closed'); + assert_not_equals(cancelPromise2, closedReaderPromise, 'cancel() promise 2 should be distinct from reader.closed'); + + return Promise.all([ + cancelPromise1.then(v => assert_equals(v, undefined, 'first cancel() should fulfill with undefined')), + cancelPromise2.then(v => assert_equals(v, undefined, 'second cancel() should fulfill with undefined')) + ]); + + }, label + ': cancel() should return a distinct fulfilled promise each time'); +}; + +self.templatedRSErroredReader = (label, factory, error) => { + test(() => {}, 'Running templatedRSErroredReader with ' + label); + + promise_test(t => { + + const reader = factory().reader; + return promise_rejects_exactly(t, error, reader.closed); + + }, label + ': closed should reject with the error'); + + promise_test(t => { + + const reader = factory().reader; + const closedBefore = reader.closed; + + return promise_rejects_exactly(t, error, closedBefore).then(() => { + reader.releaseLock(); + + const closedAfter = reader.closed; + assert_not_equals(closedBefore, closedAfter, 'the closed promise should change identity'); + + return promise_rejects_js(t, TypeError, closedAfter); + }); + + }, label + ': releasing the lock should cause closed to reject and change identity'); + + promise_test(t => { + + const reader = factory().reader; + return promise_rejects_exactly(t, error, reader.read()); + + }, label + ': read() should reject with the error'); +}; + +self.templatedRSTwoChunksOpenReader = (label, factory, chunks) => { + test(() => {}, 'Running templatedRSTwoChunksOpenReader with ' + label); + + promise_test(() => { + + const reader = factory().reader; + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct'); + }), + reader.read().then(r => { + assert_object_equals(r, { value: chunks[1], done: false }, 'second result should be correct'); + }) + ]); + + }, label + ': calling read() twice without waiting will eventually give both chunks (sequential)'); + + promise_test(() => { + + const reader = factory().reader; + + return reader.read().then(r => { + assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct'); + + return reader.read().then(r2 => { + assert_object_equals(r2, { value: chunks[1], done: false }, 'second result should be correct'); + }); + }); + + }, label + ': calling read() twice without waiting will eventually give both chunks (nested)'); + + test(() => { + + const reader = factory().reader; + assert_not_equals(reader.read(), reader.read(), 'the promises returned should be distinct'); + + }, label + ': read() should return distinct promises each time'); + + promise_test(() => { + + const reader = factory().reader; + + const promise1 = reader.closed.then(v => { + assert_equals(v, undefined, 'reader closed should fulfill with undefined'); + }); + + const promise2 = reader.read().then(r => { + assert_object_equals(r, { value: chunks[0], done: false }, + 'promise returned before cancellation should fulfill with a chunk'); + }); + + reader.cancel(); + + const promise3 = reader.read().then(r => { + assert_object_equals(r, { value: undefined, done: true }, + 'promise returned after cancellation should fulfill with an end-of-stream signal'); + }); + + return Promise.all([promise1, promise2, promise3]); + + }, label + ': cancel() after a read() should still give that single read result'); +}; + +self.templatedRSTwoChunksClosedReader = function (label, factory, chunks) { + test(() => {}, 'Running templatedRSTwoChunksClosedReader with ' + label); + + promise_test(() => { + + const reader = factory().reader; + + return Promise.all([ + reader.read().then(r => { + assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct'); + }), + reader.read().then(r => { + assert_object_equals(r, { value: chunks[1], done: false }, 'second result should be correct'); + }), + reader.read().then(r => { + assert_object_equals(r, { value: undefined, done: true }, 'third result should be correct'); + }) + ]); + + }, label + ': third read(), without waiting, should give { value: undefined, done: true } (sequential)'); + + promise_test(() => { + + const reader = factory().reader; + + return reader.read().then(r => { + assert_object_equals(r, { value: chunks[0], done: false }, 'first result should be correct'); + + return reader.read().then(r2 => { + assert_object_equals(r2, { value: chunks[1], done: false }, 'second result should be correct'); + + return reader.read().then(r3 => { + assert_object_equals(r3, { value: undefined, done: true }, 'third result should be correct'); + }); + }); + }); + + }, label + ': third read(), without waiting, should give { value: undefined, done: true } (nested)'); + + promise_test(() => { + + const streamAndReader = factory(); + const stream = streamAndReader.stream; + const reader = streamAndReader.reader; + + assert_true(stream.locked, 'stream should start locked'); + + const promise = reader.closed.then(v => { + assert_equals(v, undefined, 'reader closed should fulfill with undefined'); + assert_true(stream.locked, 'stream should remain locked'); + }); + + reader.read(); + reader.read(); + + return promise; + + }, label + + ': draining the stream via read() should cause the reader closed promise to fulfill, but locked stays true'); + + promise_test(() => { + + const streamAndReader = factory(); + const stream = streamAndReader.stream; + const reader = streamAndReader.reader; + + const promise = reader.closed.then(() => { + assert_true(stream.locked, 'the stream should start locked'); + reader.releaseLock(); // Releasing the lock after reader closed should not throw. + assert_false(stream.locked, 'the stream should end unlocked'); + }); + + reader.read(); + reader.read(); + + return promise; + + }, label + ': releasing the lock after the stream is closed should cause locked to become false'); + + promise_test(t => { + + const reader = factory().reader; + + reader.releaseLock(); + + return Promise.all([ + promise_rejects_js(t, TypeError, reader.read()), + promise_rejects_js(t, TypeError, reader.read()), + promise_rejects_js(t, TypeError, reader.read()) + ]); + + }, label + ': releasing the lock should cause further read() calls to reject with a TypeError'); + + promise_test(() => { + + const streamAndReader = factory(); + const stream = streamAndReader.stream; + const reader = streamAndReader.reader; + + const readerClosed = reader.closed; + + assert_equals(reader.closed, readerClosed, 'accessing reader.closed twice in succession gives the same value'); + + const promise = reader.read().then(() => { + assert_equals(reader.closed, readerClosed, 'reader.closed is the same after read() fulfills'); + + reader.releaseLock(); + + assert_equals(reader.closed, readerClosed, 'reader.closed is the same after releasing the lock'); + + const newReader = stream.getReader(); + return newReader.read(); + }); + + assert_equals(reader.closed, readerClosed, 'reader.closed is the same after calling read()'); + + return promise; + + }, label + ': reader\'s closed property always returns the same promise'); +}; + +self.templatedRSTeeCancel = (label, factory) => { + test(() => {}, `Running templatedRSTeeCancel with ${label}`); + + promise_test(async () => { + + const reason1 = new Error('We\'re wanted men.'); + const reason2 = new Error('I have the death sentence on twelve systems.'); + + let resolve; + const promise = new Promise(r => resolve = r); + const rs = factory({ + cancel(reason) { + assert_array_equals(reason, [reason1, reason2], + 'the cancel reason should be an array containing those from the branches'); + resolve(); + } + }); + + const [branch1, branch2] = rs.tee(); + await Promise.all([ + branch1.cancel(reason1), + branch2.cancel(reason2), + promise + ]); + + }, `${label}: canceling both branches should aggregate the cancel reasons into an array`); + + promise_test(async () => { + + const reason1 = new Error('This little one\'s not worth the effort.'); + const reason2 = new Error('Come, let me get you something.'); + + let resolve; + const promise = new Promise(r => resolve = r); + const rs = factory({ + cancel(reason) { + assert_array_equals(reason, [reason1, reason2], + 'the cancel reason should be an array containing those from the branches'); + resolve(); + } + }); + + const [branch1, branch2] = rs.tee(); + await Promise.all([ + branch2.cancel(reason2), + branch1.cancel(reason1), + promise + ]); + + }, `${label}: canceling both branches in reverse order should aggregate the cancel reasons into an array`); + + promise_test(async t => { + + const theError = { name: 'I\'ll be careful.' }; + const rs = factory({ + cancel() { + throw theError; + } + }); + + const [branch1, branch2] = rs.tee(); + await Promise.all([ + promise_rejects_exactly(t, theError, branch1.cancel()), + promise_rejects_exactly(t, theError, branch2.cancel()) + ]); + + }, `${label}: failing to cancel the original stream should cause cancel() to reject on branches`); + + promise_test(async t => { + + const theError = { name: 'You just watch yourself!' }; + let controller; + const stream = factory({ + start(c) { + controller = c; + } + }); + + const [branch1, branch2] = stream.tee(); + controller.error(theError); + + await Promise.all([ + promise_rejects_exactly(t, theError, branch1.cancel()), + promise_rejects_exactly(t, theError, branch2.cancel()) + ]); + + }, `${label}: erroring a teed stream should properly handle canceled branches`); + +}; + +self.templatedRSThrowAfterCloseOrError = (label, factory) => { + test(() => {}, 'Running templatedRSThrowAfterCloseOrError with ' + label); + + const theError = new Error('a unique string'); + + promise_test(async t => { + let controller; + const stream = factory({ + start: t.step_func((c) => { + controller = c; + }) + }); + + controller.close(); + + assert_throws_js(TypeError, () => controller.enqueue(new Uint8Array([1]))); + }, `${label}: enqueue() throws after close()`); + + promise_test(async t => { + let controller; + const stream = factory({ + start: t.step_func((c) => { + controller = c; + }) + }); + + controller.enqueue(new Uint8Array([1])); + controller.close(); + + assert_throws_js(TypeError, () => controller.enqueue(new Uint8Array([2]))); + }, `${label}: enqueue() throws after enqueue() and close()`); + + promise_test(async t => { + let controller; + const stream = factory({ + start: t.step_func((c) => { + controller = c; + }) + }); + + controller.error(theError); + + assert_throws_js(TypeError, () => controller.enqueue(new Uint8Array([1]))); + }, `${label}: enqueue() throws after error()`); + + promise_test(async t => { + let controller; + const stream = factory({ + start: t.step_func((c) => { + controller = c; + }) + }); + + controller.error(theError); + + assert_throws_js(TypeError, () => controller.close()); + }, `${label}: close() throws after error()`); +}; diff --git a/test/js/third_party/wpt-streams/streams/resources/rs-utils.js b/test/js/third_party/wpt-streams/streams/resources/rs-utils.js new file mode 100644 index 000000000000..0f7742a5b3b1 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/resources/rs-utils.js @@ -0,0 +1,226 @@ +'use strict'; +(function () { + // Fake setInterval-like functionality in environments that don't have it + class IntervalHandle { + constructor(callback, delayMs) { + this.callback = callback; + this.delayMs = delayMs; + this.cancelled = false; + Promise.resolve().then(() => this.check()); + } + + async check() { + while (true) { + await new Promise(resolve => step_timeout(resolve, this.delayMs)); + if (this.cancelled) { + return; + } + this.callback(); + } + } + + cancel() { + this.cancelled = true; + } + } + + let localSetInterval, localClearInterval; + if (typeof globalThis.setInterval !== "undefined" && + typeof globalThis.clearInterval !== "undefined") { + localSetInterval = globalThis.setInterval; + localClearInterval = globalThis.clearInterval; + } else { + localSetInterval = function setInterval(callback, delayMs) { + return new IntervalHandle(callback, delayMs); + } + localClearInterval = function clearInterval(handle) { + handle.cancel(); + } + } + + class RandomPushSource { + constructor(toPush) { + this.pushed = 0; + this.toPush = toPush; + this.started = false; + this.paused = false; + this.closed = false; + + this._intervalHandle = null; + } + + readStart() { + if (this.closed) { + return; + } + + if (!this.started) { + this._intervalHandle = localSetInterval(writeChunk, 2); + this.started = true; + } + + if (this.paused) { + this._intervalHandle = localSetInterval(writeChunk, 2); + this.paused = false; + } + + const source = this; + function writeChunk() { + if (source.paused) { + return; + } + + source.pushed++; + + if (source.toPush > 0 && source.pushed > source.toPush) { + if (source._intervalHandle) { + localClearInterval(source._intervalHandle); + source._intervalHandle = undefined; + } + source.closed = true; + source.onend(); + } else { + source.ondata(randomChunk(128)); + } + } + } + + readStop() { + if (this.paused) { + return; + } + + if (this.started) { + this.paused = true; + localClearInterval(this._intervalHandle); + this._intervalHandle = undefined; + } else { + throw new Error('Can\'t pause reading an unstarted source.'); + } + } + } + + function randomChunk(size) { + let chunk = ''; + + for (let i = 0; i < size; ++i) { + // Add a random character from the basic printable ASCII set. + chunk += String.fromCharCode(Math.round(Math.random() * 84) + 32); + } + + return chunk; + } + + function readableStreamToArray(readable, reader) { + if (reader === undefined) { + reader = readable.getReader(); + } + + const chunks = []; + + return pump(); + + function pump() { + return reader.read().then(result => { + if (result.done) { + return chunks; + } + + chunks.push(result.value); + return pump(); + }); + } + } + + class SequentialPullSource { + constructor(limit, options) { + const async = options && options.async; + + this.current = 0; + this.limit = limit; + this.opened = false; + this.closed = false; + + this._exec = f => f(); + if (async) { + this._exec = f => step_timeout(f, 0); + } + } + + open(cb) { + this._exec(() => { + this.opened = true; + cb(); + }); + } + + read(cb) { + this._exec(() => { + if (++this.current <= this.limit) { + cb(null, false, this.current); + } else { + cb(null, true, null); + } + }); + } + + close(cb) { + this._exec(() => { + this.closed = true; + cb(); + }); + } + } + + function sequentialReadableStream(limit, options) { + const sequentialSource = new SequentialPullSource(limit, options); + + const stream = new ReadableStream({ + start() { + return new Promise((resolve, reject) => { + sequentialSource.open(err => { + if (err) { + reject(err); + } + resolve(); + }); + }); + }, + + pull(c) { + return new Promise((resolve, reject) => { + sequentialSource.read((err, done, chunk) => { + if (err) { + reject(err); + } else if (done) { + sequentialSource.close(err2 => { + if (err2) { + reject(err2); + } + c.close(); + resolve(); + }); + } else { + c.enqueue(chunk); + resolve(); + } + }); + }); + } + }); + + stream.source = sequentialSource; + + return stream; + } + + function transferArrayBufferView(view) { + return structuredClone(view, { transfer: [view.buffer] }); + } + + self.RandomPushSource = RandomPushSource; + self.readableStreamToArray = readableStreamToArray; + self.sequentialReadableStream = sequentialReadableStream; + self.transferArrayBufferView = transferArrayBufferView; + +}()); diff --git a/test/js/third_party/wpt-streams/streams/resources/test-utils.js b/test/js/third_party/wpt-streams/streams/resources/test-utils.js new file mode 100644 index 000000000000..a38f78027bf0 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/resources/test-utils.js @@ -0,0 +1,27 @@ +'use strict'; + +self.delay = ms => new Promise(resolve => step_timeout(resolve, ms)); + +// For tests which verify that the implementation doesn't do something it shouldn't, it's better not to use a +// timeout. Instead, assume that any reasonable implementation is going to finish work after 2 times around the event +// loop, and use flushAsyncEvents().then(() => assert_array_equals(...)); +// Some tests include promise resolutions which may mean the test code takes a couple of event loop visits itself. So go +// around an extra 2 times to avoid complicating those tests. +self.flushAsyncEvents = () => delay(0).then(() => delay(0)).then(() => delay(0)).then(() => delay(0)); + +self.assert_typed_array_equals = (actual, expected, message) => { + const prefix = message === undefined ? '' : `${message} `; + assert_equals(typeof actual, 'object', `${prefix}type is object`); + assert_equals(actual.constructor, expected.constructor, `${prefix}constructor`); + assert_equals(actual.byteOffset, expected.byteOffset, `${prefix}byteOffset`); + assert_equals(actual.byteLength, expected.byteLength, `${prefix}byteLength`); + assert_equals(actual.buffer.byteLength, expected.buffer.byteLength, `${prefix}buffer.byteLength`); + assert_array_equals([...actual], [...expected], `${prefix}contents`); + assert_array_equals([...new Uint8Array(actual.buffer)], [...new Uint8Array(expected.buffer)], `${prefix}buffer contents`); +}; + +self.makePromiseAndResolveFunc = () => { + let resolve; + const promise = new Promise(r => { resolve = r; }); + return [promise, resolve]; +}; diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/backpressure.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/backpressure.any.js new file mode 100644 index 000000000000..6befba41b795 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/backpressure.any.js @@ -0,0 +1,195 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +// META: script=../resources/test-utils.js +'use strict'; + +const error1 = new Error('error1 message'); +error1.name = 'error1'; + +promise_test(() => { + const ts = recordingTransformStream(); + const writer = ts.writable.getWriter(); + // This call never resolves. + writer.write('a'); + return flushAsyncEvents().then(() => { + assert_array_equals(ts.events, [], 'transform should not be called'); + }); +}, 'backpressure allows no transforms with a default identity transform and no reader'); + +promise_test(() => { + const ts = recordingTransformStream({}, undefined, { highWaterMark: 1 }); + const writer = ts.writable.getWriter(); + // This call to write() resolves asynchronously. + writer.write('a'); + // This call to write() waits for backpressure that is never relieved and never calls transform(). + writer.write('b'); + return flushAsyncEvents().then(() => { + assert_array_equals(ts.events, ['transform', 'a'], 'transform should be called once'); + }); +}, 'backpressure only allows one transform() with a identity transform with a readable HWM of 1 and no reader'); + +promise_test(() => { + // Without a transform() implementation, recordingTransformStream() never enqueues anything. + const ts = recordingTransformStream({ + transform() { + // Discard all chunks. As a result, the readable side is never full enough to exert backpressure and transform() + // keeps being called. + } + }, undefined, { highWaterMark: 1 }); + const writer = ts.writable.getWriter(); + const writePromises = []; + for (let i = 0; i < 4; ++i) { + writePromises.push(writer.write(i)); + } + return Promise.all(writePromises).then(() => { + assert_array_equals(ts.events, ['transform', 0, 'transform', 1, 'transform', 2, 'transform', 3], + 'all 4 events should be transformed'); + }); +}, 'transform() should keep being called as long as there is no backpressure'); + +promise_test(() => { + const ts = new TransformStream({}, undefined, { highWaterMark: 1 }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + const events = []; + const writerPromises = [ + writer.write('a').then(() => events.push('a')), + writer.write('b').then(() => events.push('b')), + writer.close().then(() => events.push('closed'))]; + return delay(0).then(() => { + assert_array_equals(events, ['a'], 'the first write should have resolved'); + return reader.read(); + }).then(({ value, done }) => { + assert_false(done, 'done should not be true'); + assert_equals('a', value, 'value should be "a"'); + return delay(0); + }).then(() => { + assert_array_equals(events, ['a', 'b', 'closed'], 'both writes and close() should have resolved'); + return reader.read(); + }).then(({ value, done }) => { + assert_false(done, 'done should still not be true'); + assert_equals('b', value, 'value should be "b"'); + return reader.read(); + }).then(({ done }) => { + assert_true(done, 'done should be true'); + return writerPromises; + }); +}, 'writes should resolve as soon as transform completes'); + +promise_test(() => { + const ts = new TransformStream(undefined, undefined, { highWaterMark: 0 }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + const readPromise = reader.read(); + writer.write('a'); + return readPromise.then(({ value, done }) => { + assert_false(done, 'not done'); + assert_equals(value, 'a', 'value should be "a"'); + }); +}, 'calling pull() before the first write() with backpressure should work'); + +promise_test(() => { + let reader; + const ts = recordingTransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk); + return reader.read(); + } + }, undefined, { highWaterMark: 1 }); + const writer = ts.writable.getWriter(); + reader = ts.readable.getReader(); + return writer.write('a'); +}, 'transform() should be able to read the chunk it just enqueued'); + +promise_test(() => { + let resolveTransform; + const transformPromise = new Promise(resolve => { + resolveTransform = resolve; + }); + const ts = recordingTransformStream({ + transform() { + return transformPromise; + } + }, undefined, new CountQueuingStrategy({ highWaterMark: Infinity })); + const writer = ts.writable.getWriter(); + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); + return delay(0).then(() => { + writer.write('a'); + assert_array_equals(ts.events, ['transform', 'a']); + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0'); + return flushAsyncEvents(); + }).then(() => { + assert_equals(writer.desiredSize, 0, 'desiredSize should still be 0'); + resolveTransform(); + return delay(0); + }).then(() => { + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); + }); +}, 'blocking transform() should cause backpressure'); + +promise_test(t => { + const ts = new TransformStream(); + ts.readable.cancel(error1); + return promise_rejects_exactly(t, error1, ts.writable.getWriter().closed, 'closed should reject'); +}, 'writer.closed should resolve after readable is canceled during start'); + +promise_test(t => { + const ts = new TransformStream({}, undefined, { highWaterMark: 0 }); + return delay(0).then(() => { + ts.readable.cancel(error1); + return promise_rejects_exactly(t, error1, ts.writable.getWriter().closed, 'closed should reject'); + }); +}, 'writer.closed should resolve after readable is canceled with backpressure'); + +promise_test(t => { + const ts = new TransformStream({}, undefined, { highWaterMark: 1 }); + return delay(0).then(() => { + ts.readable.cancel(error1); + return promise_rejects_exactly(t, error1, ts.writable.getWriter().closed, 'closed should reject'); + }); +}, 'writer.closed should resolve after readable is canceled with no backpressure'); + +promise_test(() => { + const ts = new TransformStream({}, undefined, { highWaterMark: 1 }); + const writer = ts.writable.getWriter(); + return delay(0).then(() => { + const writePromise = writer.write('a'); + ts.readable.cancel(error1); + return writePromise; + }); +}, 'cancelling the readable should cause a pending write to resolve'); + +promise_test(t => { + const rs = new ReadableStream(); + const ts = new TransformStream(); + const pipePromise = rs.pipeTo(ts.writable); + ts.readable.cancel(error1); + return promise_rejects_exactly(t, error1, pipePromise, 'promise returned from pipeTo() should be rejected'); +}, 'cancelling the readable side of a TransformStream should abort an empty pipe'); + +promise_test(t => { + const rs = new ReadableStream(); + const ts = new TransformStream(); + const pipePromise = rs.pipeTo(ts.writable); + return delay(0).then(() => { + ts.readable.cancel(error1); + return promise_rejects_exactly(t, error1, pipePromise, 'promise returned from pipeTo() should be rejected'); + }); +}, 'cancelling the readable side of a TransformStream should abort an empty pipe after startup'); + +promise_test(t => { + const rs = new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.enqueue('b'); + controller.enqueue('c'); + } + }); + const ts = new TransformStream(); + const pipePromise = rs.pipeTo(ts.writable); + // Allow data to flow into the pipe. + return delay(0).then(() => { + ts.readable.cancel(error1); + return promise_rejects_exactly(t, error1, pipePromise, 'promise returned from pipeTo() should be rejected'); + }); +}, 'cancelling the readable side of a TransformStream should abort a full pipe'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/cancel.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/cancel.any.js new file mode 100644 index 000000000000..fc5ef9570404 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/cancel.any.js @@ -0,0 +1,205 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +'use strict'; + +const thrownError = new Error('bad things are happening!'); +thrownError.name = 'error1'; + +const originalReason = new Error('original reason'); +originalReason.name = 'error2'; + +promise_test(async t => { + let cancelled = undefined; + const ts = new TransformStream({ + cancel(reason) { + cancelled = reason; + } + }); + const res = await ts.readable.cancel(thrownError); + assert_equals(res, undefined, 'readable.cancel() should return undefined'); + assert_equals(cancelled, thrownError, 'transformer.cancel() should be called with the passed reason'); +}, 'cancelling the readable side should call transformer.cancel()'); + +promise_test(async t => { + const ts = new TransformStream({ + cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + throw thrownError; + } + }); + const writer = ts.writable.getWriter(); + const cancelPromise = ts.readable.cancel(originalReason); + await promise_rejects_exactly(t, thrownError, cancelPromise, 'readable.cancel() should reject with thrownError'); + await promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject with thrownError'); +}, 'cancelling the readable side should reject if transformer.cancel() throws'); + +promise_test(async t => { + let aborted = undefined; + const ts = new TransformStream({ + cancel(reason) { + aborted = reason; + }, + flush: t.unreached_func('flush should not be called') + }); + const res = await ts.writable.abort(thrownError); + assert_equals(res, undefined, 'writable.abort() should return undefined'); + assert_equals(aborted, thrownError, 'transformer.abort() should be called with the passed reason'); +}, 'aborting the writable side should call transformer.abort()'); + +promise_test(async t => { + const ts = new TransformStream({ + cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + throw thrownError; + }, + flush: t.unreached_func('flush should not be called') + }); + const reader = ts.readable.getReader(); + const abortPromise = ts.writable.abort(originalReason); + await promise_rejects_exactly(t, thrownError, abortPromise, 'writable.abort() should reject with thrownError'); + await promise_rejects_exactly(t, thrownError, reader.closed, 'reader.closed should reject with thrownError'); +}, 'aborting the writable side should reject if transformer.cancel() throws'); + +promise_test(async t => { + const ts = new TransformStream({ + async cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + throw thrownError; + }, + flush: t.unreached_func('flush should not be called') + }); + const cancelPromise = ts.readable.cancel(originalReason); + const closePromise = ts.writable.close(); + await Promise.all([ + promise_rejects_exactly(t, thrownError, cancelPromise, 'cancelPromise should reject with thrownError'), + promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject with thrownError'), + ]); +}, 'closing the writable side should reject if a parallel transformer.cancel() throws'); + +promise_test(async t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + async cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + controller.error(thrownError); + }, + flush: t.unreached_func('flush should not be called') + }); + const cancelPromise = ts.readable.cancel(originalReason); + const closePromise = ts.writable.close(); + await Promise.all([ + promise_rejects_exactly(t, thrownError, cancelPromise, 'cancelPromise should reject with thrownError'), + promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject with thrownError'), + ]); +}, 'readable.cancel() and a parallel writable.close() should reject if a transformer.cancel() calls controller.error()'); + +promise_test(async t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + async cancel(reason) { + assert_equals(reason, originalReason, 'transformer.cancel() should be called with the passed reason'); + controller.error(thrownError); + }, + flush: t.unreached_func('flush should not be called') + }); + const cancelPromise = ts.writable.abort(originalReason); + await promise_rejects_exactly(t, thrownError, cancelPromise, 'cancelPromise should reject with thrownError'); + const closePromise = ts.readable.cancel(1); + await promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject with thrownError'); +}, 'writable.abort() and readable.cancel() should reject if a transformer.cancel() calls controller.error()'); + +promise_test(async t => { + const cancelReason = new Error('cancel reason'); + let controller; + let cancelPromise; + let flushCalled = false; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + flush() { + flushCalled = true; + cancelPromise = ts.readable.cancel(cancelReason); + }, + cancel: t.unreached_func('cancel should not be called') + }); + await flushAsyncEvents(); // ensure stream is started + await ts.writable.close(); + assert_true(flushCalled, 'flush() was called'); + await cancelPromise; +}, 'readable.cancel() should not call cancel() when flush() is already called from writable.close()'); + +promise_test(async t => { + const cancelReason = new Error('cancel reason'); + const abortReason = new Error('abort reason'); + let cancelCalls = 0; + let controller; + let cancelPromise; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + cancel() { + if (++cancelCalls === 1) { + cancelPromise = ts.readable.cancel(cancelReason); + } + }, + flush: t.unreached_func('flush should not be called') + }); + await flushAsyncEvents(); // ensure stream is started + await ts.writable.abort(abortReason); + assert_equals(cancelCalls, 1); + await cancelPromise; + assert_equals(cancelCalls, 1); +}, 'readable.cancel() should not call cancel() again when already called from writable.abort()'); + +promise_test(async t => { + const cancelReason = new Error('cancel reason'); + let controller; + let closePromise; + let cancelCalled = false; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + cancel() { + cancelCalled = true; + closePromise = ts.writable.close(); + }, + flush: t.unreached_func('flush should not be called') + }); + await flushAsyncEvents(); // ensure stream is started + await ts.readable.cancel(cancelReason); + assert_true(cancelCalled, 'cancel() was called'); + await closePromise; +}, 'writable.close() should not call flush() when cancel() is already called from readable.cancel()'); + +promise_test(async t => { + const cancelReason = new Error('cancel reason'); + const abortReason = new Error('abort reason'); + let cancelCalls = 0; + let controller; + let abortPromise; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + cancel() { + if (++cancelCalls === 1) { + abortPromise = ts.writable.abort(abortReason); + } + }, + flush: t.unreached_func('flush should not be called') + }); + await flushAsyncEvents(); // ensure stream is started + await promise_rejects_exactly(t, abortReason, ts.readable.cancel(cancelReason)); + assert_equals(cancelCalls, 1); + await promise_rejects_exactly(t, abortReason, abortPromise); + assert_equals(cancelCalls, 1); +}, 'writable.abort() should not call cancel() again when already called from readable.cancel()'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/errors.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/errors.any.js new file mode 100644 index 000000000000..7efe894f4887 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/errors.any.js @@ -0,0 +1,360 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +'use strict'; + +const thrownError = new Error('bad things are happening!'); +thrownError.name = 'error1'; + +promise_test(t => { + const ts = new TransformStream({ + transform() { + throw thrownError; + }, + cancel: t.unreached_func('cancel should not be called') + }); + + const reader = ts.readable.getReader(); + + const writer = ts.writable.getWriter(); + + return Promise.all([ + promise_rejects_exactly(t, thrownError, writer.write('a'), + 'writable\'s write should reject with the thrown error'), + promise_rejects_exactly(t, thrownError, reader.read(), + 'readable\'s read should reject with the thrown error'), + promise_rejects_exactly(t, thrownError, reader.closed, + 'readable\'s closed should be rejected with the thrown error'), + promise_rejects_exactly(t, thrownError, writer.closed, + 'writable\'s closed should be rejected with the thrown error') + ]); +}, 'TransformStream errors thrown in transform put the writable and readable in an errored state'); + +promise_test(t => { + const ts = new TransformStream({ + transform() { + }, + flush() { + throw thrownError; + }, + cancel: t.unreached_func('cancel should not be called') + }); + + const reader = ts.readable.getReader(); + + const writer = ts.writable.getWriter(); + + return Promise.all([ + writer.write('a'), + promise_rejects_exactly(t, thrownError, writer.close(), + 'writable\'s close should reject with the thrown error'), + promise_rejects_exactly(t, thrownError, reader.read(), + 'readable\'s read should reject with the thrown error'), + promise_rejects_exactly(t, thrownError, reader.closed, + 'readable\'s closed should be rejected with the thrown error'), + promise_rejects_exactly(t, thrownError, writer.closed, + 'writable\'s closed should be rejected with the thrown error') + ]); +}, 'TransformStream errors thrown in flush put the writable and readable in an errored state'); + +test(t => { + new TransformStream({ + start(c) { + c.enqueue('a'); + c.error(new Error('generic error')); + assert_throws_js(TypeError, () => c.enqueue('b'), 'enqueue() should throw'); + }, + cancel: t.unreached_func('cancel should not be called') + }); +}, 'errored TransformStream should not enqueue new chunks'); + +promise_test(t => { + const ts = new TransformStream({ + start() { + return flushAsyncEvents().then(() => { + throw thrownError; + }); + }, + transform: t.unreached_func('transform should not be called'), + flush: t.unreached_func('flush should not be called'), + cancel: t.unreached_func('cancel should not be called') + }); + + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + return Promise.all([ + promise_rejects_exactly(t, thrownError, writer.write('a'), 'writer should reject with thrownError'), + promise_rejects_exactly(t, thrownError, writer.close(), 'close() should reject with thrownError'), + promise_rejects_exactly(t, thrownError, reader.read(), 'reader should reject with thrownError') + ]); +}, 'TransformStream transformer.start() rejected promise should error the stream'); + +promise_test(t => { + const controllerError = new Error('start failure'); + controllerError.name = 'controllerError'; + const ts = new TransformStream({ + start(c) { + return flushAsyncEvents() + .then(() => { + c.error(controllerError); + throw new Error('ignored error'); + }); + }, + transform: t.unreached_func('transform should never be called if start() fails'), + flush: t.unreached_func('flush should never be called if start() fails'), + cancel: t.unreached_func('cancel should never be called if start() fails') + }); + + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + return Promise.all([ + promise_rejects_exactly(t, controllerError, writer.write('a'), 'writer should reject with controllerError'), + promise_rejects_exactly(t, controllerError, writer.close(), 'close should reject with same error'), + promise_rejects_exactly(t, controllerError, reader.read(), 'reader should reject with same error') + ]); +}, 'when controller.error is followed by a rejection, the error reason should come from controller.error'); + +test(() => { + assert_throws_js(URIError, () => new TransformStream({ + start() { throw new URIError('start thrown error'); }, + transform() {} + }), 'constructor should throw'); +}, 'TransformStream constructor should throw when start does'); + +test(() => { + const strategy = { + size() { throw new URIError('size thrown error'); } + }; + + assert_throws_js(URIError, () => new TransformStream({ + start(c) { + c.enqueue('a'); + }, + transform() {} + }, undefined, strategy), 'constructor should throw the same error strategy.size throws'); +}, 'when strategy.size throws inside start(), the constructor should throw the same error'); + +test(() => { + const controllerError = new URIError('controller.error'); + + let controller; + const strategy = { + size() { + controller.error(controllerError); + throw new Error('redundant error'); + } + }; + + assert_throws_js(URIError, () => new TransformStream({ + start(c) { + controller = c; + c.enqueue('a'); + }, + transform() {} + }, undefined, strategy), 'the first error should be thrown'); +}, 'when strategy.size calls controller.error() then throws, the constructor should throw the first error'); + +promise_test(t => { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + const closedPromise = writer.closed; + return Promise.all([ + ts.readable.cancel(thrownError), + promise_rejects_exactly(t, thrownError, closedPromise, 'closed should throw a TypeError') + ]); +}, 'cancelling the readable side should error the writable'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + const writePromise = writer.write('a'); + const closePromise = writer.close(); + controller.error(thrownError); + return Promise.all([ + promise_rejects_exactly(t, thrownError, reader.closed, 'reader.closed should reject'), + promise_rejects_exactly(t, thrownError, writePromise, 'writePromise should reject'), + promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject')]); +}, 'it should be possible to error the readable between close requested and complete'); + +promise_test(t => { + const ts = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk); + controller.terminate(); + throw thrownError; + } + }, undefined, { highWaterMark: 1 }); + const writePromise = ts.writable.getWriter().write('a'); + const closedPromise = ts.readable.getReader().closed; + return Promise.all([ + promise_rejects_exactly(t, thrownError, writePromise, 'write() should reject'), + promise_rejects_exactly(t, thrownError, closedPromise, 'reader.closed should reject') + ]); +}, 'an exception from transform() should error the stream if terminate has been requested but not completed'); + +promise_test(t => { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + // The microtask following transformer.start() hasn't completed yet, so the abort is queued and not notified to the + // TransformStream yet. + const abortPromise = writer.abort(thrownError); + const cancelPromise = ts.readable.cancel(new Error('cancel reason')); + return Promise.all([ + abortPromise, + cancelPromise, + promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject'), + ]); +}, 'abort should set the close reason for the writable when it happens before cancel during start, and cancel should ' + + 'reject'); + +promise_test(t => { + let resolveTransform; + const transformPromise = new Promise(resolve => { + resolveTransform = resolve; + }); + const ts = new TransformStream({ + transform() { + return transformPromise; + } + }, undefined, { highWaterMark: 2 }); + const writer = ts.writable.getWriter(); + return delay(0).then(() => { + const writePromise = writer.write(); + const abortPromise = writer.abort(thrownError); + const cancelPromise = ts.readable.cancel(new Error('cancel reason')); + resolveTransform(); + return Promise.all([ + writePromise, + abortPromise, + cancelPromise, + promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject with thrownError')]); + }); +}, 'abort should set the close reason for the writable when it happens before cancel during underlying sink write, ' + + 'but cancel should still succeed'); + +const ignoredError = new Error('ignoredError'); +ignoredError.name = 'ignoredError'; + +promise_test(t => { + const ts = new TransformStream({ + start(controller) { + controller.error(thrownError); + controller.error(ignoredError); + } + }); + return promise_rejects_exactly(t, thrownError, ts.writable.abort(), 'abort() should reject with thrownError'); +}, 'controller.error() should do nothing the second time it is called'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + const cancelPromise = ts.readable.cancel(ignoredError); + controller.error(thrownError); + return Promise.all([ + cancelPromise, + promise_rejects_exactly(t, thrownError, ts.writable.getWriter().closed, 'closed should reject with thrownError') + ]); +}, 'controller.error() should close writable immediately after readable.cancel()'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + return ts.readable.cancel(thrownError).then(() => { + controller.error(ignoredError); + return promise_rejects_exactly(t, thrownError, ts.writable.getWriter().closed, 'closed should reject with thrownError'); + }); +}, 'controller.error() should do nothing after readable.cancel() resolves'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + return ts.writable.abort(thrownError).then(() => { + controller.error(ignoredError); + return promise_rejects_exactly(t, thrownError, ts.writable.getWriter().closed, 'closed should reject with thrownError'); + }); +}, 'controller.error() should do nothing after writable.abort() has completed'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + transform() { + throw thrownError; + } + }, undefined, { highWaterMark: Infinity }); + const writer = ts.writable.getWriter(); + return promise_rejects_exactly(t, thrownError, writer.write(), 'write() should reject').then(() => { + controller.error(); + return promise_rejects_exactly(t, thrownError, writer.closed, 'closed should reject with thrownError'); + }); +}, 'controller.error() should do nothing after a transformer method has thrown an exception'); + +promise_test(t => { + let controller; + let calls = 0; + const ts = new TransformStream({ + start(c) { + controller = c; + }, + transform() { + ++calls; + } + }, undefined, { highWaterMark: 1 }); + return delay(0).then(() => { + // Create backpressure. + controller.enqueue('a'); + const writer = ts.writable.getWriter(); + // transform() will not be called until backpressure is relieved. + const writePromise = writer.write('b'); + assert_equals(calls, 0, 'transform() should not have been called'); + controller.error(thrownError); + // Now backpressure has been relieved and the write can proceed. + return promise_rejects_exactly(t, thrownError, writePromise, 'write() should reject').then(() => { + assert_equals(calls, 0, 'transform() should not be called'); + }); + }); +}, 'erroring during write with backpressure should result in the write failing'); + +promise_test(t => { + const ts = new TransformStream({}, undefined, { highWaterMark: 0 }); + return delay(0).then(() => { + const writer = ts.writable.getWriter(); + // write should start synchronously + const writePromise = writer.write(0); + // The underlying sink's abort() is not called until the write() completes. + const abortPromise = writer.abort(thrownError); + // Perform a read to relieve backpressure and permit the write() to complete. + const readPromise = ts.readable.getReader().read(); + return Promise.all([ + promise_rejects_exactly(t, thrownError, readPromise, 'read() should reject'), + promise_rejects_exactly(t, thrownError, writePromise, 'write() should reject'), + abortPromise + ]); + }); +}, 'a write() that was waiting for backpressure should reject if the writable is aborted'); + +promise_test(t => { + const ts = new TransformStream(); + ts.writable.abort(thrownError); + const reader = ts.readable.getReader(); + return promise_rejects_exactly(t, thrownError, reader.read(), 'read() should reject with thrownError'); +}, 'the readable should be errored with the reason passed to the writable abort() method'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/flush.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/flush.any.js new file mode 100644 index 000000000000..487de1c93b0f --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/flush.any.js @@ -0,0 +1,146 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +'use strict'; + +promise_test(() => { + let flushCalled = false; + const ts = new TransformStream({ + transform() { }, + flush() { + flushCalled = true; + } + }); + + return ts.writable.getWriter().close().then(() => { + return assert_true(flushCalled, 'closing the writable triggers the transform flush immediately'); + }); +}, 'TransformStream flush is called immediately when the writable is closed, if no writes are queued'); + +promise_test(() => { + let flushCalled = false; + let resolveTransform; + const ts = new TransformStream({ + transform() { + return new Promise(resolve => { + resolveTransform = resolve; + }); + }, + flush() { + flushCalled = true; + return new Promise(() => {}); // never resolves + } + }, undefined, { highWaterMark: 1 }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + assert_false(flushCalled, 'closing the writable does not immediately call flush if writes are not finished'); + + let rsClosed = false; + ts.readable.getReader().closed.then(() => { + rsClosed = true; + }); + + return delay(0).then(() => { + assert_false(flushCalled, 'closing the writable does not asynchronously call flush if writes are not finished'); + resolveTransform(); + return delay(0); + }).then(() => { + assert_true(flushCalled, 'flush is eventually called'); + assert_false(rsClosed, 'if flushPromise does not resolve, the readable does not become closed'); + }); +}, 'TransformStream flush is called after all queued writes finish, once the writable is closed'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + start(controller) { + c = controller; + }, + transform() { + }, + flush() { + c.enqueue('x'); + c.enqueue('y'); + } + }); + + const reader = ts.readable.getReader(); + + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + return reader.read().then(result1 => { + assert_equals(result1.value, 'x', 'the first chunk read is the first one enqueued in flush'); + assert_equals(result1.done, false, 'the first chunk read is the first one enqueued in flush'); + + return reader.read().then(result2 => { + assert_equals(result2.value, 'y', 'the second chunk read is the second one enqueued in flush'); + assert_equals(result2.done, false, 'the second chunk read is the second one enqueued in flush'); + }); + }); +}, 'TransformStream flush gets a chance to enqueue more into the readable'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + start(controller) { + c = controller; + }, + transform() { + }, + flush() { + c.enqueue('x'); + c.enqueue('y'); + return delay(0); + } + }); + + const reader = ts.readable.getReader(); + + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + + return Promise.all([ + reader.read().then(result1 => { + assert_equals(result1.value, 'x', 'the first chunk read is the first one enqueued in flush'); + assert_equals(result1.done, false, 'the first chunk read is the first one enqueued in flush'); + + return reader.read().then(result2 => { + assert_equals(result2.value, 'y', 'the second chunk read is the second one enqueued in flush'); + assert_equals(result2.done, false, 'the second chunk read is the second one enqueued in flush'); + }); + }), + reader.closed.then(() => { + assert_true(true, 'readable reader becomes closed'); + }) + ]); +}, 'TransformStream flush gets a chance to enqueue more into the readable, and can then async close'); + +const error1 = new Error('error1'); +error1.name = 'error1'; + +promise_test(t => { + const ts = new TransformStream({ + flush(controller) { + controller.error(error1); + } + }); + return promise_rejects_exactly(t, error1, ts.writable.getWriter().close(), 'close() should reject'); +}, 'error() during flush should cause writer.close() to reject'); + +promise_test(async t => { + let flushed = false; + const ts = new TransformStream({ + flush() { + flushed = true; + }, + cancel: t.unreached_func('cancel should not be called') + }); + const closePromise = ts.writable.close(); + await delay(0); + const cancelPromise = ts.readable.cancel(error1); + await Promise.all([closePromise, cancelPromise]); + assert_equals(flushed, true, 'transformer.flush() should be called'); +}, 'closing the writable side should call transformer.flush() and a parallel readable.cancel() should not reject'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/general.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/general.any.js new file mode 100644 index 000000000000..dff2e7e8a70d --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/general.any.js @@ -0,0 +1,452 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/rs-utils.js +'use strict'; + +test(() => { + new TransformStream({ transform() { } }); +}, 'TransformStream can be constructed with a transform function'); + +test(() => { + new TransformStream(); + new TransformStream({}); +}, 'TransformStream can be constructed with no transform function'); + +test(() => { + const ts = new TransformStream({ transform() { } }); + + const writer = ts.writable.getWriter(); + assert_equals(writer.desiredSize, 1, 'writer.desiredSize should be 1'); +}, 'TransformStream writable starts in the writable state'); + +promise_test(() => { + const ts = new TransformStream(); + + const writer = ts.writable.getWriter(); + writer.write('a'); + assert_equals(writer.desiredSize, 0, 'writer.desiredSize should be 0 after write()'); + + return ts.readable.getReader().read().then(result => { + assert_equals(result.value, 'a', + 'result from reading the readable is the same as was written to writable'); + assert_false(result.done, 'stream should not be done'); + + return delay(0).then(() => assert_equals(writer.desiredSize, 1, 'desiredSize should be 1 again')); + }); +}, 'Identity TransformStream: can read from readable what is put into writable'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + start(controller) { + c = controller; + }, + transform(chunk) { + c.enqueue(chunk.toUpperCase()); + } + }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + + return ts.readable.getReader().read().then(result => { + assert_equals(result.value, 'A', + 'result from reading the readable is the transformation of what was written to writable'); + assert_false(result.done, 'stream should not be done'); + }); +}, 'Uppercaser sync TransformStream: can read from readable transformed version of what is put into writable'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + start(controller) { + c = controller; + }, + transform(chunk) { + c.enqueue(chunk.toUpperCase()); + c.enqueue(chunk.toUpperCase()); + } + }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + + const reader = ts.readable.getReader(); + + return reader.read().then(result1 => { + assert_equals(result1.value, 'A', + 'the first chunk read is the transformation of the single chunk written'); + assert_false(result1.done, 'stream should not be done'); + + return reader.read().then(result2 => { + assert_equals(result2.value, 'A', + 'the second chunk read is also the transformation of the single chunk written'); + assert_false(result2.done, 'stream should not be done'); + }); + }); +}, 'Uppercaser-doubler sync TransformStream: can read both chunks put into the readable'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + start(controller) { + c = controller; + }, + transform(chunk) { + return delay(0).then(() => c.enqueue(chunk.toUpperCase())); + } + }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + + return ts.readable.getReader().read().then(result => { + assert_equals(result.value, 'A', + 'result from reading the readable is the transformation of what was written to writable'); + assert_false(result.done, 'stream should not be done'); + }); +}, 'Uppercaser async TransformStream: can read from readable transformed version of what is put into writable'); + +promise_test(() => { + let doSecondEnqueue; + let returnFromTransform; + const ts = new TransformStream({ + transform(chunk, controller) { + delay(0).then(() => controller.enqueue(chunk.toUpperCase())); + doSecondEnqueue = () => controller.enqueue(chunk.toUpperCase()); + return new Promise(resolve => { + returnFromTransform = resolve; + }); + } + }); + + const reader = ts.readable.getReader(); + + const writer = ts.writable.getWriter(); + writer.write('a'); + + return reader.read().then(result1 => { + assert_equals(result1.value, 'A', + 'the first chunk read is the transformation of the single chunk written'); + assert_false(result1.done, 'stream should not be done'); + doSecondEnqueue(); + + return reader.read().then(result2 => { + assert_equals(result2.value, 'A', + 'the second chunk read is also the transformation of the single chunk written'); + assert_false(result2.done, 'stream should not be done'); + returnFromTransform(); + }); + }); +}, 'Uppercaser-doubler async TransformStream: can read both chunks put into the readable'); + +promise_test(() => { + const ts = new TransformStream({ transform() { } }); + + const writer = ts.writable.getWriter(); + writer.close(); + + return Promise.all([writer.closed, ts.readable.getReader().closed]); +}, 'TransformStream: by default, closing the writable closes the readable (when there are no queued writes)'); + +promise_test(() => { + let transformResolve; + const transformPromise = new Promise(resolve => { + transformResolve = resolve; + }); + const ts = new TransformStream({ + transform() { + return transformPromise; + } + }, undefined, { highWaterMark: 1 }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + + let rsClosed = false; + ts.readable.getReader().closed.then(() => { + rsClosed = true; + }); + + return delay(0).then(() => { + assert_equals(rsClosed, false, 'readable is not closed after a tick'); + transformResolve(); + + return writer.closed.then(() => { + // TODO: Is this expectation correct? + assert_equals(rsClosed, true, 'readable is closed at that point'); + }); + }); +}, 'TransformStream: by default, closing the writable waits for transforms to finish before closing both'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + start(controller) { + c = controller; + }, + transform() { + c.enqueue('x'); + c.enqueue('y'); + return delay(0); + } + }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + return writer.closed.then(() => { + return readableChunks.then(chunks => { + assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable'); + }); + }); +}, 'TransformStream: by default, closing the writable closes the readable after sync enqueues and async done'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + start(controller) { + c = controller; + }, + transform() { + return delay(0) + .then(() => c.enqueue('x')) + .then(() => c.enqueue('y')) + .then(() => delay(0)); + } + }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + return writer.closed.then(() => { + return readableChunks.then(chunks => { + assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable'); + }); + }); +}, 'TransformStream: by default, closing the writable closes the readable after async enqueues and async done'); + +promise_test(() => { + let c; + const ts = new TransformStream({ + suffix: '-suffix', + + start(controller) { + c = controller; + c.enqueue('start' + this.suffix); + }, + + transform(chunk) { + c.enqueue(chunk + this.suffix); + }, + + flush() { + c.enqueue('flushed' + this.suffix); + } + }); + + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + return writer.closed.then(() => { + return readableChunks.then(chunks => { + assert_array_equals(chunks, ['start-suffix', 'a-suffix', 'flushed-suffix'], 'all enqueued chunks have suffixes'); + }); + }); +}, 'Transform stream should call transformer methods as methods'); + +promise_test(() => { + function functionWithOverloads() {} + functionWithOverloads.apply = () => assert_unreached('apply() should not be called'); + functionWithOverloads.call = () => assert_unreached('call() should not be called'); + const ts = new TransformStream({ + start: functionWithOverloads, + transform: functionWithOverloads, + flush: functionWithOverloads + }); + const writer = ts.writable.getWriter(); + writer.write('a'); + writer.close(); + + return readableStreamToArray(ts.readable); +}, 'methods should not not have .apply() or .call() called'); + +promise_test(t => { + let startCalled = false; + let startDone = false; + let transformDone = false; + let flushDone = false; + const ts = new TransformStream({ + start() { + startCalled = true; + return flushAsyncEvents().then(() => { + startDone = true; + }); + }, + transform() { + return t.step(() => { + assert_true(startDone, 'transform() should not be called until the promise returned from start() has resolved'); + return flushAsyncEvents().then(() => { + transformDone = true; + }); + }); + }, + flush() { + return t.step(() => { + assert_true(transformDone, + 'flush() should not be called until the promise returned from transform() has resolved'); + return flushAsyncEvents().then(() => { + flushDone = true; + }); + }); + } + }, undefined, { highWaterMark: 1 }); + + assert_true(startCalled, 'start() should be called synchronously'); + + const writer = ts.writable.getWriter(); + const writePromise = writer.write('a'); + return writer.close().then(() => { + assert_true(flushDone, 'promise returned from flush() should have resolved'); + return writePromise; + }); +}, 'TransformStream start, transform, and flush should be strictly ordered'); + +promise_test(() => { + let transformCalled = false; + const ts = new TransformStream({ + transform() { + transformCalled = true; + } + }, undefined, { highWaterMark: Infinity }); + // transform() is only called synchronously when there is no backpressure and all microtasks have run. + return delay(0).then(() => { + const writePromise = ts.writable.getWriter().write(); + assert_true(transformCalled, 'transform() should have been called'); + return writePromise; + }); +}, 'it should be possible to call transform() synchronously'); + +promise_test(() => { + const ts = new TransformStream({}, undefined, { highWaterMark: 0 }); + + const writer = ts.writable.getWriter(); + writer.close(); + + return Promise.all([writer.closed, ts.readable.getReader().closed]); +}, 'closing the writable should close the readable when there are no queued chunks, even with backpressure'); + +test(() => { + new TransformStream({ + start(controller) { + controller.terminate(); + assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue should throw'); + } + }); +}, 'enqueue() should throw after controller.terminate()'); + +promise_test(() => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + const cancelPromise = ts.readable.cancel(); + assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue should throw'); + return cancelPromise; +}, 'enqueue() should throw after readable.cancel()'); + +test(() => { + new TransformStream({ + start(controller) { + controller.terminate(); + controller.terminate(); + } + }); +}, 'controller.terminate() should do nothing the second time it is called'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + const cancelReason = { name: 'cancelReason' }; + const cancelPromise = ts.readable.cancel(cancelReason); + controller.terminate(); + return Promise.all([ + cancelPromise, + promise_rejects_js(t, TypeError, ts.writable.getWriter().closed, 'closed should reject with TypeError') + ]); +}, 'terminate() should abort writable immediately after readable.cancel()'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }); + const cancelReason = { name: 'cancelReason' }; + return ts.readable.cancel(cancelReason).then(() => { + controller.terminate(); + return promise_rejects_exactly(t, cancelReason, ts.writable.getWriter().closed, 'closed should reject with TypeError'); + }) +}, 'terminate() should do nothing after readable.cancel() resolves'); + + +promise_test(() => { + let calls = 0; + new TransformStream({ + start() { + ++calls; + } + }); + return flushAsyncEvents().then(() => { + assert_equals(calls, 1, 'start() should have been called exactly once'); + }); +}, 'start() should not be called twice'); + +test(() => { + assert_throws_js(RangeError, () => new TransformStream({ readableType: 'bytes' }), 'constructor should throw'); +}, 'specifying a defined readableType should throw'); + +test(() => { + assert_throws_js(RangeError, () => new TransformStream({ writableType: 'bytes' }), 'constructor should throw'); +}, 'specifying a defined writableType should throw'); + +test(() => { + class Subclass extends TransformStream { + extraFunction() { + return true; + } + } + assert_equals( + Object.getPrototypeOf(Subclass.prototype), TransformStream.prototype, + 'Subclass.prototype\'s prototype should be TransformStream.prototype'); + assert_equals(Object.getPrototypeOf(Subclass), TransformStream, + 'Subclass\'s prototype should be TransformStream'); + const sub = new Subclass(); + assert_true(sub instanceof TransformStream, + 'Subclass object should be an instance of TransformStream'); + assert_true(sub instanceof Subclass, + 'Subclass object should be an instance of Subclass'); + const readableGetter = Object.getOwnPropertyDescriptor( + TransformStream.prototype, 'readable').get; + assert_equals(readableGetter.call(sub), sub.readable, + 'Subclass object should pass brand check'); + assert_true(sub.extraFunction(), + 'extraFunction() should be present on Subclass object'); +}, 'Subclassing TransformStream should work'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/lipfuzz.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/lipfuzz.any.js new file mode 100644 index 000000000000..f9f148aaf1c6 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/lipfuzz.any.js @@ -0,0 +1,163 @@ +// META: global=window,worker +'use strict'; + +class LipFuzzTransformer { + constructor(substitutions) { + this.substitutions = substitutions; + this.partialChunk = ''; + this.lastIndex = undefined; + } + + transform(chunk, controller) { + chunk = this.partialChunk + chunk; + this.partialChunk = ''; + // lastIndex is the index of the first character after the last substitution. + this.lastIndex = 0; + chunk = chunk.replace(/\{\{([a-zA-Z0-9_-]+)\}\}/g, this.replaceTag.bind(this)); + // Regular expression for an incomplete template at the end of a string. + const partialAtEndRegexp = /\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g; + // Avoid looking at any characters that have already been substituted. + partialAtEndRegexp.lastIndex = this.lastIndex; + this.lastIndex = undefined; + const match = partialAtEndRegexp.exec(chunk); + if (match) { + this.partialChunk = chunk.substring(match.index); + chunk = chunk.substring(0, match.index); + } + controller.enqueue(chunk); + } + + flush(controller) { + if (this.partialChunk.length > 0) { + controller.enqueue(this.partialChunk); + } + } + + replaceTag(match, p1, offset) { + let replacement = this.substitutions[p1]; + if (replacement === undefined) { + replacement = ''; + } + this.lastIndex = offset + replacement.length; + return replacement; + } +} + +const substitutions = { + in1: 'out1', + in2: 'out2', + quine: '{{quine}}', + bogusPartial: '{{incompleteResult}' +}; + +const cases = [ + { + input: [''], + output: [''] + }, + { + input: [], + output: [] + }, + { + input: ['{{in1}}'], + output: ['out1'] + }, + { + input: ['z{{in1}}'], + output: ['zout1'] + }, + { + input: ['{{in1}}q'], + output: ['out1q'] + }, + { + input: ['{{in1}}{{in1}'], + output: ['out1', '{{in1}'] + }, + { + input: ['{{in1}}{{in1}', '}'], + output: ['out1', 'out1'] + }, + { + input: ['{{in1', '}}'], + output: ['', 'out1'] + }, + { + input: ['{{', 'in1}}'], + output: ['', 'out1'] + }, + { + input: ['{', '{in1}}'], + output: ['', 'out1'] + }, + { + input: ['{{', 'in1}'], + output: ['', '', '{{in1}'] + }, + { + input: ['{'], + output: ['', '{'] + }, + { + input: ['{', ''], + output: ['', '', '{'] + }, + { + input: ['{', '{', 'i', 'n', '1', '}', '}'], + output: ['', '', '', '', '', '', 'out1'] + }, + { + input: ['{{in1}}{{in2}}{{in1}}'], + output: ['out1out2out1'] + }, + { + input: ['{{wrong}}'], + output: [''] + }, + { + input: ['{{wron', 'g}}'], + output: ['', ''] + }, + { + input: ['{{quine}}'], + output: ['{{quine}}'] + }, + { + input: ['{{bogusPartial}}'], + output: ['{{incompleteResult}'] + }, + { + input: ['{{bogusPartial}}}'], + output: ['{{incompleteResult}}'] + } +]; + +for (const testCase of cases) { + const inputChunks = testCase.input; + const outputChunks = testCase.output; + promise_test(() => { + const lft = new TransformStream(new LipFuzzTransformer(substitutions)); + const writer = lft.writable.getWriter(); + const promises = []; + for (const inputChunk of inputChunks) { + promises.push(writer.write(inputChunk)); + } + promises.push(writer.close()); + const reader = lft.readable.getReader(); + let readerChain = Promise.resolve(); + for (const outputChunk of outputChunks) { + readerChain = readerChain.then(() => { + return reader.read().then(({ value, done }) => { + assert_false(done, `done should be false when reading ${outputChunk}`); + assert_equals(value, outputChunk, `value should match outputChunk`); + }); + }); + } + readerChain = readerChain.then(() => { + return reader.read().then(({ done }) => assert_true(done, `done should be true`)); + }); + promises.push(readerChain); + return Promise.all(promises); + }, `testing "${inputChunks}" (length ${inputChunks.length})`); +} diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/patched-global.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/patched-global.any.js new file mode 100644 index 000000000000..2d04e3b948b3 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/patched-global.any.js @@ -0,0 +1,53 @@ +// META: global=window,worker +'use strict'; + +// Tests which patch the global environment are kept separate to avoid +// interfering with other tests. + +test(t => { + // eslint-disable-next-line no-extend-native, accessor-pairs + Object.defineProperty(Object.prototype, 'highWaterMark', { + set() { throw new Error('highWaterMark setter called'); }, + configurable: true + }); + + // eslint-disable-next-line no-extend-native, accessor-pairs + Object.defineProperty(Object.prototype, 'size', { + set() { throw new Error('size setter called'); }, + configurable: true + }); + + t.add_cleanup(() => { + delete Object.prototype.highWaterMark; + delete Object.prototype.size; + }); + + assert_not_equals(new TransformStream(), null, 'constructor should work'); +}, 'TransformStream constructor should not call setters for highWaterMark or size'); + +test(t => { + const oldReadableStream = ReadableStream; + const oldWritableStream = WritableStream; + const getReader = ReadableStream.prototype.getReader; + const getWriter = WritableStream.prototype.getWriter; + + // Replace ReadableStream and WritableStream with broken versions. + ReadableStream = function () { + throw new Error('Called the global ReadableStream constructor'); + }; + WritableStream = function () { + throw new Error('Called the global WritableStream constructor'); + }; + t.add_cleanup(() => { + ReadableStream = oldReadableStream; + WritableStream = oldWritableStream; + }); + + const ts = new TransformStream(); + + // Just to be sure, ensure the readable and writable pass brand checks. + assert_not_equals(getReader.call(ts.readable), undefined, + 'getReader should work when called on ts.readable'); + assert_not_equals(getWriter.call(ts.writable), undefined, + 'getWriter should work when called on ts.writable'); +}, 'TransformStream should use the original value of ReadableStream and WritableStream'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/properties.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/properties.any.js new file mode 100644 index 000000000000..02981b8bc76a --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/properties.any.js @@ -0,0 +1,49 @@ +// META: global=window,worker +'use strict'; + +const transformerMethods = { + start: { + length: 1, + trigger: () => Promise.resolve() + }, + transform: { + length: 2, + trigger: ts => ts.writable.getWriter().write() + }, + flush: { + length: 1, + trigger: ts => ts.writable.getWriter().close() + } +}; + +for (const method in transformerMethods) { + const { length, trigger } = transformerMethods[method]; + + // Some semantic tests of how transformer methods are called can be found in general.js, as well as in the test files + // specific to each method. + promise_test(() => { + let argCount; + const ts = new TransformStream({ + [method](...args) { + argCount = args.length; + } + }, undefined, { highWaterMark: Infinity }); + return Promise.resolve(trigger(ts)).then(() => { + assert_equals(argCount, length, `${method} should be called with ${length} arguments`); + }); + }, `transformer method ${method} should be called with the right number of arguments`); + + promise_test(() => { + let methodWasCalled = false; + function Transformer() {} + Transformer.prototype = { + [method]() { + methodWasCalled = true; + } + }; + const ts = new TransformStream(new Transformer(), undefined, { highWaterMark: Infinity }); + return Promise.resolve(trigger(ts)).then(() => { + assert_true(methodWasCalled, `${method} should be called`); + }); + }, `transformer method ${method} should be called even when it's located on the prototype chain`); +} diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/reentrant-strategies.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/reentrant-strategies.any.js new file mode 100644 index 000000000000..306cce8fc8b7 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/reentrant-strategies.any.js @@ -0,0 +1,323 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +// META: script=../resources/rs-utils.js +// META: script=../resources/test-utils.js +'use strict'; + +// The size() function of readableStrategy can re-entrantly call back into the TransformStream implementation. This +// makes it risky to cache state across the call to ReadableStreamDefaultControllerEnqueue. These tests attempt to catch +// such errors. They are separated from the other strategy tests because no real user code should ever do anything like +// this. +// +// There is no such issue with writableStrategy size() because it is never called from within TransformStream +// algorithms. + +const error1 = new Error('error1'); +error1.name = 'error1'; + +promise_test(() => { + let controller; + let calls = 0; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + ++calls; + if (calls < 2) { + controller.enqueue('b'); + } + return 1; + }, + highWaterMark: Infinity + }); + const writer = ts.writable.getWriter(); + return Promise.all([writer.write('a'), writer.close()]) + .then(() => readableStreamToArray(ts.readable)) + .then(array => assert_array_equals(array, ['b', 'a'], 'array should contain two chunks')); +}, 'enqueue() inside size() should work'); + +promise_test(() => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + // The readable queue is empty. + controller.terminate(); + // The readable state has gone from "readable" to "closed". + return 1; + // This chunk will be enqueued, but will be impossible to read because the state is already "closed". + }, + highWaterMark: Infinity + }); + const writer = ts.writable.getWriter(); + return writer.write('a') + .then(() => readableStreamToArray(ts.readable)) + .then(array => assert_array_equals(array, [], 'array should contain no chunks')); + // The chunk 'a' is still in readable's queue. readable is closed so 'a' cannot be read. writable's queue is empty and + // it is still writable. +}, 'terminate() inside size() should work'); + +promise_test(t => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + controller.error(error1); + return 1; + }, + highWaterMark: Infinity + }); + const writer = ts.writable.getWriter(); + return writer.write('a') + .then(() => promise_rejects_exactly(t, error1, ts.readable.getReader().read(), 'read() should reject')); +}, 'error() inside size() should work'); + +promise_test(() => { + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + assert_equals(controller.desiredSize, 1, 'desiredSize should be 1'); + return 1; + }, + highWaterMark: 1 + }); + const writer = ts.writable.getWriter(); + return Promise.all([writer.write('a'), writer.close()]) + .then(() => readableStreamToArray(ts.readable)) + .then(array => assert_array_equals(array, ['a'], 'array should contain one chunk')); +}, 'desiredSize inside size() should work'); + +promise_test(t => { + let cancelPromise; + const ts = new TransformStream({}, undefined, { + size() { + cancelPromise = ts.readable.cancel(error1); + return 1; + }, + highWaterMark: Infinity + }); + const writer = ts.writable.getWriter(); + return writer.write('a') + .then(() => { + promise_rejects_exactly(t, error1, writer.closed, 'writer.closed should reject'); + return cancelPromise; + }); +}, 'readable cancel() inside size() should work'); + +promise_test(() => { + let controller; + let pipeToPromise; + const ws = recordingWritableStream(); + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + if (!pipeToPromise) { + pipeToPromise = ts.readable.pipeTo(ws); + } + return 1; + }, + highWaterMark: 1 + }); + // Allow promise returned by start() to resolve so that enqueue() will happen synchronously. + return delay(0).then(() => { + controller.enqueue('a'); + assert_not_equals(pipeToPromise, undefined); + + // Some pipeTo() implementations need an additional chunk enqueued in order for the first one to be processed. See + // https://github.com/whatwg/streams/issues/794 for background. + controller.enqueue('a'); + + // Give pipeTo() a chance to process the queued chunks. + return delay(0); + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'a'], 'ws should contain two chunks'); + controller.terminate(); + return pipeToPromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 'a', 'write', 'a', 'close'], 'target should have been closed'); + }); +}, 'pipeTo() inside size() should work'); + +promise_test(() => { + let controller; + let readPromise; + let calls = 0; + let reader; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + // This is triggered by controller.enqueue(). The queue is empty and there are no pending reads. pull() is called + // synchronously, allowing transform() to proceed asynchronously. This results in a second call to enqueue(), + // which resolves this pending read() without calling size() again. + readPromise = reader.read(); + ++calls; + return 1; + }, + highWaterMark: 0 + }); + reader = ts.readable.getReader(); + const writer = ts.writable.getWriter(); + let writeResolved = false; + const writePromise = writer.write('b').then(() => { + writeResolved = true; + }); + return flushAsyncEvents().then(() => { + assert_false(writeResolved); + controller.enqueue('a'); + assert_equals(calls, 1, 'size() should have been called once'); + return delay(0); + }).then(() => { + assert_true(writeResolved); + assert_equals(calls, 1, 'size() should only be called once'); + return readPromise; + }).then(({ value, done }) => { + assert_false(done, 'done should be false'); + // See https://github.com/whatwg/streams/issues/794 for why this chunk is not 'a'. + assert_equals(value, 'b', 'chunk should have been read'); + assert_equals(calls, 1, 'calls should still be 1'); + return writePromise; + }); +}, 'read() inside of size() should work'); + +promise_test(() => { + let writer; + let writePromise1; + let calls = 0; + const ts = new TransformStream({}, undefined, { + size() { + ++calls; + if (calls < 2) { + writePromise1 = writer.write('a'); + } + return 1; + }, + highWaterMark: Infinity + }); + writer = ts.writable.getWriter(); + // Give pull() a chance to be called. + return delay(0).then(() => { + // This write results in a synchronous call to transform(), enqueue(), and size(). + const writePromise2 = writer.write('b'); + assert_equals(calls, 1, 'size() should have been called once'); + return Promise.all([writePromise1, writePromise2, writer.close()]); + }).then(() => { + assert_equals(calls, 2, 'size() should have been called twice'); + return readableStreamToArray(ts.readable); + }).then(array => { + assert_array_equals(array, ['b', 'a'], 'both chunks should have been enqueued'); + assert_equals(calls, 2, 'calls should still be 2'); + }); +}, 'writer.write() inside size() should work'); + +promise_test(() => { + let controller; + let writer; + let writePromise; + let calls = 0; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + ++calls; + if (calls < 2) { + writePromise = writer.write('a'); + } + return 1; + }, + highWaterMark: Infinity + }); + writer = ts.writable.getWriter(); + // Give pull() a chance to be called. + return delay(0).then(() => { + // This enqueue results in synchronous calls to size(), write(), transform() and enqueue(). + controller.enqueue('b'); + assert_equals(calls, 2, 'size() should have been called twice'); + return Promise.all([writePromise, writer.close()]); + }).then(() => { + return readableStreamToArray(ts.readable); + }).then(array => { + // Because one call to enqueue() is nested inside the other, they finish in the opposite order that they were + // called, so the chunks end up reverse order. + assert_array_equals(array, ['a', 'b'], 'both chunks should have been enqueued'); + assert_equals(calls, 2, 'calls should still be 2'); + }); +}, 'synchronous writer.write() inside size() should work'); + +promise_test(() => { + let writer; + let closePromise; + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + closePromise = writer.close(); + return 1; + }, + highWaterMark: 1 + }); + writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + // Wait for the promise returned by start() to be resolved so that the call to close() will result in a synchronous + // call to TransformStreamDefaultSink. + return delay(0).then(() => { + controller.enqueue('a'); + return reader.read(); + }).then(({ value, done }) => { + assert_false(done, 'done should be false'); + assert_equals(value, 'a', 'value should be correct'); + return reader.read(); + }).then(({ done }) => { + assert_true(done, 'done should be true'); + return closePromise; + }); +}, 'writer.close() inside size() should work'); + +promise_test(t => { + let abortPromise; + let controller; + const ts = new TransformStream({ + start(c) { + controller = c; + } + }, undefined, { + size() { + abortPromise = ts.writable.abort(error1); + return 1; + }, + highWaterMark: 1 + }); + const reader = ts.readable.getReader(); + // Wait for the promise returned by start() to be resolved so that the call to abort() will result in a synchronous + // call to TransformStreamDefaultSink. + return delay(0).then(() => { + controller.enqueue('a'); + return reader.read(); + }).then(({ value, done }) => { + assert_false(done, 'done should be false'); + assert_equals(value, 'a', 'value should be correct'); + return Promise.all([promise_rejects_exactly(t, error1, reader.read(), 'read() should reject'), abortPromise]); + }); +}, 'writer.abort() inside size() should work'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/strategies.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/strategies.any.js new file mode 100644 index 000000000000..94055ad99dc9 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/strategies.any.js @@ -0,0 +1,150 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +// META: script=../resources/test-utils.js +'use strict'; + +// Here we just test that the strategies are correctly passed to the readable and writable sides. We assume that +// ReadableStream and WritableStream will correctly apply the strategies when they are being used by a TransformStream +// and so it isn't necessary to repeat their tests here. + +test(() => { + const ts = new TransformStream({}, { highWaterMark: 17 }); + assert_equals(ts.writable.getWriter().desiredSize, 17, 'desiredSize should be 17'); +}, 'writableStrategy highWaterMark should work'); + +promise_test(() => { + const ts = recordingTransformStream({}, undefined, { highWaterMark: 9 }); + const writer = ts.writable.getWriter(); + for (let i = 0; i < 10; ++i) { + writer.write(i); + } + return delay(0).then(() => { + assert_array_equals(ts.events, [ + 'transform', 0, 'transform', 1, 'transform', 2, 'transform', 3, 'transform', 4, + 'transform', 5, 'transform', 6, 'transform', 7, 'transform', 8], + 'transform() should have been called 9 times'); + }); +}, 'readableStrategy highWaterMark should work'); + +promise_test(t => { + let writableSizeCalled = false; + let readableSizeCalled = false; + let transformCalled = false; + const ts = new TransformStream( + { + transform(chunk, controller) { + t.step(() => { + transformCalled = true; + assert_true(writableSizeCalled, 'writableStrategy.size() should have been called'); + assert_false(readableSizeCalled, 'readableStrategy.size() should not have been called'); + controller.enqueue(chunk); + assert_true(readableSizeCalled, 'readableStrategy.size() should have been called'); + }); + } + }, + { + size() { + writableSizeCalled = true; + return 1; + } + }, + { + size() { + readableSizeCalled = true; + return 1; + }, + highWaterMark: Infinity + }); + return ts.writable.getWriter().write().then(() => { + assert_true(transformCalled, 'transform() should be called'); + }); +}, 'writable should have the correct size() function'); + +test(() => { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + assert_equals(writer.desiredSize, 1, 'default writable HWM is 1'); + writer.write(undefined); + assert_equals(writer.desiredSize, 0, 'default chunk size is 1'); +}, 'default writable strategy should be equivalent to { highWaterMark: 1 }'); + +promise_test(t => { + const ts = new TransformStream({ + transform(chunk, controller) { + return t.step(() => { + assert_equals(controller.desiredSize, 0, 'desiredSize should be 0'); + controller.enqueue(undefined); + // The first chunk enqueued is consumed by the pending read(). + assert_equals(controller.desiredSize, 0, 'desiredSize should still be 0'); + controller.enqueue(undefined); + assert_equals(controller.desiredSize, -1, 'desiredSize should be -1'); + }); + } + }); + const writePromise = ts.writable.getWriter().write(); + return ts.readable.getReader().read().then(() => writePromise); +}, 'default readable strategy should be equivalent to { highWaterMark: 0 }'); + +test(() => { + assert_throws_js(RangeError, () => new TransformStream(undefined, { highWaterMark: -1 }), + 'should throw RangeError for negative writableHighWaterMark'); + assert_throws_js(RangeError, () => new TransformStream(undefined, undefined, { highWaterMark: -1 }), + 'should throw RangeError for negative readableHighWaterMark'); + assert_throws_js(RangeError, () => new TransformStream(undefined, { highWaterMark: NaN }), + 'should throw RangeError for NaN writableHighWaterMark'); + assert_throws_js(RangeError, () => new TransformStream(undefined, undefined, { highWaterMark: NaN }), + 'should throw RangeError for NaN readableHighWaterMark'); +}, 'a RangeError should be thrown for an invalid highWaterMark'); + +const objectThatConvertsTo42 = { + toString() { + return '42'; + } +}; + +test(() => { + const ts = new TransformStream(undefined, { highWaterMark: objectThatConvertsTo42 }); + const writer = ts.writable.getWriter(); + assert_equals(writer.desiredSize, 42, 'writable HWM is 42'); +}, 'writableStrategy highWaterMark should be converted to a number'); + +test(() => { + const ts = new TransformStream({ + start(controller) { + assert_equals(controller.desiredSize, 42, 'desiredSize should be 42'); + } + }, undefined, { highWaterMark: objectThatConvertsTo42 }); +}, 'readableStrategy highWaterMark should be converted to a number'); + +promise_test(t => { + const ts = new TransformStream(undefined, undefined, { + size() { return NaN; }, + highWaterMark: 1 + }); + const writer = ts.writable.getWriter(); + return promise_rejects_js(t, RangeError, writer.write(), 'write should reject'); +}, 'a bad readableStrategy size function should cause writer.write() to reject on an identity transform'); + +promise_test(t => { + const ts = new TransformStream({ + transform(chunk, controller) { + // This assert has the important side-effect of catching the error, so transform() does not throw. + assert_throws_js(RangeError, () => controller.enqueue(chunk), 'enqueue should throw'); + } + }, undefined, { + size() { + return -1; + }, + highWaterMark: 1 + }); + + const writer = ts.writable.getWriter(); + return writer.write().then(() => { + return Promise.all([ + promise_rejects_js(t, RangeError, writer.ready, 'ready should reject'), + promise_rejects_js(t, RangeError, writer.closed, 'closed should reject'), + promise_rejects_js(t, RangeError, ts.readable.getReader().closed, 'readable closed should reject') + ]); + }); +}, 'a bad readableStrategy size function should error the stream on enqueue even when transformer.transform() ' + + 'catches the exception'); diff --git a/test/js/third_party/wpt-streams/streams/transform-streams/terminate.any.js b/test/js/third_party/wpt-streams/streams/transform-streams/terminate.any.js new file mode 100644 index 000000000000..670006366db2 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/transform-streams/terminate.any.js @@ -0,0 +1,100 @@ +// META: global=window,worker +// META: script=../resources/recording-streams.js +// META: script=../resources/test-utils.js +'use strict'; + +promise_test(t => { + const ts = recordingTransformStream({}, undefined, { highWaterMark: 0 }); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(0); + } + }); + let pipeToRejected = false; + const pipeToPromise = promise_rejects_js(t, TypeError, rs.pipeTo(ts.writable), 'pipeTo should reject').then(() => { + pipeToRejected = true; + }); + return delay(0).then(() => { + assert_array_equals(ts.events, [], 'transform() should have seen no chunks'); + assert_false(pipeToRejected, 'pipeTo() should not have rejected yet'); + ts.controller.terminate(); + return pipeToPromise; + }).then(() => { + assert_array_equals(ts.events, [], 'transform() should still have seen no chunks'); + assert_true(pipeToRejected, 'pipeToRejected must be true'); + }); +}, 'controller.terminate() should error pipeTo()'); + +promise_test(t => { + const ts = recordingTransformStream({}, undefined, { highWaterMark: 1 }); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(0); + controller.enqueue(1); + } + }); + const pipeToPromise = rs.pipeTo(ts.writable); + return delay(0).then(() => { + assert_array_equals(ts.events, ['transform', 0], 'transform() should have seen one chunk'); + ts.controller.terminate(); + return promise_rejects_js(t, TypeError, pipeToPromise, 'pipeTo() should reject'); + }).then(() => { + assert_array_equals(ts.events, ['transform', 0], 'transform() should still have seen only one chunk'); + }); +}, 'controller.terminate() should prevent remaining chunks from being processed'); + +test(() => { + new TransformStream({ + start(controller) { + controller.enqueue(0); + controller.terminate(); + assert_throws_js(TypeError, () => controller.enqueue(1), 'enqueue should throw'); + } + }); +}, 'controller.enqueue() should throw after controller.terminate()'); + +const error1 = new Error('error1'); +error1.name = 'error1'; + +promise_test(t => { + const ts = new TransformStream({ + start(controller) { + controller.enqueue(0); + controller.terminate(); + controller.error(error1); + } + }); + return Promise.all([ + promise_rejects_js(t, TypeError, ts.writable.abort(), 'abort() should reject with a TypeError'), + promise_rejects_exactly(t, error1, ts.readable.cancel(), 'cancel() should reject with error1'), + promise_rejects_exactly(t, error1, ts.readable.getReader().closed, 'closed should reject with error1') + ]); +}, 'controller.error() after controller.terminate() with queued chunk should error the readable'); + +promise_test(t => { + const ts = new TransformStream({ + start(controller) { + controller.terminate(); + controller.error(error1); + } + }); + return Promise.all([ + promise_rejects_js(t, TypeError, ts.writable.abort(), 'abort() should reject with a TypeError'), + ts.readable.cancel(), + ts.readable.getReader().closed + ]); +}, 'controller.error() after controller.terminate() without queued chunk should do nothing'); + +promise_test(() => { + const ts = new TransformStream({ + flush(controller) { + controller.terminate(); + } + }); + const writer = ts.writable.getWriter(); + return Promise.all([ + writer.close(), + writer.closed, + ts.readable.getReader().closed + ]); +}, 'controller.terminate() inside flush() should not prevent writer.close() from succeeding'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/aborting.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/aborting.any.js new file mode 100644 index 000000000000..58362b766901 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/aborting.any.js @@ -0,0 +1,1567 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +const error2 = new Error('error2'); +error2.name = 'error2'; + +promise_test(t => { + const ws = new WritableStream({ + write: t.unreached_func('write() should not be called') + }); + + const writer = ws.getWriter(); + const writePromise = writer.write('a'); + + const readyPromise = writer.ready; + + writer.abort(error1); + + assert_equals(writer.ready, readyPromise, 'the ready promise property should not change'); + + return Promise.all([ + promise_rejects_exactly(t, error1, readyPromise, 'the ready promise should reject with error1'), + promise_rejects_exactly(t, error1, writePromise, 'the write() promise should reject with error1') + ]); +}, 'Aborting a WritableStream before it starts should cause the writer\'s unsettled ready promise to reject'); + +promise_test(t => { + const ws = new WritableStream(); + + const writer = ws.getWriter(); + writer.write('a'); + + const readyPromise = writer.ready; + + return readyPromise.then(() => { + writer.abort(error1); + + assert_not_equals(writer.ready, readyPromise, 'the ready promise property should change'); + return promise_rejects_exactly(t, error1, writer.ready, 'the ready promise should reject with error1'); + }); +}, 'Aborting a WritableStream should cause the writer\'s fulfilled ready promise to reset to a rejected one'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, writer.abort(), 'abort() should reject with a TypeError'); +}, 'abort() on a released writer rejects'); + +promise_test(t => { + const ws = recordingWritableStream(); + + return delay(0) + .then(() => { + const writer = ws.getWriter(); + + const abortPromise = writer.abort(error1); + + return Promise.all([ + promise_rejects_exactly(t, error1, writer.write(1), 'write(1) must reject with error1'), + promise_rejects_exactly(t, error1, writer.write(2), 'write(2) must reject with error1'), + abortPromise + ]); + }) + .then(() => { + assert_array_equals(ws.events, ['abort', error1]); + }); +}, 'Aborting a WritableStream immediately prevents future writes'); + +promise_test(t => { + const ws = recordingWritableStream(); + const results = []; + + return delay(0) + .then(() => { + const writer = ws.getWriter(); + + results.push( + writer.write(1), + promise_rejects_exactly(t, error1, writer.write(2), 'write(2) must reject with error1'), + promise_rejects_exactly(t, error1, writer.write(3), 'write(3) must reject with error1') + ); + + const abortPromise = writer.abort(error1); + + results.push( + promise_rejects_exactly(t, error1, writer.write(4), 'write(4) must reject with error1'), + promise_rejects_exactly(t, error1, writer.write(5), 'write(5) must reject with error1') + ); + + return abortPromise; + }).then(() => { + assert_array_equals(ws.events, ['write', 1, 'abort', error1]); + + return Promise.all(results); + }); +}, 'Aborting a WritableStream prevents further writes after any that are in progress'); + +promise_test(() => { + const ws = new WritableStream({ + abort() { + return 'Hello'; + } + }); + const writer = ws.getWriter(); + + return writer.abort('a').then(value => { + assert_equals(value, undefined, 'fulfillment value must be undefined'); + }); +}, 'Fulfillment value of writer.abort() call must be undefined even if the underlying sink returns a non-undefined ' + + 'value'); + +promise_test(t => { + const ws = new WritableStream({ + abort() { + throw error1; + } + }); + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.abort(undefined), + 'rejection reason of abortPromise must be the error thrown by abort'); +}, 'WritableStream if sink\'s abort throws, the promise returned by writer.abort() rejects'); + +promise_test(t => { + const ws = new WritableStream({ + abort() { + throw error1; + } + }); + const writer = ws.getWriter(); + + const abortPromise1 = writer.abort(undefined); + const abortPromise2 = writer.abort(undefined); + + assert_equals(abortPromise1, abortPromise2, 'the promises must be the same'); + + return promise_rejects_exactly(t, error1, abortPromise1, 'promise must have matching rejection'); +}, 'WritableStream if sink\'s abort throws, the promise returned by multiple writer.abort()s is the same and rejects'); + +promise_test(t => { + const ws = new WritableStream({ + abort() { + throw error1; + } + }); + + return promise_rejects_exactly(t, error1, ws.abort(undefined), + 'rejection reason of abortPromise must be the error thrown by abort'); +}, 'WritableStream if sink\'s abort throws, the promise returned by ws.abort() rejects'); + +promise_test(t => { + let resolveWritePromise; + const ws = new WritableStream({ + write() { + return new Promise(resolve => { + resolveWritePromise = resolve; + }); + }, + abort() { + throw error1; + } + }); + + const writer = ws.getWriter(); + + writer.write().catch(() => {}); + return flushAsyncEvents().then(() => { + const abortPromise = writer.abort(undefined); + + resolveWritePromise(); + return promise_rejects_exactly(t, error1, abortPromise, + 'rejection reason of abortPromise must be the error thrown by abort'); + }); +}, 'WritableStream if sink\'s abort throws, for an abort performed during a write, the promise returned by ' + + 'ws.abort() rejects'); + +promise_test(() => { + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + + return writer.abort(error1).then(() => { + assert_array_equals(ws.events, ['abort', error1]); + }); +}, 'Aborting a WritableStream passes through the given reason'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + + const abortPromise = writer.abort(error1); + + const events = []; + writer.ready.catch(() => { + events.push('ready'); + }); + writer.closed.catch(() => { + events.push('closed'); + }); + + return Promise.all([ + abortPromise, + promise_rejects_exactly(t, error1, writer.write(), 'writing should reject with error1'), + promise_rejects_exactly(t, error1, writer.close(), 'closing should reject with error1'), + promise_rejects_exactly(t, error1, writer.ready, 'ready should reject with error1'), + promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1') + ]).then(() => { + assert_array_equals(['ready', 'closed'], events, 'ready should reject before closed'); + }); +}, 'Aborting a WritableStream puts it in an errored state with the error passed to abort()'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + + const writePromise = promise_rejects_exactly(t, error1, writer.write('a'), + 'writing should reject with error1'); + + writer.abort(error1); + + return writePromise; +}, 'Aborting a WritableStream causes any outstanding write() promises to be rejected with the reason supplied'); + +promise_test(t => { + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + + const closePromise = writer.close(); + const abortPromise = writer.abort(error1); + + return Promise.all([ + promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1'), + promise_rejects_exactly(t, error1, closePromise, 'close() should reject with error1'), + abortPromise + ]).then(() => { + assert_array_equals(ws.events, ['abort', error1]); + }); +}, 'Closing but then immediately aborting a WritableStream causes the stream to error'); + +promise_test(() => { + let resolveClose; + const ws = new WritableStream({ + close() { + return new Promise(resolve => { + resolveClose = resolve; + }); + } + }); + const writer = ws.getWriter(); + + const closePromise = writer.close(); + + return delay(0).then(() => { + const abortPromise = writer.abort(error1); + resolveClose(); + return Promise.all([ + writer.closed, + abortPromise, + closePromise + ]); + }); +}, 'Closing a WritableStream and aborting it while it closes causes the stream to ignore the abort attempt'); + +promise_test(() => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + + writer.close(); + + return delay(0).then(() => writer.abort()); +}, 'Aborting a WritableStream after it is closed is a no-op'); + +promise_test(t => { + // Testing that per https://github.com/whatwg/streams/issues/620#issuecomment-263483953 the fallback to close was + // removed. + + // Cannot use recordingWritableStream since it always has an abort + let closeCalled = false; + const ws = new WritableStream({ + close() { + closeCalled = true; + } + }); + + const writer = ws.getWriter(); + + writer.abort(error1); + + return promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1').then(() => { + assert_false(closeCalled, 'close must not have been called'); + }); +}, 'WritableStream should NOT call underlying sink\'s close if no abort is supplied (historical)'); + +promise_test(() => { + let thenCalled = false; + const ws = new WritableStream({ + abort() { + return { + then(onFulfilled) { + thenCalled = true; + onFulfilled(); + } + }; + } + }); + const writer = ws.getWriter(); + return writer.abort().then(() => assert_true(thenCalled, 'then() should be called')); +}, 'returning a thenable from abort() should work'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + return flushAsyncEvents(); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('a'); + writer.abort(error1); + let closedRejected = false; + return Promise.all([ + writePromise.then(() => assert_false(closedRejected, '.closed should not resolve before write()')), + promise_rejects_exactly(t, error1, writer.closed, '.closed should reject').then(() => { + closedRejected = true; + }) + ]); + }); +}, '.closed should not resolve before fulfilled write()'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + return Promise.reject(error1); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('a'); + const abortPromise = writer.abort(error2); + let closedRejected = false; + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise, 'write() should reject') + .then(() => assert_false(closedRejected, '.closed should not resolve before write()')), + promise_rejects_exactly(t, error2, writer.closed, '.closed should reject') + .then(() => { + closedRejected = true; + }), + abortPromise + ]); + }); +}, '.closed should not resolve before rejected write(); write() error should not overwrite abort() error'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + return flushAsyncEvents(); + } + }, new CountQueuingStrategy({ highWaterMark: 4 })); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const settlementOrder = []; + return Promise.all([ + writer.write('1').then(() => settlementOrder.push(1)), + promise_rejects_exactly(t, error1, writer.write('2'), 'first queued write should be rejected') + .then(() => settlementOrder.push(2)), + promise_rejects_exactly(t, error1, writer.write('3'), 'second queued write should be rejected') + .then(() => settlementOrder.push(3)), + writer.abort(error1) + ]).then(() => assert_array_equals([1, 2, 3], settlementOrder, 'writes should be satisfied in order')); + }); +}, 'writes should be satisfied in order when aborting'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + return Promise.reject(error1); + } + }, new CountQueuingStrategy({ highWaterMark: 4 })); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const settlementOrder = []; + return Promise.all([ + promise_rejects_exactly(t, error1, writer.write('1'), 'in-flight write should be rejected') + .then(() => settlementOrder.push(1)), + promise_rejects_exactly(t, error2, writer.write('2'), 'first queued write should be rejected') + .then(() => settlementOrder.push(2)), + promise_rejects_exactly(t, error2, writer.write('3'), 'second queued write should be rejected') + .then(() => settlementOrder.push(3)), + writer.abort(error2) + ]).then(() => assert_array_equals([1, 2, 3], settlementOrder, 'writes should be satisfied in order')); + }); +}, 'writes should be satisfied in order after rejected write when aborting'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + return Promise.reject(error1); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + return Promise.all([ + promise_rejects_exactly(t, error1, writer.write('a'), 'writer.write() should reject with error from underlying write()'), + promise_rejects_exactly(t, error2, writer.close(), + 'writer.close() should reject with error from underlying write()'), + writer.abort(error2) + ]); + }); +}, 'close() should reject with abort reason why abort() is first error'); + +promise_test(() => { + let resolveWrite; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + + const writer = ws.getWriter(); + return writer.ready.then(() => { + writer.write('a'); + const abortPromise = writer.abort('b'); + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a'], 'abort should not be called while write is in-flight'); + resolveWrite(); + return abortPromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'abort', 'b'], 'abort should be called after the write finishes'); + }); + }); + }); +}, 'underlying abort() should not be called until underlying write() completes'); + +promise_test(() => { + let resolveClose; + const ws = recordingWritableStream({ + close() { + return new Promise(resolve => { + resolveClose = resolve; + }); + } + }); + + const writer = ws.getWriter(); + return writer.ready.then(() => { + writer.close(); + const abortPromise = writer.abort(); + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['close'], 'abort should not be called while close is in-flight'); + resolveClose(); + return abortPromise.then(() => { + assert_array_equals(ws.events, ['close'], 'abort should not be called'); + }); + }); + }); +}, 'underlying abort() should not be called if underlying close() has started'); + +promise_test(t => { + let rejectClose; + let abortCalled = false; + const ws = new WritableStream({ + close() { + return new Promise((resolve, reject) => { + rejectClose = reject; + }); + }, + abort() { + abortCalled = true; + } + }); + + const writer = ws.getWriter(); + return writer.ready.then(() => { + const closePromise = writer.close(); + const abortPromise = writer.abort(); + return flushAsyncEvents().then(() => { + assert_false(abortCalled, 'underlying abort should not be called while close is in-flight'); + rejectClose(error1); + return promise_rejects_exactly(t, error1, abortPromise, 'abort should reject with the same reason').then(() => { + return promise_rejects_exactly(t, error1, closePromise, 'close should reject with the same reason'); + }).then(() => { + assert_false(abortCalled, 'underlying abort should not be called after close completes'); + }); + }); + }); +}, 'if underlying close() has started and then rejects, the abort() and close() promises should reject with the ' + + 'underlying close rejection reason'); + +promise_test(t => { + let resolveWrite; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + + const writer = ws.getWriter(); + return writer.ready.then(() => { + writer.write('a'); + const closePromise = writer.close(); + const abortPromise = writer.abort(error1); + + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, ['write', 'a'], 'abort should not be called while write is in-flight'); + resolveWrite(); + return abortPromise.then(() => { + assert_array_equals(ws.events, ['write', 'a', 'abort', error1], 'abort should be called after write completes'); + return promise_rejects_exactly(t, error1, closePromise, 'promise returned by close() should be rejected'); + }); + }); + }); +}, 'an abort() that happens during a write() should trigger the underlying abort() even with a close() queued'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + return new Promise(() => {}); + } + }); + + const writer = ws.getWriter(); + return writer.ready.then(() => { + writer.write('a'); + writer.abort(error1); + writer.releaseLock(); + const writer2 = ws.getWriter(); + return promise_rejects_exactly(t, error1, writer2.ready, + 'ready of the second writer should be rejected with error1'); + }); +}, 'if a writer is created for a stream with a pending abort, its ready should be rejected with the abort error'); + +promise_test(() => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const closePromise = writer.close(); + const abortPromise = writer.abort(); + const events = []; + return Promise.all([ + closePromise.then(() => { events.push('close'); }), + abortPromise.then(() => { events.push('abort'); }) + ]).then(() => { + assert_array_equals(events, ['close', 'abort']); + }); + }); +}, 'writer close() promise should resolve before abort() promise'); + +promise_test(t => { + const ws = new WritableStream({ + write(chunk, controller) { + controller.error(error1); + return new Promise(() => {}); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + writer.write('a'); + return promise_rejects_exactly(t, error1, writer.ready, 'writer.ready should reject'); + }); +}, 'writer.ready should reject on controller error without waiting for underlying write'); + +promise_test(t => { + let rejectWrite; + const ws = new WritableStream({ + write() { + return new Promise((resolve, reject) => { + rejectWrite = reject; + }); + } + }); + + let writePromise; + let abortPromise; + + const events = []; + + const writer = ws.getWriter(); + + writer.closed.catch(() => { + events.push('closed'); + }); + + // Wait for ws to start + return flushAsyncEvents().then(() => { + writePromise = writer.write('a'); + writePromise.catch(() => { + events.push('writePromise'); + }); + + abortPromise = writer.abort(error1); + abortPromise.then(() => { + events.push('abortPromise'); + }); + + const writePromise2 = writer.write('a'); + + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise2, 'writePromise2 must reject with the error from abort'), + promise_rejects_exactly(t, error1, writer.ready, 'writer.ready must reject with the error from abort'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, [], 'writePromise, abortPromise and writer.closed must not be rejected yet'); + + rejectWrite(error2); + + return Promise.all([ + promise_rejects_exactly(t, error2, writePromise, + 'writePromise must reject with the error returned from the sink\'s write method'), + abortPromise, + promise_rejects_exactly(t, error1, writer.closed, + 'writer.closed must reject with the error from abort'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'], + 'writePromise, abortPromise and writer.closed must settle'); + + const writePromise3 = writer.write('a'); + + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise3, + 'writePromise3 must reject with the error from abort'), + promise_rejects_exactly(t, error1, writer.ready, + 'writer.ready must be still rejected with the error indicating abort') + ]); + }).then(() => { + writer.releaseLock(); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.ready, + 'writer.ready must be rejected with an error indicating release'), + promise_rejects_js(t, TypeError, writer.closed, + 'writer.closed must be rejected with an error indicating release') + ]); + }); +}, 'writer.abort() while there is an in-flight write, and then finish the write with rejection'); + +promise_test(t => { + let resolveWrite; + let controller; + const ws = new WritableStream({ + write(chunk, c) { + controller = c; + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + + let writePromise; + let abortPromise; + + const events = []; + + const writer = ws.getWriter(); + + writer.closed.catch(() => { + events.push('closed'); + }); + + // Wait for ws to start + return flushAsyncEvents().then(() => { + writePromise = writer.write('a'); + writePromise.then(() => { + events.push('writePromise'); + }); + + abortPromise = writer.abort(error1); + abortPromise.then(() => { + events.push('abortPromise'); + }); + + const writePromise2 = writer.write('a'); + + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise2, 'writePromise2 must reject with the error from abort'), + promise_rejects_exactly(t, error1, writer.ready, 'writer.ready must reject with the error from abort'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, [], 'writePromise, abortPromise and writer.closed must not be fulfilled/rejected yet'); + + // This error is too late to change anything. abort() has already changed the stream state to 'erroring'. + controller.error(error2); + + const writePromise3 = writer.write('a'); + + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise3, + 'writePromise3 must reject with the error from abort'), + promise_rejects_exactly(t, error1, writer.ready, + 'writer.ready must be still rejected with the error indicating abort'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals( + events, [], + 'writePromise, abortPromise and writer.closed must not be fulfilled/rejected yet even after ' + + 'controller.error() call'); + + resolveWrite(); + + return Promise.all([ + writePromise, + abortPromise, + promise_rejects_exactly(t, error1, writer.closed, + 'writer.closed must reject with the error from abort'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'], + 'writePromise, abortPromise and writer.closed must settle'); + + const writePromise4 = writer.write('a'); + + return Promise.all([ + writePromise, + promise_rejects_exactly(t, error1, writePromise4, + 'writePromise4 must reject with the error from abort'), + promise_rejects_exactly(t, error1, writer.ready, + 'writer.ready must be still rejected with the error indicating abort') + ]); + }).then(() => { + writer.releaseLock(); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.ready, + 'writer.ready must be rejected with an error indicating release'), + promise_rejects_js(t, TypeError, writer.closed, + 'writer.closed must be rejected with an error indicating release') + ]); + }); +}, 'writer.abort(), controller.error() while there is an in-flight write, and then finish the write'); + +promise_test(t => { + let resolveClose; + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + }, + close() { + return new Promise(resolve => { + resolveClose = resolve; + }); + } + }); + + let closePromise; + let abortPromise; + + const events = []; + + const writer = ws.getWriter(); + + writer.closed.then(() => { + events.push('closed'); + }); + + // Wait for ws to start + return flushAsyncEvents().then(() => { + closePromise = writer.close(); + closePromise.then(() => { + events.push('closePromise'); + }); + + abortPromise = writer.abort(error1); + abortPromise.then(() => { + events.push('abortPromise'); + }); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.close(), + 'writer.close() must reject with an error indicating already closing'), + promise_rejects_exactly(t, error1, writer.ready, 'writer.ready must reject with the error from abort'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, [], 'closePromise, abortPromise and writer.closed must not be fulfilled/rejected yet'); + + controller.error(error2); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.close(), + 'writer.close() must reject with an error indicating already closing'), + promise_rejects_exactly(t, error1, writer.ready, + 'writer.ready must be still rejected with the error indicating abort'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals( + events, [], + 'closePromise, abortPromise and writer.closed must not be fulfilled/rejected yet even after ' + + 'controller.error() call'); + + resolveClose(); + + return Promise.all([ + closePromise, + abortPromise, + writer.closed, + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'], + 'closedPromise, abortPromise and writer.closed must fulfill'); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.close(), + 'writer.close() must reject with an error indicating already closing'), + promise_rejects_exactly(t, error1, writer.ready, + 'writer.ready must be still rejected with the error indicating abort') + ]); + }).then(() => { + writer.releaseLock(); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.close(), + 'writer.close() must reject with an error indicating release'), + promise_rejects_js(t, TypeError, writer.ready, + 'writer.ready must be rejected with an error indicating release'), + promise_rejects_js(t, TypeError, writer.closed, + 'writer.closed must be rejected with an error indicating release') + ]); + }); +}, 'writer.abort(), controller.error() while there is an in-flight close, and then finish the close'); + +promise_test(t => { + let resolveWrite; + let controller; + const ws = recordingWritableStream({ + write(chunk, c) { + controller = c; + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + + let writePromise; + let abortPromise; + + const events = []; + + const writer = ws.getWriter(); + + writer.closed.catch(() => { + events.push('closed'); + }); + + // Wait for ws to start + return flushAsyncEvents().then(() => { + writePromise = writer.write('a'); + writePromise.then(() => { + events.push('writePromise'); + }); + + controller.error(error2); + + const writePromise2 = writer.write('a'); + + return Promise.all([ + promise_rejects_exactly(t, error2, writePromise2, + 'writePromise2 must reject with the error passed to the controller\'s error method'), + promise_rejects_exactly(t, error2, writer.ready, + 'writer.ready must reject with the error passed to the controller\'s error method'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, [], 'writePromise and writer.closed must not be fulfilled/rejected yet'); + + abortPromise = writer.abort(error1); + abortPromise.catch(() => { + events.push('abortPromise'); + }); + + const writePromise3 = writer.write('a'); + + return Promise.all([ + promise_rejects_exactly(t, error2, writePromise3, + 'writePromise3 must reject with the error passed to the controller\'s error method'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals( + events, [], + 'writePromise and writer.closed must not be fulfilled/rejected yet even after writer.abort()'); + + resolveWrite(); + + return Promise.all([ + promise_rejects_exactly(t, error2, abortPromise, + 'abort() must reject with the error passed to the controller\'s error method'), + promise_rejects_exactly(t, error2, writer.closed, + 'writer.closed must reject with the error passed to the controller\'s error method'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'], + 'writePromise, abortPromise and writer.closed must fulfill/reject'); + assert_array_equals(ws.events, ['write', 'a'], 'sink abort() should not be called'); + + const writePromise4 = writer.write('a'); + + return Promise.all([ + writePromise, + promise_rejects_exactly(t, error2, writePromise4, + 'writePromise4 must reject with the error passed to the controller\'s error method'), + promise_rejects_exactly(t, error2, writer.ready, + 'writer.ready must be still rejected with the error passed to the controller\'s error method') + ]); + }).then(() => { + writer.releaseLock(); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.ready, + 'writer.ready must be rejected with an error indicating release'), + promise_rejects_js(t, TypeError, writer.closed, + 'writer.closed must be rejected with an error indicating release') + ]); + }); +}, 'controller.error(), writer.abort() while there is an in-flight write, and then finish the write'); + +promise_test(t => { + let resolveClose; + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + }, + close() { + return new Promise(resolve => { + resolveClose = resolve; + }); + } + }); + + let closePromise; + let abortPromise; + + const events = []; + + const writer = ws.getWriter(); + + writer.closed.then(() => { + events.push('closed'); + }); + + // Wait for ws to start + return flushAsyncEvents().then(() => { + closePromise = writer.close(); + closePromise.then(() => { + events.push('closePromise'); + }); + + controller.error(error2); + + return flushAsyncEvents(); + }).then(() => { + assert_array_equals(events, [], 'closePromise must not be fulfilled/rejected yet'); + + abortPromise = writer.abort(error1); + abortPromise.then(() => { + events.push('abortPromise'); + }); + + return Promise.all([ + promise_rejects_exactly(t, error2, writer.ready, + 'writer.ready must reject with the error passed to the controller\'s error method'), + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals( + events, [], + 'closePromise and writer.closed must not be fulfilled/rejected yet even after writer.abort()'); + + resolveClose(); + + return Promise.all([ + closePromise, + promise_rejects_exactly(t, error2, writer.ready, + 'writer.ready must be still rejected with the error passed to the controller\'s error method'), + writer.closed, + flushAsyncEvents() + ]); + }).then(() => { + assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'], + 'abortPromise, closePromise and writer.closed must fulfill/reject'); + }).then(() => { + writer.releaseLock(); + + return Promise.all([ + promise_rejects_js(t, TypeError, writer.ready, + 'writer.ready must be rejected with an error indicating release'), + promise_rejects_js(t, TypeError, writer.closed, + 'writer.closed must be rejected with an error indicating release') + ]); + }); +}, 'controller.error(), writer.abort() while there is an in-flight close, and then finish the close'); + +promise_test(t => { + let resolveWrite; + const ws = new WritableStream({ + write() { + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('a'); + const closed = writer.closed; + const abortPromise = writer.abort(); + writer.releaseLock(); + resolveWrite(); + return Promise.all([ + writePromise, + abortPromise, + promise_rejects_js(t, TypeError, closed, 'closed should reject')]); + }); +}, 'releaseLock() while aborting should reject the original closed promise'); + +// TODO(ricea): Consider removing this test if it is no longer useful. +promise_test(t => { + let resolveWrite; + let resolveAbort; + let resolveAbortStarted; + const abortStarted = new Promise(resolve => { + resolveAbortStarted = resolve; + }); + const ws = new WritableStream({ + write() { + return new Promise(resolve => { + resolveWrite = resolve; + }); + }, + abort() { + resolveAbortStarted(); + return new Promise(resolve => { + resolveAbort = resolve; + }); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('a'); + const closed = writer.closed; + const abortPromise = writer.abort(); + resolveWrite(); + return abortStarted.then(() => { + writer.releaseLock(); + assert_equals(writer.closed, closed, 'closed promise should not have changed'); + resolveAbort(); + return Promise.all([ + writePromise, + abortPromise, + promise_rejects_js(t, TypeError, closed, 'closed should reject')]); + }); + }); +}, 'releaseLock() during delayed async abort() should reject the writer.closed promise'); + +promise_test(() => { + let resolveStart; + const ws = recordingWritableStream({ + start() { + return new Promise(resolve => { + resolveStart = resolve; + }); + } + }); + const abortPromise = ws.abort('done'); + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, [], 'abort() should not be called during start()'); + resolveStart(); + return abortPromise.then(() => { + assert_array_equals(ws.events, ['abort', 'done'], 'abort() should be called after start() is done'); + }); + }); +}, 'sink abort() should not be called until sink start() is done'); + +promise_test(() => { + let resolveStart; + let controller; + const ws = recordingWritableStream({ + start(c) { + controller = c; + return new Promise(resolve => { + resolveStart = resolve; + }); + } + }); + const abortPromise = ws.abort('done'); + controller.error(error1); + resolveStart(); + return abortPromise.then(() => + assert_array_equals(ws.events, ['abort', 'done'], + 'abort() should still be called if start() errors the controller')); +}, 'if start attempts to error the controller after abort() has been called, then it should lose'); + +promise_test(() => { + const ws = recordingWritableStream({ + start() { + return Promise.reject(error1); + } + }); + return ws.abort('done').then(() => + assert_array_equals(ws.events, ['abort', 'done'], 'abort() should still be called if start() rejects')); +}, 'stream abort() promise should still resolve if sink start() rejects'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + const writerReady1 = writer.ready; + writer.abort(error1); + const writerReady2 = writer.ready; + assert_not_equals(writerReady1, writerReady2, 'abort() should replace the ready promise with a rejected one'); + return Promise.all([writerReady1, + promise_rejects_exactly(t, error1, writerReady2, 'writerReady2 should reject')]); +}, 'writer abort() during sink start() should replace the writer.ready promise synchronously'); + +promise_test(t => { + const events = []; + const ws = recordingWritableStream(); + const writer = ws.getWriter(); + const writePromise1 = writer.write(1); + const abortPromise = writer.abort(error1); + const writePromise2 = writer.write(2); + const closePromise = writer.close(); + writePromise1.catch(() => events.push('write1')); + abortPromise.then(() => events.push('abort')); + writePromise2.catch(() => events.push('write2')); + closePromise.catch(() => events.push('close')); + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise1, 'first write() should reject'), + abortPromise, + promise_rejects_exactly(t, error1, writePromise2, 'second write() should reject'), + promise_rejects_exactly(t, error1, closePromise, 'close() should reject') + ]) + .then(() => { + assert_array_equals(events, ['write2', 'write1', 'abort', 'close'], + 'promises should resolve in the standard order'); + assert_array_equals(ws.events, ['abort', error1], 'underlying sink write() should not be called'); + }); +}, 'promises returned from other writer methods should be rejected when writer abort() happens during sink start()'); + +promise_test(t => { + let writeReject; + let controller; + const ws = new WritableStream({ + write(chunk, c) { + controller = c; + return new Promise((resolve, reject) => { + writeReject = reject; + }); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('a'); + const abortPromise = writer.abort(); + controller.error(error1); + writeReject(error2); + return Promise.all([ + promise_rejects_exactly(t, error2, writePromise, 'write() should reject with error2'), + abortPromise + ]); + }); +}, 'abort() should succeed despite rejection from write'); + +promise_test(t => { + let closeReject; + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + }, + close() { + return new Promise((resolve, reject) => { + closeReject = reject; + }); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const closePromise = writer.close(); + const abortPromise = writer.abort(); + controller.error(error1); + closeReject(error2); + return Promise.all([ + promise_rejects_exactly(t, error2, closePromise, 'close() should reject with error2'), + promise_rejects_exactly(t, error2, abortPromise, 'abort() should reject with error2') + ]); + }); +}, 'abort() should be rejected with the rejection returned from close()'); + +promise_test(t => { + let rejectWrite; + const ws = recordingWritableStream({ + write() { + return new Promise((resolve, reject) => { + rejectWrite = reject; + }); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('1'); + const abortPromise = writer.abort(error2); + rejectWrite(error1); + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise, 'write should reject'), + abortPromise, + promise_rejects_exactly(t, error2, writer.closed, 'closed should reject with error2') + ]); + }).then(() => { + assert_array_equals(ws.events, ['write', '1', 'abort', error2], 'abort sink method should be called'); + }); +}, 'a rejecting sink.write() should not prevent sink.abort() from being called'); + +promise_test(() => { + const ws = recordingWritableStream({ + start() { + return Promise.reject(error1); + } + }); + return ws.abort(error2) + .then(() => { + assert_array_equals(ws.events, ['abort', error2]); + }); +}, 'when start errors after stream abort(), underlying sink abort() should be called anyway'); + +promise_test(() => { + const ws = new WritableStream(); + const abortPromise1 = ws.abort(); + const abortPromise2 = ws.abort(); + assert_equals(abortPromise1, abortPromise2, 'the promises must be the same'); + + return abortPromise1.then( + v => assert_equals(v, undefined, 'abort() should fulfill with undefined')); +}, 'when calling abort() twice on the same stream, both should give the same promise that fulfills with undefined'); + +promise_test(() => { + const ws = new WritableStream(); + const abortPromise1 = ws.abort(); + + return abortPromise1.then(v1 => { + assert_equals(v1, undefined, 'first abort() should fulfill with undefined'); + + const abortPromise2 = ws.abort(); + assert_not_equals(abortPromise2, abortPromise1, 'because we waited, the second promise should be a new promise'); + + return abortPromise2.then(v2 => { + assert_equals(v2, undefined, 'second abort() should fulfill with undefined'); + }); + }); +}, 'when calling abort() twice on the same stream, but sequentially so so there\'s no pending abort the second time, ' + + 'both should fulfill with undefined'); + +promise_test(t => { + const ws = new WritableStream({ + start(c) { + c.error(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.closed, 'writer.closed should reject').then(() => { + return writer.abort().then( + v => assert_equals(v, undefined, 'abort() should fulfill with undefined')); + }); +}, 'calling abort() on an errored stream should fulfill with undefined'); + +promise_test(t => { + let controller; + let resolveWrite; + const ws = recordingWritableStream({ + start(c) { + controller = c; + }, + write() { + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('chunk'); + controller.error(error1); + const abortPromise = writer.abort(error2); + resolveWrite(); + return Promise.all([ + writePromise, + promise_rejects_exactly(t, error1, abortPromise, 'abort() should reject') + ]).then(() => { + assert_array_equals(ws.events, ['write', 'chunk'], 'sink abort() should not be called'); + }); + }); +}, 'sink abort() should not be called if stream was erroring due to controller.error() before abort() was called'); + +promise_test(t => { + let resolveWrite; + let size = 1; + const ws = recordingWritableStream({ + write() { + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }, { + size() { + return size; + }, + highWaterMark: 1 + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise1 = writer.write('chunk1'); + size = NaN; + const writePromise2 = writer.write('chunk2'); + const abortPromise = writer.abort(error2); + resolveWrite(); + return Promise.all([ + writePromise1, + promise_rejects_js(t, RangeError, writePromise2, 'second write() should reject'), + promise_rejects_js(t, RangeError, abortPromise, 'abort() should reject') + ]).then(() => { + assert_array_equals(ws.events, ['write', 'chunk1'], 'sink abort() should not be called'); + }); + }); +}, 'sink abort() should not be called if stream was erroring due to bad strategy before abort() was called'); + +promise_test(t => { + const ws = new WritableStream(); + return ws.abort().then(() => { + const writer = ws.getWriter(); + return writer.closed.then(t.unreached_func('closed promise should not fulfill'), + e => assert_equals(e, undefined, 'e should be undefined')); + }); +}, 'abort with no arguments should set the stored error to undefined'); + +promise_test(t => { + const ws = new WritableStream(); + return ws.abort(undefined).then(() => { + const writer = ws.getWriter(); + return writer.closed.then(t.unreached_func('closed promise should not fulfill'), + e => assert_equals(e, undefined, 'e should be undefined')); + }); +}, 'abort with an undefined argument should set the stored error to undefined'); + +promise_test(t => { + const ws = new WritableStream(); + return ws.abort('string argument').then(() => { + const writer = ws.getWriter(); + return writer.closed.then(t.unreached_func('closed promise should not fulfill'), + e => assert_equals(e, 'string argument', 'e should be \'string argument\'')); + }); +}, 'abort with a string argument should set the stored error to that argument'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + return promise_rejects_js(t, TypeError, ws.abort(), 'abort should reject') + .then(() => writer.ready); +}, 'abort on a locked stream should reject'); + +test(t => { + let ctrl; + const ws = new WritableStream({start(c) { ctrl = c; }}); + const e = Error('hello'); + + assert_true(ctrl.signal instanceof AbortSignal); + assert_false(ctrl.signal.aborted); + assert_equals(ctrl.signal.reason, undefined, 'signal.reason before abort'); + ws.abort(e); + assert_true(ctrl.signal.aborted); + assert_equals(ctrl.signal.reason, e); +}, 'WritableStreamDefaultController.signal'); + +promise_test(async t => { + let ctrl; + let resolve; + const called = new Promise(r => resolve = r); + + const ws = new WritableStream({ + start(c) { ctrl = c; }, + write() { resolve(); return new Promise(() => {}); } + }); + const writer = ws.getWriter(); + + writer.write(99); + await called; + + assert_false(ctrl.signal.aborted); + assert_equals(ctrl.signal.reason, undefined, 'signal.reason before abort'); + writer.abort(); + assert_true(ctrl.signal.aborted); + assert_true(ctrl.signal.reason instanceof DOMException, 'signal.reason is a DOMException'); + assert_equals(ctrl.signal.reason.name, 'AbortError', 'signal.reason is an AbortError'); +}, 'the abort signal is signalled synchronously - write'); + +promise_test(async t => { + let ctrl; + let resolve; + const called = new Promise(r => resolve = r); + + const ws = new WritableStream({ + start(c) { ctrl = c; }, + close() { resolve(); return new Promise(() => {}); } + }); + const writer = ws.getWriter(); + + writer.close(99); + await called; + + assert_false(ctrl.signal.aborted); + writer.abort(); + assert_true(ctrl.signal.aborted); +}, 'the abort signal is signalled synchronously - close'); + +promise_test(async t => { + let ctrl; + const ws = new WritableStream({start(c) { ctrl = c; }}); + const writer = ws.getWriter(); + + const e = TypeError(); + ctrl.error(e); + await promise_rejects_exactly(t, e, writer.closed); + assert_false(ctrl.signal.aborted); +}, 'the abort signal is not signalled on error'); + +promise_test(async t => { + let ctrl; + const e = TypeError(); + const ws = new WritableStream({ + start(c) { ctrl = c; }, + async write() { throw e; } + }); + const writer = ws.getWriter(); + + await promise_rejects_exactly(t, e, writer.write('hello'), 'write result'); + await promise_rejects_exactly(t, e, writer.closed, 'closed'); + assert_false(ctrl.signal.aborted); +}, 'the abort signal is not signalled on write failure'); + +promise_test(async t => { + let ctrl; + const e = TypeError(); + const ws = new WritableStream({ + start(c) { ctrl = c; }, + async close() { throw e; } + }); + const writer = ws.getWriter(); + + await promise_rejects_exactly(t, e, writer.close(), 'close result'); + await promise_rejects_exactly(t, e, writer.closed, 'closed'); + assert_false(ctrl.signal.aborted); +}, 'the abort signal is not signalled on close failure'); + +promise_test(async t => { + let ctrl; + let abortPromise; + let abortPromiseFromSignal; + const e1 = SyntaxError(); + const e2 = TypeError(); + const ws = new WritableStream({ + start(c) { ctrl = c; }, + }); + + const writer = ws.getWriter(); + ctrl.signal.addEventListener('abort', () => { + abortPromiseFromSignal = writer.abort(e2); + }); + abortPromise = writer.abort(e1); + assert_true(ctrl.signal.aborted); + + await Promise.all([ + abortPromise, + abortPromiseFromSignal, + promise_rejects_exactly(t, e2, writer.closed, 'closed') + ]); +}, 'recursive abort() call from abort() aborting signal (not started)'); + +promise_test(async t => { + let ctrl; + let abortPromise; + let abortPromiseFromSignal; + const e1 = SyntaxError(); + const e2 = TypeError(); + const ws = new WritableStream({ + start(c) { ctrl = c; }, + }); + await flushAsyncEvents(); // ensure stream is started + + const writer = ws.getWriter(); + ctrl.signal.addEventListener('abort', () => { + abortPromiseFromSignal = writer.abort(e2); + }); + abortPromise = writer.abort(e1); + assert_true(ctrl.signal.aborted); + + await Promise.all([ + abortPromise, + abortPromiseFromSignal, + promise_rejects_exactly(t, e2, writer.closed, 'closed') + ]); +}, 'recursive abort() call from abort() aborting signal'); + +promise_test(async t => { + let ctrl; + let abortPromise; + let closePromiseFromSignal; + const theError = SyntaxError(); + const ws = new WritableStream({ + start(c) { ctrl = c; }, + }); + + const writer = ws.getWriter(); + ctrl.signal.addEventListener('abort', () => { + closePromiseFromSignal = writer.close(); + }); + abortPromise = writer.abort(theError); + assert_true(ctrl.signal.aborted); + + await Promise.all([ + abortPromise, + promise_rejects_exactly(t, theError, closePromiseFromSignal, 'closed'), + promise_rejects_exactly(t, theError, writer.closed, 'closed') + ]); +}, 'recursive close() call from abort() aborting signal (not started)'); + +promise_test(async t => { + let ctrl; + let abortPromise; + let closePromiseFromSignal; + const theError = SyntaxError(); + const ws = new WritableStream({ + start(c) { ctrl = c; }, + }); + await flushAsyncEvents(); // ensure stream is started + + const writer = ws.getWriter(); + ctrl.signal.addEventListener('abort', () => { + closePromiseFromSignal = writer.close(); + }); + abortPromise = writer.abort(theError); + assert_true(ctrl.signal.aborted); + + await Promise.all([ + abortPromise, + closePromiseFromSignal, + writer.closed + ]); +}, 'recursive close() call from abort() aborting signal'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/bad-strategies.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/bad-strategies.any.js new file mode 100644 index 000000000000..63fa443065ee --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/bad-strategies.any.js @@ -0,0 +1,95 @@ +// META: global=window,worker +'use strict'; + +const error1 = new Error('a unique string'); +error1.name = 'error1'; + +test(() => { + assert_throws_exactly(error1, () => { + new WritableStream({}, { + get size() { + throw error1; + }, + highWaterMark: 5 + }); + }, 'construction should re-throw the error'); +}, 'Writable stream: throwing strategy.size getter'); + +test(() => { + assert_throws_js(TypeError, () => { + new WritableStream({}, { size: 'a string' }); + }); +}, 'reject any non-function value for strategy.size'); + +test(() => { + assert_throws_exactly(error1, () => { + new WritableStream({}, { + size() { + return 1; + }, + get highWaterMark() { + throw error1; + } + }); + }, 'construction should re-throw the error'); +}, 'Writable stream: throwing strategy.highWaterMark getter'); + +test(() => { + + for (const highWaterMark of [-1, -Infinity, NaN, 'foo', {}]) { + assert_throws_js(RangeError, () => { + new WritableStream({}, { + size() { + return 1; + }, + highWaterMark + }); + }, `construction should throw a RangeError for ${highWaterMark}`); + } +}, 'Writable stream: invalid strategy.highWaterMark'); + +promise_test(t => { + const ws = new WritableStream({}, { + size() { + throw error1; + }, + highWaterMark: 5 + }); + + const writer = ws.getWriter(); + + const p1 = promise_rejects_exactly(t, error1, writer.write('a'), 'write should reject with the thrown error'); + + const p2 = promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the thrown error'); + + return Promise.all([p1, p2]); +}, 'Writable stream: throwing strategy.size method'); + +promise_test(() => { + const sizes = [NaN, -Infinity, Infinity, -1]; + return Promise.all(sizes.map(size => { + const ws = new WritableStream({}, { + size() { + return size; + }, + highWaterMark: 5 + }); + + const writer = ws.getWriter(); + + return writer.write('a').then(() => assert_unreached('write must reject'), writeE => { + assert_equals(writeE.name, 'RangeError', `write must reject with a RangeError for ${size}`); + + return writer.closed.then(() => assert_unreached('write must reject'), closedE => { + assert_equals(closedE, writeE, `closed should reject with the same error as write`); + }); + }); + })); +}, 'Writable stream: invalid strategy.size return value'); + +test(() => { + assert_throws_js(TypeError, () => new WritableStream(undefined, { + size: 'not a function', + highWaterMark: NaN + }), 'WritableStream constructor should throw a TypeError'); +}, 'Writable stream: invalid size beats invalid highWaterMark'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/bad-underlying-sinks.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/bad-underlying-sinks.any.js new file mode 100644 index 000000000000..d0b3467978ea --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/bad-underlying-sinks.any.js @@ -0,0 +1,204 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +test(() => { + assert_throws_exactly(error1, () => { + new WritableStream({ + get start() { + throw error1; + } + }); + }, 'constructor should throw same error as throwing start getter'); + + assert_throws_exactly(error1, () => { + new WritableStream({ + start() { + throw error1; + } + }); + }, 'constructor should throw same error as throwing start method'); + + assert_throws_js(TypeError, () => { + new WritableStream({ + start: 'not a function or undefined' + }); + }, 'constructor should throw TypeError when passed a non-function start property'); + + assert_throws_js(TypeError, () => { + new WritableStream({ + start: { apply() {} } + }); + }, 'constructor should throw TypeError when passed a non-function start property with an .apply method'); +}, 'start: errors in start cause WritableStream constructor to throw'); + +promise_test(t => { + + const ws = recordingWritableStream({ + close() { + throw error1; + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.close(), 'close() promise must reject with the thrown error') + .then(() => promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the thrown error')) + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'closed promise must reject with the thrown error')) + .then(() => { + assert_array_equals(ws.events, ['close']); + }); + +}, 'close: throwing method should cause writer close() and ready to reject'); + +promise_test(t => { + + const ws = recordingWritableStream({ + close() { + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.close(), 'close() promise must reject with the same error') + .then(() => promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the same error')) + .then(() => assert_array_equals(ws.events, ['close'])); + +}, 'close: returning a rejected promise should cause writer close() and ready to reject'); + +test(() => { + assert_throws_exactly(error1, () => new WritableStream({ + get close() { + throw error1; + } + }), 'constructor should throw'); +}, 'close: throwing getter should cause constructor to throw'); + +test(() => { + assert_throws_exactly(error1, () => new WritableStream({ + get write() { + throw error1; + } + }), 'constructor should throw'); +}, 'write: throwing getter should cause write() and closed to reject'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + throw error1; + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('a'), 'write should reject with the thrown error') + .then(() => promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the thrown error')); +}, 'write: throwing method should cause write() and closed to reject'); + +promise_test(t => { + + let rejectSinkWritePromise; + const ws = recordingWritableStream({ + write() { + return new Promise((r, reject) => { + rejectSinkWritePromise = reject; + }); + } + }); + + return flushAsyncEvents().then(() => { + const writer = ws.getWriter(); + const writePromise = writer.write('a'); + rejectSinkWritePromise(error1); + + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise, 'writer write must reject with the same error'), + promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the same error') + ]); + }) + .then(() => { + assert_array_equals(ws.events, ['write', 'a']); + }); + +}, 'write: returning a promise that becomes rejected after the writer write() should cause writer write() and ready ' + + 'to reject'); + +promise_test(t => { + + const ws = recordingWritableStream({ + write() { + if (ws.events.length === 2) { + return delay(0); + } + + return Promise.reject(error1); + } + }); + + const writer = ws.getWriter(); + + // Do not wait for this; we want to test the ready promise when the stream is "full" (desiredSize = 0), but if we wait + // then the stream will transition back to "empty" (desiredSize = 1) + writer.write('a'); + const readyPromise = writer.ready; + + return promise_rejects_exactly(t, error1, writer.write('b'), 'second write must reject with the same error').then(() => { + assert_equals(writer.ready, readyPromise, + 'the ready promise must not change, since the queue was full after the first write, so the pending one simply ' + + 'transitioned'); + return promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the same error'); + }) + .then(() => assert_array_equals(ws.events, ['write', 'a', 'write', 'b'])); + +}, 'write: returning a rejected promise (second write) should cause writer write() and ready to reject'); + +test(() => { + assert_throws_js(TypeError, () => new WritableStream({ + start: 'test' + }), 'constructor should throw'); +}, 'start: non-function start method'); + +test(() => { + assert_throws_js(TypeError, () => new WritableStream({ + write: 'test' + }), 'constructor should throw'); +}, 'write: non-function write method'); + +test(() => { + assert_throws_js(TypeError, () => new WritableStream({ + close: 'test' + }), 'constructor should throw'); +}, 'close: non-function close method'); + +test(() => { + assert_throws_js(TypeError, () => new WritableStream({ + abort: { apply() {} } + }), 'constructor should throw'); +}, 'abort: non-function abort method with .apply'); + +test(() => { + assert_throws_exactly(error1, () => new WritableStream({ + get abort() { + throw error1; + } + }), 'constructor should throw'); +}, 'abort: throwing getter should cause abort() and closed to reject'); + +promise_test(t => { + const abortReason = new Error('different string'); + const ws = new WritableStream({ + abort() { + throw error1; + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.abort(abortReason), 'abort should reject with the thrown error') + .then(() => promise_rejects_exactly(t, abortReason, writer.closed, 'closed should reject with abortReason')); +}, 'abort: throwing method should cause abort() and closed to reject'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/byte-length-queuing-strategy.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/byte-length-queuing-strategy.any.js new file mode 100644 index 000000000000..ce1962e8917f --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/byte-length-queuing-strategy.any.js @@ -0,0 +1,28 @@ +// META: global=window,worker +'use strict'; + +promise_test(t => { + let isDone = false; + const ws = new WritableStream( + { + write() { + return new Promise(resolve => { + t.step_timeout(() => { + isDone = true; + resolve(); + }, 200); + }); + }, + + close() { + assert_true(isDone, 'close is only called once the promise has been resolved'); + } + }, + new ByteLengthQueuingStrategy({ highWaterMark: 1024 * 16 }) + ); + + const writer = ws.getWriter(); + writer.write({ byteLength: 1024 }); + + return writer.close(); +}, 'Closing a writable stream with in-flight writes below the high water mark delays the close call properly'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/close.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/close.any.js new file mode 100644 index 000000000000..9c1bc93b011d --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/close.any.js @@ -0,0 +1,481 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +const error2 = new Error('error2'); +error2.name = 'error2'; + +promise_test(() => { + const ws = new WritableStream({ + close() { + return 'Hello'; + } + }); + + const writer = ws.getWriter(); + + const closePromise = writer.close(); + return closePromise.then(value => assert_equals(value, undefined, 'fulfillment value must be undefined')); +}, 'fulfillment value of writer.close() call must be undefined even if the underlying sink returns a non-undefined ' + + 'value'); + +promise_test(() => { + let controller; + let resolveClose; + const ws = new WritableStream({ + start(c) { + controller = c; + }, + close() { + return new Promise(resolve => { + resolveClose = resolve; + }); + } + }); + + const writer = ws.getWriter(); + + const closePromise = writer.close(); + return flushAsyncEvents().then(() => { + controller.error(error1); + return flushAsyncEvents(); + }).then(() => { + resolveClose(); + return Promise.all([ + closePromise, + writer.closed, + flushAsyncEvents().then(() => writer.closed)]); + }); +}, 'when sink calls error asynchronously while sink close is in-flight, the stream should not become errored'); + +promise_test(() => { + let controller; + const passedError = new Error('error me'); + const ws = new WritableStream({ + start(c) { + controller = c; + }, + close() { + controller.error(passedError); + } + }); + + const writer = ws.getWriter(); + + return writer.close().then(() => writer.closed); +}, 'when sink calls error synchronously while closing, the stream should not become errored'); + +promise_test(t => { + const ws = new WritableStream({ + close() { + throw error1; + } + }); + + const writer = ws.getWriter(); + + return Promise.all([ + writer.write('y'), + promise_rejects_exactly(t, error1, writer.close(), 'close() must reject with the error'), + promise_rejects_exactly(t, error1, writer.closed, 'closed must reject with the error') + ]); +}, 'when the sink throws during close, and the close is requested while a write is still in-flight, the stream should ' + + 'become errored during the close'); + +promise_test(() => { + const ws = new WritableStream({ + write(chunk, controller) { + controller.error(error1); + return new Promise(() => {}); + } + }); + + const writer = ws.getWriter(); + writer.write('a'); + + return delay(0).then(() => { + writer.releaseLock(); + }); +}, 'releaseLock on a stream with a pending write in which the stream has been errored'); + +promise_test(() => { + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + }, + close() { + controller.error(error1); + return new Promise(() => {}); + } + }); + + const writer = ws.getWriter(); + writer.close(); + + return delay(0).then(() => { + writer.releaseLock(); + }); +}, 'releaseLock on a stream with a pending close in which controller.error() was called'); + +promise_test(() => { + const ws = recordingWritableStream(); + + const writer = ws.getWriter(); + + return writer.ready.then(() => { + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); + + writer.close(); + assert_equals(writer.desiredSize, 1, 'desiredSize should be still 1'); + + return writer.ready.then(v => { + assert_equals(v, undefined, 'ready promise should be fulfilled with undefined'); + assert_array_equals(ws.events, ['close'], 'write and abort should not be called'); + }); + }); +}, 'when close is called on a WritableStream in writable state, ready should return a fulfilled promise'); + +promise_test(() => { + const ws = recordingWritableStream({ + write() { + return new Promise(() => {}); + } + }); + + const writer = ws.getWriter(); + + return writer.ready.then(() => { + writer.write('a'); + + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0'); + + let calledClose = false; + return Promise.all([ + writer.ready.then(v => { + assert_equals(v, undefined, 'ready promise should be fulfilled with undefined'); + assert_true(calledClose, 'ready should not be fulfilled before writer.close() is called'); + assert_array_equals(ws.events, ['write', 'a'], 'sink abort() should not be called'); + }), + flushAsyncEvents().then(() => { + writer.close(); + calledClose = true; + }) + ]); + }); +}, 'when close is called on a WritableStream in waiting state, ready promise should be fulfilled'); + +promise_test(() => { + let asyncCloseFinished = false; + const ws = recordingWritableStream({ + close() { + return flushAsyncEvents().then(() => { + asyncCloseFinished = true; + }); + } + }); + + const writer = ws.getWriter(); + return writer.ready.then(() => { + writer.write('a'); + + writer.close(); + + return writer.ready.then(v => { + assert_false(asyncCloseFinished, 'ready promise should be fulfilled before async close completes'); + assert_equals(v, undefined, 'ready promise should be fulfilled with undefined'); + assert_array_equals(ws.events, ['write', 'a', 'close'], 'sink abort() should not be called'); + }); + }); +}, 'when close is called on a WritableStream in waiting state, ready should be fulfilled immediately even if close ' + + 'takes a long time'); + +promise_test(t => { + const rejection = { name: 'letter' }; + const ws = new WritableStream({ + close() { + return { + then(onFulfilled, onRejected) { onRejected(rejection); } + }; + } + }); + return promise_rejects_exactly(t, rejection, ws.getWriter().close(), 'close() should return a rejection'); +}, 'returning a thenable from close() should work'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const closePromise = writer.close(); + const closedPromise = writer.closed; + writer.releaseLock(); + return Promise.all([ + closePromise, + promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected') + ]); + }); +}, 'releaseLock() should not change the result of sync close()'); + +promise_test(t => { + const ws = new WritableStream({ + close() { + return flushAsyncEvents(); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const closePromise = writer.close(); + const closedPromise = writer.closed; + writer.releaseLock(); + return Promise.all([ + closePromise, + promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected') + ]); + }); +}, 'releaseLock() should not change the result of async close()'); + +promise_test(() => { + let resolveClose; + const ws = new WritableStream({ + close() { + const promise = new Promise(resolve => { + resolveClose = resolve; + }); + return promise; + } + }); + const writer = ws.getWriter(); + const closePromise = writer.close(); + writer.releaseLock(); + return delay(0).then(() => { + resolveClose(); + return closePromise.then(() => { + assert_equals(ws.getWriter().desiredSize, 0, 'desiredSize should be 0'); + }); + }); +}, 'close() should set state to CLOSED even if writer has detached'); + +promise_test(() => { + let resolveClose; + const ws = new WritableStream({ + close() { + const promise = new Promise(resolve => { + resolveClose = resolve; + }); + return promise; + } + }); + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + return delay(0).then(() => { + const abortingWriter = ws.getWriter(); + const abortPromise = abortingWriter.abort(); + abortingWriter.releaseLock(); + resolveClose(); + return abortPromise; + }); +}, 'the promise returned by async abort during close should resolve'); + +// Though the order in which the promises are fulfilled or rejected is arbitrary, we're checking it for +// interoperability. We can change the order as long as we file bugs on all implementers to update to the latest tests +// to keep them interoperable. + +promise_test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + + const closePromise = writer.close(); + + const events = []; + return Promise.all([ + closePromise.then(() => { + events.push('closePromise'); + }), + writer.closed.then(() => { + events.push('closed'); + }) + ]).then(() => { + assert_array_equals(events, ['closePromise', 'closed'], + 'promises must fulfill/reject in the expected order'); + }); +}, 'promises must fulfill/reject in the expected order on closure'); + +promise_test(() => { + const ws = new WritableStream({}); + + // Wait until the WritableStream starts so that the close() call gets processed. Otherwise, abort() will be + // processed without waiting for completion of the close(). + return delay(0).then(() => { + const writer = ws.getWriter(); + + const closePromise = writer.close(); + const abortPromise = writer.abort(error1); + + const events = []; + return Promise.all([ + closePromise.then(() => { + events.push('closePromise'); + }), + abortPromise.then(() => { + events.push('abortPromise'); + }), + writer.closed.then(() => { + events.push('closed'); + }) + ]).then(() => { + assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'], + 'promises must fulfill/reject in the expected order'); + }); + }); +}, 'promises must fulfill/reject in the expected order on aborted closure'); + +promise_test(t => { + const ws = new WritableStream({ + close() { + return Promise.reject(error1); + } + }); + + // Wait until the WritableStream starts so that the close() call gets processed. + return delay(0).then(() => { + const writer = ws.getWriter(); + + const closePromise = writer.close(); + const abortPromise = writer.abort(error2); + + const events = []; + closePromise.catch(() => events.push('closePromise')); + abortPromise.catch(() => events.push('abortPromise')); + writer.closed.catch(() => events.push('closed')); + return Promise.all([ + promise_rejects_exactly(t, error1, closePromise, + 'closePromise must reject with the error returned from the sink\'s close method'), + promise_rejects_exactly(t, error1, abortPromise, + 'abortPromise must reject with the error returned from the sink\'s close method'), + promise_rejects_exactly(t, error2, writer.closed, + 'writer.closed must reject with error2') + ]).then(() => { + assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'], + 'promises must fulfill/reject in the expected order'); + }); + }); +}, 'promises must fulfill/reject in the expected order on aborted and errored closure'); + +promise_test(t => { + let resolveWrite; + let controller; + const ws = new WritableStream({ + write(chunk, c) { + controller = c; + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + const writer = ws.getWriter(); + return writer.ready.then(() => { + const writePromise = writer.write('c'); + controller.error(error1); + const closePromise = writer.close(); + let closeRejected = false; + closePromise.catch(() => { + closeRejected = true; + }); + return flushAsyncEvents().then(() => { + assert_false(closeRejected); + resolveWrite(); + return Promise.all([ + writePromise, + promise_rejects_exactly(t, error1, closePromise, 'close() should reject') + ]).then(() => { + assert_true(closeRejected); + }); + }); + }); +}, 'close() should not reject until no sink methods are in flight'); + +promise_test(() => { + const ws = new WritableStream(); + const writer1 = ws.getWriter(); + return writer1.close().then(() => { + writer1.releaseLock(); + const writer2 = ws.getWriter(); + const ready = writer2.ready; + assert_equals(ready.constructor, Promise); + return ready; + }); +}, 'ready promise should be initialised as fulfilled for a writer on a closed stream'); + +promise_test(() => { + const ws = new WritableStream(); + ws.close(); + const writer = ws.getWriter(); + return writer.closed; +}, 'close() on a writable stream should work'); + +promise_test(t => { + const ws = new WritableStream(); + ws.getWriter(); + return promise_rejects_js(t, TypeError, ws.close(), 'close should reject'); +}, 'close() on a locked stream should reject'); + +promise_test(t => { + const ws = new WritableStream({ + start(controller) { + controller.error(error1); + } + }); + return promise_rejects_exactly(t, error1, ws.close(), 'close should reject with error1'); +}, 'close() on an erroring stream should reject'); + +promise_test(t => { + const ws = new WritableStream({ + start(controller) { + controller.error(error1); + } + }); + const writer = ws.getWriter(); + return promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the error').then(() => { + writer.releaseLock(); + return promise_rejects_js(t, TypeError, ws.close(), 'close should reject'); + }); +}, 'close() on an errored stream should reject'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + return writer.close().then(() => { + return promise_rejects_js(t, TypeError, ws.close(), 'close should reject'); + }); +}, 'close() on an closed stream should reject'); + +promise_test(t => { + const ws = new WritableStream({ + close() { + return new Promise(() => {}); + } + }); + + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + return promise_rejects_js(t, TypeError, ws.close(), 'close should reject'); +}, 'close() on a stream with a pending close should reject'); + +// See https://github.com/whatwg/streams/issues/1341. +promise_test(async t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + + await writer.write(1); + await writer.close(); + + return promise_rejects_js(t, TypeError, writer.write(2), 'write should reject'); +}, 'write() on a closed stream should reject'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/constructor.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/constructor.any.js new file mode 100644 index 000000000000..ba54e39cdbc5 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/constructor.any.js @@ -0,0 +1,159 @@ +// META: global=window,worker +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +const error2 = new Error('error2'); +error2.name = 'error2'; + +promise_test(() => { + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + } + }); + + // Now error the stream after its construction. + controller.error(error1); + + const writer = ws.getWriter(); + + assert_equals(writer.desiredSize, null, 'desiredSize should be null'); + return writer.closed.catch(r => { + assert_equals(r, error1, 'ws should be errored by the passed error'); + }); +}, 'controller argument should be passed to start method'); + +promise_test(t => { + const ws = new WritableStream({ + write(chunk, controller) { + controller.error(error1); + } + }); + + const writer = ws.getWriter(); + + return Promise.all([ + writer.write('a'), + promise_rejects_exactly(t, error1, writer.closed, 'controller.error() in write() should error the stream') + ]); +}, 'controller argument should be passed to write method'); + +// Older versions of the standard had the controller argument passed to close(). It wasn't useful, and so has been +// removed. This test remains to identify implementations that haven't been updated. +promise_test(t => { + const ws = new WritableStream({ + close(...args) { + t.step(() => { + assert_array_equals(args, [], 'no arguments should be passed to close'); + }); + } + }); + + return ws.getWriter().close(); +}, 'controller argument should not be passed to close method'); + +promise_test(() => { + const ws = new WritableStream({}, { + highWaterMark: 1000, + size() { return 1; } + }); + + const writer = ws.getWriter(); + + assert_equals(writer.desiredSize, 1000, 'desiredSize should be 1000'); + return writer.ready.then(v => { + assert_equals(v, undefined, 'ready promise should fulfill with undefined'); + }); +}, 'highWaterMark should be reflected to desiredSize'); + +promise_test(() => { + const ws = new WritableStream({}, { + highWaterMark: Infinity, + size() { return 0; } + }); + + const writer = ws.getWriter(); + + assert_equals(writer.desiredSize, Infinity, 'desiredSize should be Infinity'); + + return writer.ready; +}, 'WritableStream should be writable and ready should fulfill immediately if the strategy does not apply ' + + 'backpressure'); + +test(() => { + new WritableStream(); +}, 'WritableStream should be constructible with no arguments'); + +test(() => { + assert_throws_js(RangeError, () => new WritableStream({ type: 'bytes' }), 'constructor should throw'); +}, `WritableStream can't be constructed with a defined type`); + +test(() => { + const underlyingSink = { get start() { throw error1; } }; + const queuingStrategy = { highWaterMark: 0, get size() { throw error2; } }; + + // underlyingSink is converted in prose in the method body, whereas queuingStrategy is done at the IDL layer. + // So the queuingStrategy exception should be encountered first. + assert_throws_exactly(error2, () => new WritableStream(underlyingSink, queuingStrategy)); +}, 'underlyingSink argument should be converted after queuingStrategy argument'); + +test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + + assert_equals(typeof writer.write, 'function', 'writer should have a write method'); + assert_equals(typeof writer.abort, 'function', 'writer should have an abort method'); + assert_equals(typeof writer.close, 'function', 'writer should have a close method'); + + assert_equals(writer.desiredSize, 1, 'desiredSize should start at 1'); + + assert_not_equals(typeof writer.ready, 'undefined', 'writer should have a ready property'); + assert_equals(typeof writer.ready.then, 'function', 'ready property should be thenable'); + assert_not_equals(typeof writer.closed, 'undefined', 'writer should have a closed property'); + assert_equals(typeof writer.closed.then, 'function', 'closed property should be thenable'); +}, 'WritableStream instances should have standard methods and properties'); + +test(() => { + let WritableStreamDefaultController; + new WritableStream({ + start(c) { + WritableStreamDefaultController = c.constructor; + } + }); + + assert_throws_js(TypeError, () => new WritableStreamDefaultController({}), + 'constructor should throw a TypeError exception'); +}, 'WritableStreamDefaultController constructor should throw'); + +test(() => { + let WritableStreamDefaultController; + const stream = new WritableStream({ + start(c) { + WritableStreamDefaultController = c.constructor; + } + }); + + assert_throws_js(TypeError, () => new WritableStreamDefaultController(stream), + 'constructor should throw a TypeError exception'); +}, 'WritableStreamDefaultController constructor should throw when passed an initialised WritableStream'); + +test(() => { + const stream = new WritableStream(); + const writer = stream.getWriter(); + const WritableStreamDefaultWriter = writer.constructor; + writer.releaseLock(); + assert_throws_js(TypeError, () => new WritableStreamDefaultWriter({}), + 'constructor should throw a TypeError exception'); +}, 'WritableStreamDefaultWriter should throw unless passed a WritableStream'); + +test(() => { + const stream = new WritableStream(); + const writer = stream.getWriter(); + const WritableStreamDefaultWriter = writer.constructor; + assert_throws_js(TypeError, () => new WritableStreamDefaultWriter(stream), + 'constructor should throw a TypeError exception'); +}, 'WritableStreamDefaultWriter constructor should throw when stream argument is locked'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/count-queuing-strategy.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/count-queuing-strategy.any.js new file mode 100644 index 000000000000..064e16e81506 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/count-queuing-strategy.any.js @@ -0,0 +1,124 @@ +// META: global=window,worker +'use strict'; + +test(() => { + new WritableStream({}, new CountQueuingStrategy({ highWaterMark: 4 })); +}, 'Can construct a writable stream with a valid CountQueuingStrategy'); + +promise_test(() => { + const dones = Object.create(null); + + const ws = new WritableStream( + { + write(chunk) { + return new Promise(resolve => { + dones[chunk] = resolve; + }); + } + }, + new CountQueuingStrategy({ highWaterMark: 0 }) + ); + + const writer = ws.getWriter(); + let writePromiseB; + let writePromiseC; + + return Promise.resolve().then(() => { + assert_equals(writer.desiredSize, 0, 'desiredSize should be initially 0'); + + const writePromiseA = writer.write('a'); + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 1st write()'); + + writePromiseB = writer.write('b'); + assert_equals(writer.desiredSize, -2, 'desiredSize should be -2 after 2nd write()'); + + dones.a(); + return writePromiseA; + }).then(() => { + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after completing 1st write()'); + + dones.b(); + return writePromiseB; + }).then(() => { + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 2nd write()'); + + writePromiseC = writer.write('c'); + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 3rd write()'); + + dones.c(); + return writePromiseC; + }).then(() => { + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 3rd write()'); + }); +}, 'Correctly governs the value of a WritableStream\'s state property (HWM = 0)'); + +promise_test(() => { + const dones = Object.create(null); + + const ws = new WritableStream( + { + write(chunk) { + return new Promise(resolve => { + dones[chunk] = resolve; + }); + } + }, + new CountQueuingStrategy({ highWaterMark: 4 }) + ); + + const writer = ws.getWriter(); + let writePromiseB; + let writePromiseC; + let writePromiseD; + + return Promise.resolve().then(() => { + assert_equals(writer.desiredSize, 4, 'desiredSize should be initially 4'); + + const writePromiseA = writer.write('a'); + assert_equals(writer.desiredSize, 3, 'desiredSize should be 3 after 1st write()'); + + writePromiseB = writer.write('b'); + assert_equals(writer.desiredSize, 2, 'desiredSize should be 2 after 2nd write()'); + + writePromiseC = writer.write('c'); + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1 after 3rd write()'); + + writePromiseD = writer.write('d'); + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after 4th write()'); + + writer.write('e'); + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 5th write()'); + + writer.write('f'); + assert_equals(writer.desiredSize, -2, 'desiredSize should be -2 after 6th write()'); + + writer.write('g'); + assert_equals(writer.desiredSize, -3, 'desiredSize should be -3 after 7th write()'); + + dones.a(); + return writePromiseA; + }).then(() => { + assert_equals(writer.desiredSize, -2, 'desiredSize should be -2 after completing 1st write()'); + + dones.b(); + return writePromiseB; + }).then(() => { + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after completing 2nd write()'); + + dones.c(); + return writePromiseC; + }).then(() => { + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 3rd write()'); + + writer.write('h'); + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 8th write()'); + + dones.d(); + return writePromiseD; + }).then(() => { + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 4th write()'); + + writer.write('i'); + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 9th write()'); + }); +}, 'Correctly governs the value of a WritableStream\'s state property (HWM = 4)'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/crashtests/garbage-collection.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/crashtests/garbage-collection.any.js new file mode 100644 index 000000000000..9f64e9b7a8ac --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/crashtests/garbage-collection.any.js @@ -0,0 +1,90 @@ +// META: global=window,worker +// META: script=/common/gc.js +'use strict'; + +// See https://crbug.com/390646657 for details. +promise_test(async () => { + const written = new WritableStream({ + write(chunk) { + return new Promise(resolve => {}); + } + }).getWriter().write('just nod if you can hear me'); + for (let i = 0; i < 5; ++i) + await garbageCollect(); +}, 'Garbage-collecting a stream writer with a pending write should not crash'); + +promise_test(async () => { + const closed = new WritableStream({ + write(chunk) { } + }).getWriter().closed; + for (let i = 0; i < 5; ++i) + await garbageCollect(); +}, 'Garbage-collecting a stream writer should not crash with closed promise is retained'); + +promise_test(async () => { + let writer = new WritableStream({ + write(chunk) { return new Promise(resolve => {}); }, + close() { return new Promise(resolve => {}); } + }).getWriter(); + writer.write('is there anyone home?'); + writer.close(); + writer = null; + for (let i = 0; i < 5; ++i) + await garbageCollect(); +}, 'Garbage-collecting a stream writer should not crash with close promise pending'); + +promise_test(async () => { + const ready = new WritableStream({ + write(chunk) { } + }, {highWaterMark: 0}).getWriter().ready; + for (let i = 0; i < 5; ++i) + await garbageCollect(); +}, 'Garbage-collecting a stream writer should not crash when backpressure is being applied'); + +// Repro for https://crbug.com/455800266 +promise_test(async () => { + // This logic is wrapped in a function to make it easy to garbage collect all + // references to the WritableStream. + const createWritableStream = async () => { + const WRITE_COUNT = 2; + let writes_done = 0; + const { promise, resolve } = Promise.withResolvers(); + + const ws = new WritableStream({ + write() { + if (writes_done === WRITE_COUNT) { + // Will never resolve, leaving the write operation pending. + return new Promise(resolve => { }); + } + ++writes_done; + return promise; + } + }); + + const writer = ws.getWriter(); + await writer.ready; + + const writeChunks = () => { + for (let i = 0; i < WRITE_COUNT; ++i) { + const ready = writer.ready; + writer.write("chunk"); + } + }; + + // Apply backpressure. + writeChunks(); + + // Release backpressure. + resolve(); + await writer.ready; + + // Apply backpressure again. + writeChunks(); + }; + + await createWritableStream(); + + for (let i = 0; i < 5; ++i) { + await garbageCollect(); + } +}, "WritableStream should not crash when garbage collected with backpressure"); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/error.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/error.any.js new file mode 100644 index 000000000000..faf3fdd95214 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/error.any.js @@ -0,0 +1,64 @@ +// META: global=window,worker +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +const error2 = new Error('error2'); +error2.name = 'error2'; + +promise_test(t => { + const ws = new WritableStream({ + start(controller) { + controller.error(error1); + } + }); + return promise_rejects_exactly(t, error1, ws.getWriter().closed, 'stream should be errored'); +}, 'controller.error() should error the stream'); + +test(() => { + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + } + }); + ws.abort(); + controller.error(error1); +}, 'controller.error() on erroring stream should not throw'); + +promise_test(t => { + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + } + }); + controller.error(error1); + controller.error(error2); + return promise_rejects_exactly(t, error1, ws.getWriter().closed, 'first controller.error() should win'); +}, 'surplus calls to controller.error() should be a no-op'); + +promise_test(() => { + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + } + }); + return ws.abort().then(() => { + controller.error(error1); + }); +}, 'controller.error() on errored stream should not throw'); + +promise_test(() => { + let controller; + const ws = new WritableStream({ + start(c) { + controller = c; + } + }); + return ws.getWriter().close().then(() => { + controller.error(error1); + }); +}, 'controller.error() on closed stream should not throw'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/floating-point-total-queue-size.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/floating-point-total-queue-size.any.js new file mode 100644 index 000000000000..bd34cc53a695 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/floating-point-total-queue-size.any.js @@ -0,0 +1,87 @@ +// META: global=window,worker +'use strict'; + +// Due to the limitations of floating-point precision, the calculation of desiredSize sometimes gives different answers +// than adding up the items in the queue would. It is important that implementations give the same result in these edge +// cases so that developers do not come to depend on non-standard behaviour. See +// https://github.com/whatwg/streams/issues/582 and linked issues for further discussion. + +promise_test(() => { + const writer = setupTestStream(); + + const writePromises = [ + writer.write(2), + writer.write(Number.MAX_SAFE_INTEGER) + ]; + + assert_equals(writer.desiredSize, 0 - 2 - Number.MAX_SAFE_INTEGER, + 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing two chunks)'); + + return Promise.all(writePromises).then(() => { + assert_equals(writer.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative'); + }); +}, 'Floating point arithmetic must manifest near NUMBER.MAX_SAFE_INTEGER (total ends up positive)'); + +promise_test(() => { + const writer = setupTestStream(); + + const writePromises = [ + writer.write(1e-16), + writer.write(1) + ]; + + assert_equals(writer.desiredSize, 0 - 1e-16 - 1, + 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing two chunks)'); + + return Promise.all(writePromises).then(() => { + assert_equals(writer.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative'); + }); +}, 'Floating point arithmetic must manifest near 0 (total ends up positive, but clamped)'); + +promise_test(() => { + const writer = setupTestStream(); + + const writePromises = [ + writer.write(1e-16), + writer.write(1), + writer.write(2e-16) + ]; + + assert_equals(writer.desiredSize, 0 - 1e-16 - 1 - 2e-16, + 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing three chunks)'); + + return Promise.all(writePromises).then(() => { + assert_equals(writer.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1 + 2e-16, + 'desiredSize must be calculated using floating-point arithmetic (after the three chunks have finished writing)'); + }); +}, 'Floating point arithmetic must manifest near 0 (total ends up positive, and not clamped)'); + +promise_test(() => { + const writer = setupTestStream(); + + const writePromises = [ + writer.write(2e-16), + writer.write(1) + ]; + + assert_equals(writer.desiredSize, 0 - 2e-16 - 1, + 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing two chunks)'); + + return Promise.all(writePromises).then(() => { + assert_equals(writer.desiredSize, 0 - 2e-16 - 1 + 2e-16 + 1, + 'desiredSize must be calculated using floating-point arithmetic (after the two chunks have finished writing)'); + }); +}, 'Floating point arithmetic must manifest near 0 (total ends up zero)'); + +function setupTestStream() { + const strategy = { + size(x) { + return x; + }, + highWaterMark: 0 + }; + + const ws = new WritableStream({}, strategy); + + return ws.getWriter(); +} diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/garbage-collection.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/garbage-collection.any.js new file mode 100644 index 000000000000..a5d935c9aa00 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/garbage-collection.any.js @@ -0,0 +1,21 @@ +// META: global=window,worker +// META: script=/common/gc.js +'use strict'; + +promise_test(async () => { + + let written = false; + const promise = (() => { + const rs = new WritableStream({ + write() { + written = true; + } + }); + const writer = rs.getWriter(); + return writer.write('something'); + })(); + await garbageCollect(); + await promise; + assert_true(written); + +}, 'A WritableStream and its writer should not be garbage collected while there is a write promise pending'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/general.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/general.any.js new file mode 100644 index 000000000000..cede7fd0845b --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/general.any.js @@ -0,0 +1,277 @@ +// META: global=window,worker +'use strict'; + +test(() => { + const ws = new WritableStream({}); + const writer = ws.getWriter(); + writer.releaseLock(); + + assert_throws_js(TypeError, () => writer.desiredSize, 'desiredSize should throw a TypeError'); +}, 'desiredSize on a released writer'); + +test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); +}, 'desiredSize initial value'); + +promise_test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + + writer.close(); + + return writer.closed.then(() => { + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0'); + }); +}, 'desiredSize on a writer for a closed stream'); + +test(() => { + const ws = new WritableStream({ + start(c) { + c.error(); + } + }); + + const writer = ws.getWriter(); + assert_equals(writer.desiredSize, null, 'desiredSize should be null'); +}, 'desiredSize on a writer for an errored stream'); + +test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + writer.close(); + writer.releaseLock(); + + ws.getWriter(); +}, 'ws.getWriter() on a closing WritableStream'); + +promise_test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + return writer.close().then(() => { + writer.releaseLock(); + + ws.getWriter(); + }); +}, 'ws.getWriter() on a closed WritableStream'); + +test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + writer.abort(); + writer.releaseLock(); + + ws.getWriter(); +}, 'ws.getWriter() on an aborted WritableStream'); + +promise_test(() => { + const ws = new WritableStream({ + start(c) { + c.error(); + } + }); + + const writer = ws.getWriter(); + return writer.closed.then( + v => assert_unreached('writer.closed fulfilled unexpectedly with: ' + v), + () => { + writer.releaseLock(); + + ws.getWriter(); + } + ); +}, 'ws.getWriter() on an errored WritableStream'); + +promise_test(() => { + const ws = new WritableStream({}); + + const writer = ws.getWriter(); + writer.releaseLock(); + + return writer.closed.then( + v => assert_unreached('writer.closed fulfilled unexpectedly with: ' + v), + closedRejection => { + assert_equals(closedRejection.name, 'TypeError', 'closed promise should reject with a TypeError'); + return writer.ready.then( + v => assert_unreached('writer.ready fulfilled unexpectedly with: ' + v), + readyRejection => assert_equals(readyRejection, closedRejection, + 'ready promise should reject with the same error') + ); + } + ); +}, 'closed and ready on a released writer'); + +promise_test(t => { + let thisObject = null; + // Calls to Sink methods after the first are implicitly ignored. Only the first value that is passed to the resolver + // is used. + class Sink { + start() { + // Called twice + t.step(() => { + assert_equals(this, thisObject, 'start should be called as a method'); + }); + } + + write() { + t.step(() => { + assert_equals(this, thisObject, 'write should be called as a method'); + }); + } + + close() { + t.step(() => { + assert_equals(this, thisObject, 'close should be called as a method'); + }); + } + + abort() { + t.step(() => { + assert_equals(this, thisObject, 'abort should be called as a method'); + }); + } + } + + const theSink = new Sink(); + thisObject = theSink; + const ws = new WritableStream(theSink); + + const writer = ws.getWriter(); + + writer.write('a'); + const closePromise = writer.close(); + + const ws2 = new WritableStream(theSink); + const writer2 = ws2.getWriter(); + const abortPromise = writer2.abort(); + + return Promise.all([ + closePromise, + abortPromise + ]); +}, 'WritableStream should call underlying sink methods as methods'); + +promise_test(t => { + function functionWithOverloads() {} + functionWithOverloads.apply = t.unreached_func('apply() should not be called'); + functionWithOverloads.call = t.unreached_func('call() should not be called'); + const underlyingSink = { + start: functionWithOverloads, + write: functionWithOverloads, + close: functionWithOverloads, + abort: functionWithOverloads + }; + // Test start(), write(), close(). + const ws1 = new WritableStream(underlyingSink); + const writer1 = ws1.getWriter(); + writer1.write('a'); + writer1.close(); + + // Test abort(). + const abortError = new Error(); + abortError.name = 'abort error'; + + const ws2 = new WritableStream(underlyingSink); + const writer2 = ws2.getWriter(); + writer2.abort(abortError); + + // Test abort() with a close underlying sink method present. (Historical; see + // https://github.com/whatwg/streams/issues/620#issuecomment-263483953 for what used to be + // tested here. But more coverage can't hurt.) + const ws3 = new WritableStream({ + start: functionWithOverloads, + write: functionWithOverloads, + close: functionWithOverloads + }); + const writer3 = ws3.getWriter(); + writer3.abort(abortError); + + return writer1.closed + .then(() => promise_rejects_exactly(t, abortError, writer2.closed, 'writer2.closed should be rejected')) + .then(() => promise_rejects_exactly(t, abortError, writer3.closed, 'writer3.closed should be rejected')); +}, 'methods should not not have .apply() or .call() called'); + +promise_test(() => { + const strategy = { + size() { + if (this !== undefined) { + throw new Error('size called as a method'); + } + return 1; + } + }; + + const ws = new WritableStream({}, strategy); + const writer = ws.getWriter(); + return writer.write('a'); +}, 'WritableStream\'s strategy.size should not be called as a method'); + +promise_test(() => { + const ws = new WritableStream(); + const writer1 = ws.getWriter(); + assert_equals(undefined, writer1.releaseLock(), 'releaseLock() should return undefined'); + const writer2 = ws.getWriter(); + assert_equals(undefined, writer1.releaseLock(), 'no-op releaseLock() should return undefined'); + // Calling releaseLock() on writer1 should not interfere with writer2. If it did, then the ready promise would be + // rejected. + return writer2.ready; +}, 'redundant releaseLock() is no-op'); + +promise_test(() => { + const events = []; + const ws = new WritableStream(); + const writer = ws.getWriter(); + return writer.ready.then(() => { + // Force the ready promise back to a pending state. + const writerPromise = writer.write('dummy'); + const readyPromise = writer.ready.catch(() => events.push('ready')); + const closedPromise = writer.closed.catch(() => events.push('closed')); + writer.releaseLock(); + return Promise.all([readyPromise, closedPromise]).then(() => { + assert_array_equals(events, ['ready', 'closed'], 'ready promise should fire before closed promise'); + // Stop the writer promise hanging around after the test has finished. + return Promise.all([ + writerPromise, + ws.abort() + ]); + }); + }); +}, 'ready promise should fire before closed on releaseLock'); + +test(() => { + class Subclass extends WritableStream { + extraFunction() { + return true; + } + } + assert_equals( + Object.getPrototypeOf(Subclass.prototype), WritableStream.prototype, + 'Subclass.prototype\'s prototype should be WritableStream.prototype'); + assert_equals(Object.getPrototypeOf(Subclass), WritableStream, + 'Subclass\'s prototype should be WritableStream'); + const sub = new Subclass(); + assert_true(sub instanceof WritableStream, + 'Subclass object should be an instance of WritableStream'); + assert_true(sub instanceof Subclass, + 'Subclass object should be an instance of Subclass'); + const lockedGetter = Object.getOwnPropertyDescriptor( + WritableStream.prototype, 'locked').get; + assert_equals(lockedGetter.call(sub), sub.locked, + 'Subclass object should pass brand check'); + assert_true(sub.extraFunction(), + 'extraFunction() should be present on Subclass object'); +}, 'Subclassing WritableStream should work'); + +test(() => { + const ws = new WritableStream(); + assert_false(ws.locked, 'stream should not be locked'); + ws.getWriter(); + assert_true(ws.locked, 'stream should be locked'); +}, 'the locked getter should return true if the stream has a writer'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/properties.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/properties.any.js new file mode 100644 index 000000000000..c95bd7d0c080 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/properties.any.js @@ -0,0 +1,53 @@ +// META: global=window,worker +'use strict'; + +const sinkMethods = { + start: { + length: 1, + trigger: () => Promise.resolve() + }, + write: { + length: 2, + trigger: writer => writer.write() + }, + close: { + length: 0, + trigger: writer => writer.close() + }, + abort: { + length: 1, + trigger: writer => writer.abort() + } +}; + +for (const method in sinkMethods) { + const { length, trigger } = sinkMethods[method]; + + // Some semantic tests of how sink methods are called can be found in general.js, as well as in the test files + // specific to each method. + promise_test(() => { + let argCount; + const ws = new WritableStream({ + [method](...args) { + argCount = args.length; + } + }); + return Promise.resolve(trigger(ws.getWriter())).then(() => { + assert_equals(argCount, length, `${method} should be called with ${length} arguments`); + }); + }, `sink method ${method} should be called with the right number of arguments`); + + promise_test(() => { + let methodWasCalled = false; + function Sink() {} + Sink.prototype = { + [method]() { + methodWasCalled = true; + } + }; + const ws = new WritableStream(new Sink()); + return Promise.resolve(trigger(ws.getWriter())).then(() => { + assert_true(methodWasCalled, `${method} should be called`); + }); + }, `sink method ${method} should be called even when it's located on the prototype chain`); +} diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/reentrant-strategy.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/reentrant-strategy.any.js new file mode 100644 index 000000000000..eb05cc068043 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/reentrant-strategy.any.js @@ -0,0 +1,174 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +// These tests exercise the pathological case of calling WritableStream* methods from within the strategy.size() +// callback. This is not something any real code should ever do. Failures here indicate subtle deviations from the +// standard that may affect real, non-pathological code. + +const error1 = { name: 'error1' }; + +promise_test(() => { + let writer; + const strategy = { + size(chunk) { + if (chunk > 0) { + writer.write(chunk - 1); + } + return chunk; + } + }; + + const ws = recordingWritableStream({}, strategy); + writer = ws.getWriter(); + return writer.write(2) + .then(() => { + assert_array_equals(ws.events, ['write', 0, 'write', 1, 'write', 2], 'writes should appear in order'); + }); +}, 'writes should be written in the standard order'); + +promise_test(() => { + let writer; + const events = []; + const strategy = { + size(chunk) { + events.push('size', chunk); + if (chunk > 0) { + writer.write(chunk - 1) + .then(() => events.push('writer.write done', chunk - 1)); + } + return chunk; + } + }; + const ws = new WritableStream({ + write(chunk) { + events.push('sink.write', chunk); + } + }, strategy); + writer = ws.getWriter(); + return writer.write(2) + .then(() => events.push('writer.write done', 2)) + .then(() => flushAsyncEvents()) + .then(() => { + assert_array_equals(events, ['size', 2, 'size', 1, 'size', 0, + 'sink.write', 0, 'sink.write', 1, 'writer.write done', 0, + 'sink.write', 2, 'writer.write done', 1, + 'writer.write done', 2], + 'events should happen in standard order'); + }); +}, 'writer.write() promises should resolve in the standard order'); + +promise_test(t => { + let controller; + const strategy = { + size() { + controller.error(error1); + return 1; + } + }; + const ws = recordingWritableStream({ + start(c) { + controller = c; + } + }, strategy); + const resolved = []; + const writer = ws.getWriter(); + const readyPromise1 = writer.ready.then(() => resolved.push('ready1')); + const writePromise = promise_rejects_exactly(t, error1, writer.write(), + 'write() should reject with the error') + .then(() => resolved.push('write')); + const readyPromise2 = promise_rejects_exactly(t, error1, writer.ready, 'ready should reject with error1') + .then(() => resolved.push('ready2')); + const closedPromise = promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1') + .then(() => resolved.push('closed')); + return Promise.all([readyPromise1, writePromise, readyPromise2, closedPromise]) + .then(() => { + assert_array_equals(resolved, ['ready1', 'write', 'ready2', 'closed'], + 'promises should resolve in standard order'); + assert_array_equals(ws.events, [], 'underlying sink write should not be called'); + }); +}, 'controller.error() should work when called from within strategy.size()'); + +promise_test(t => { + let writer; + const strategy = { + size() { + writer.close(); + return 1; + } + }; + + const ws = recordingWritableStream({}, strategy); + writer = ws.getWriter(); + return promise_rejects_js(t, TypeError, writer.write('a'), 'write() promise should reject') + .then(() => { + assert_array_equals(ws.events, ['close'], 'sink.write() should not be called'); + }); +}, 'close() should work when called from within strategy.size()'); + +promise_test(t => { + let writer; + const strategy = { + size() { + writer.abort(error1); + return 1; + } + }; + + const ws = recordingWritableStream({}, strategy); + writer = ws.getWriter(); + return promise_rejects_exactly(t, error1, writer.write('a'), 'write() promise should reject') + .then(() => { + assert_array_equals(ws.events, ['abort', error1], 'sink.write() should not be called'); + }); +}, 'abort() should work when called from within strategy.size()'); + +promise_test(t => { + let writer; + const strategy = { + size() { + writer.releaseLock(); + return 1; + } + }; + + const ws = recordingWritableStream({}, strategy); + writer = ws.getWriter(); + const writePromise = promise_rejects_js(t, TypeError, writer.write('a'), 'write() promise should reject'); + const readyPromise = promise_rejects_js(t, TypeError, writer.ready, 'ready promise should reject'); + const closedPromise = promise_rejects_js(t, TypeError, writer.closed, 'closed promise should reject'); + return Promise.all([writePromise, readyPromise, closedPromise]) + .then(() => { + assert_array_equals(ws.events, [], 'sink.write() should not be called'); + }); +}, 'releaseLock() should abort the write() when called within strategy.size()'); + +promise_test(t => { + let writer1; + let ws; + let writePromise2; + let closePromise; + let closedPromise2; + const strategy = { + size(chunk) { + if (chunk > 0) { + writer1.releaseLock(); + const writer2 = ws.getWriter(); + writePromise2 = writer2.write(0); + closePromise = writer2.close(); + closedPromise2 = writer2.closed; + } + return 1; + } + }; + ws = recordingWritableStream({}, strategy); + writer1 = ws.getWriter(); + const writePromise1 = promise_rejects_js(t, TypeError, writer1.write(1), 'write() promise should reject'); + const readyPromise = promise_rejects_js(t, TypeError, writer1.ready, 'ready promise should reject'); + const closedPromise1 = promise_rejects_js(t, TypeError, writer1.closed, 'closed promise should reject'); + return Promise.all([writePromise1, readyPromise, closedPromise1, writePromise2, closePromise, closedPromise2]) + .then(() => { + assert_array_equals(ws.events, ['write', 0, 'close'], 'sink.write() should only be called once'); + }); +}, 'original reader should error when new reader is created within strategy.size()'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/start.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/start.any.js new file mode 100644 index 000000000000..82d869430dd7 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/start.any.js @@ -0,0 +1,163 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = { name: 'error1' }; + +promise_test(() => { + let resolveStartPromise; + const ws = recordingWritableStream({ + start() { + return new Promise(resolve => { + resolveStartPromise = resolve; + }); + } + }); + + const writer = ws.getWriter(); + + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); + writer.write('a'); + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after writer.write()'); + + // Wait and verify that write isn't called. + return flushAsyncEvents() + .then(() => { + assert_array_equals(ws.events, [], 'write should not be called until start promise resolves'); + resolveStartPromise(); + return writer.ready; + }) + .then(() => assert_array_equals(ws.events, ['write', 'a'], + 'write should not be called until start promise resolves')); +}, 'underlying sink\'s write should not be called until start finishes'); + +promise_test(() => { + let resolveStartPromise; + const ws = recordingWritableStream({ + start() { + return new Promise(resolve => { + resolveStartPromise = resolve; + }); + } + }); + + const writer = ws.getWriter(); + + writer.close(); + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); + + // Wait and verify that write isn't called. + return flushAsyncEvents().then(() => { + assert_array_equals(ws.events, [], 'close should not be called until start promise resolves'); + resolveStartPromise(); + return writer.closed; + }); +}, 'underlying sink\'s close should not be called until start finishes'); + +test(() => { + const passedError = new Error('horrible things'); + + let writeCalled = false; + let closeCalled = false; + assert_throws_exactly(passedError, () => { + // recordingWritableStream cannot be used here because the exception in the + // constructor prevents assigning the object to a variable. + new WritableStream({ + start() { + throw passedError; + }, + write() { + writeCalled = true; + }, + close() { + closeCalled = true; + } + }); + }, 'constructor should throw passedError'); + assert_false(writeCalled, 'write should not be called'); + assert_false(closeCalled, 'close should not be called'); +}, 'underlying sink\'s write or close should not be called if start throws'); + +promise_test(() => { + const ws = recordingWritableStream({ + start() { + return Promise.reject(); + } + }); + + // Wait and verify that write or close aren't called. + return flushAsyncEvents() + .then(() => assert_array_equals(ws.events, [], 'write and close should not be called')); +}, 'underlying sink\'s write or close should not be invoked if the promise returned by start is rejected'); + +promise_test(t => { + const ws = new WritableStream({ + start() { + return { + then(onFulfilled, onRejected) { onRejected(error1); } + }; + } + }); + return promise_rejects_exactly(t, error1, ws.getWriter().closed, 'closed promise should be rejected'); +}, 'returning a thenable from start() should work'); + +promise_test(t => { + const ws = recordingWritableStream({ + start(controller) { + controller.error(error1); + } + }); + return promise_rejects_exactly(t, error1, ws.getWriter().write('a'), 'write() should reject with the error') + .then(() => { + assert_array_equals(ws.events, [], 'sink write() should not have been called'); + }); +}, 'controller.error() during start should cause writes to fail'); + +promise_test(t => { + let controller; + let resolveStart; + const ws = recordingWritableStream({ + start(c) { + controller = c; + return new Promise(resolve => { + resolveStart = resolve; + }); + } + }); + const writer = ws.getWriter(); + const writePromise = writer.write('a'); + const closePromise = writer.close(); + controller.error(error1); + resolveStart(); + return Promise.all([ + promise_rejects_exactly(t, error1, writePromise, 'write() should fail'), + promise_rejects_exactly(t, error1, closePromise, 'close() should fail') + ]).then(() => { + assert_array_equals(ws.events, [], 'sink write() and close() should not have been called'); + }); +}, 'controller.error() during async start should cause existing writes to fail'); + +promise_test(t => { + const events = []; + const promises = []; + function catchAndRecord(promise, name) { + promises.push(promise.then(t.unreached_func(`promise ${name} should not resolve`), + () => { + events.push(name); + })); + } + const ws = new WritableStream({ + start() { + return Promise.reject(); + } + }, { highWaterMark: 0 }); + const writer = ws.getWriter(); + catchAndRecord(writer.ready, 'ready'); + catchAndRecord(writer.closed, 'closed'); + catchAndRecord(writer.write(), 'write'); + return Promise.all(promises) + .then(() => { + assert_array_equals(events, ['ready', 'write', 'closed'], 'promises should reject in standard order'); + }); +}, 'when start() rejects, writer promises should reject in standard order'); diff --git a/test/js/third_party/wpt-streams/streams/writable-streams/write.any.js b/test/js/third_party/wpt-streams/streams/writable-streams/write.any.js new file mode 100644 index 000000000000..f0246f6cad39 --- /dev/null +++ b/test/js/third_party/wpt-streams/streams/writable-streams/write.any.js @@ -0,0 +1,284 @@ +// META: global=window,worker +// META: script=../resources/test-utils.js +// META: script=../resources/recording-streams.js +'use strict'; + +const error1 = new Error('error1'); +error1.name = 'error1'; + +const error2 = new Error('error2'); +error2.name = 'error2'; + +function writeArrayToStream(array, writableStreamWriter) { + array.forEach(chunk => writableStreamWriter.write(chunk)); + return writableStreamWriter.close(); +} + +promise_test(() => { + let storage; + const ws = new WritableStream({ + start() { + storage = []; + }, + + write(chunk) { + return delay(0).then(() => storage.push(chunk)); + }, + + close() { + return delay(0); + } + }); + + const writer = ws.getWriter(); + + const input = [1, 2, 3, 4, 5]; + return writeArrayToStream(input, writer) + .then(() => assert_array_equals(storage, input, 'correct data should be relayed to underlying sink')); +}, 'WritableStream should complete asynchronous writes before close resolves'); + +promise_test(() => { + const ws = recordingWritableStream(); + + const writer = ws.getWriter(); + + const input = [1, 2, 3, 4, 5]; + return writeArrayToStream(input, writer) + .then(() => assert_array_equals(ws.events, ['write', 1, 'write', 2, 'write', 3, 'write', 4, 'write', 5, 'close'], + 'correct data should be relayed to underlying sink')); +}, 'WritableStream should complete synchronous writes before close resolves'); + +promise_test(() => { + const ws = new WritableStream({ + write() { + return 'Hello'; + } + }); + + const writer = ws.getWriter(); + + const writePromise = writer.write('a'); + return writePromise + .then(value => assert_equals(value, undefined, 'fulfillment value must be undefined')); +}, 'fulfillment value of ws.write() call should be undefined even if the underlying sink returns a non-undefined ' + + 'value'); + +promise_test(() => { + let resolveSinkWritePromise; + const ws = new WritableStream({ + write() { + return new Promise(resolve => { + resolveSinkWritePromise = resolve; + }); + } + }); + + const writer = ws.getWriter(); + + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); + + return writer.ready.then(() => { + const writePromise = writer.write('a'); + let writePromiseResolved = false; + assert_not_equals(resolveSinkWritePromise, undefined, 'resolveSinkWritePromise should not be undefined'); + + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after writer.write()'); + + return Promise.all([ + writePromise.then(value => { + writePromiseResolved = true; + assert_equals(resolveSinkWritePromise, undefined, 'sinkWritePromise should be fulfilled before writePromise'); + + assert_equals(value, undefined, 'writePromise should be fulfilled with undefined'); + }), + writer.ready.then(value => { + assert_equals(resolveSinkWritePromise, undefined, 'sinkWritePromise should be fulfilled before writer.ready'); + assert_true(writePromiseResolved, 'writePromise should be fulfilled before writer.ready'); + + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1 again'); + + assert_equals(value, undefined, 'writePromise should be fulfilled with undefined'); + }), + flushAsyncEvents().then(() => { + resolveSinkWritePromise(); + resolveSinkWritePromise = undefined; + }) + ]); + }); +}, 'WritableStream should transition to waiting until write is acknowledged'); + +promise_test(t => { + let sinkWritePromiseRejectors = []; + const ws = new WritableStream({ + write() { + const sinkWritePromise = new Promise((r, reject) => sinkWritePromiseRejectors.push(reject)); + return sinkWritePromise; + } + }); + + const writer = ws.getWriter(); + + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); + + return writer.ready.then(() => { + const writePromise = writer.write('a'); + assert_equals(sinkWritePromiseRejectors.length, 1, 'there should be 1 rejector'); + assert_equals(writer.desiredSize, 0, 'desiredSize should be 0'); + + const writePromise2 = writer.write('b'); + assert_equals(sinkWritePromiseRejectors.length, 1, 'there should be still 1 rejector'); + assert_equals(writer.desiredSize, -1, 'desiredSize should be -1'); + + const closedPromise = writer.close(); + + assert_equals(writer.desiredSize, -1, 'desiredSize should still be -1'); + + return Promise.all([ + promise_rejects_exactly(t, error1, closedPromise, + 'closedPromise should reject with the error returned from the sink\'s write method') + .then(() => assert_equals(sinkWritePromiseRejectors.length, 0, + 'sinkWritePromise should reject before closedPromise')), + promise_rejects_exactly(t, error1, writePromise, + 'writePromise should reject with the error returned from the sink\'s write method') + .then(() => assert_equals(sinkWritePromiseRejectors.length, 0, + 'sinkWritePromise should reject before writePromise')), + promise_rejects_exactly(t, error1, writePromise2, + 'writePromise2 should reject with the error returned from the sink\'s write method') + .then(() => assert_equals(sinkWritePromiseRejectors.length, 0, + 'sinkWritePromise should reject before writePromise2')), + flushAsyncEvents().then(() => { + sinkWritePromiseRejectors[0](error1); + sinkWritePromiseRejectors = []; + }) + ]); + }); +}, 'when write returns a rejected promise, queued writes and close should be cleared'); + +promise_test(t => { + const ws = new WritableStream({ + write() { + throw error1; + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error1, writer.write('a'), + 'write() should reject with the error returned from the sink\'s write method') + .then(() => promise_rejects_js(t, TypeError, writer.close(), 'close() should be rejected')); +}, 'when sink\'s write throws an error, the stream should become errored and the promise should reject'); + +promise_test(t => { + const ws = new WritableStream({ + write(chunk, controller) { + controller.error(error1); + throw error2; + } + }); + + const writer = ws.getWriter(); + + return promise_rejects_exactly(t, error2, writer.write('a'), + 'write() should reject with the error returned from the sink\'s write method ') + .then(() => { + return Promise.all([ + promise_rejects_exactly(t, error1, writer.ready, + 'writer.ready must reject with the error passed to the controller'), + promise_rejects_exactly(t, error1, writer.closed, + 'writer.closed must reject with the error passed to the controller') + ]); + }); +}, 'writer.write(), ready and closed reject with the error passed to controller.error() made before sink.write' + + ' rejection'); + +promise_test(() => { + const numberOfWrites = 1000; + + let resolveFirstWritePromise; + let writeCount = 0; + const ws = new WritableStream({ + write() { + ++writeCount; + if (!resolveFirstWritePromise) { + return new Promise(resolve => { + resolveFirstWritePromise = resolve; + }); + } + return Promise.resolve(); + } + }); + + const writer = ws.getWriter(); + return writer.ready.then(() => { + for (let i = 1; i < numberOfWrites; ++i) { + writer.write('a'); + } + const writePromise = writer.write('a'); + + assert_equals(writeCount, 1, 'should have called sink\'s write once'); + + resolveFirstWritePromise(); + + return writePromise + .then(() => + assert_equals(writeCount, numberOfWrites, `should have called sink's write ${numberOfWrites} times`)); + }); +}, 'a large queue of writes should be processed completely'); + +promise_test(() => { + const stream = recordingWritableStream(); + const w = stream.getWriter(); + const WritableStreamDefaultWriter = w.constructor; + w.releaseLock(); + const writer = new WritableStreamDefaultWriter(stream); + return writer.ready.then(() => { + writer.write('a'); + assert_array_equals(stream.events, ['write', 'a'], 'write() should be passed to sink'); + }); +}, 'WritableStreamDefaultWriter should work when manually constructed'); + +promise_test(() => { + let thenCalled = false; + const ws = new WritableStream({ + write() { + return { + then(onFulfilled) { + thenCalled = true; + onFulfilled(); + } + }; + } + }); + return ws.getWriter().write('a').then(() => assert_true(thenCalled, 'thenCalled should be true')); +}, 'returning a thenable from write() should work'); + +promise_test(() => { + const stream = new WritableStream(); + const writer = stream.getWriter(); + const WritableStreamDefaultWriter = writer.constructor; + assert_throws_js(TypeError, () => new WritableStreamDefaultWriter(stream), + 'should not be able to construct on locked stream'); + // If stream.[[writer]] no longer points to |writer| then the closed Promise + // won't work properly. + return Promise.all([writer.close(), writer.closed]); +}, 'failing DefaultWriter constructor should not release an existing writer'); + +promise_test(t => { + const ws = new WritableStream({ + start() { + return Promise.reject(error1); + } + }, { highWaterMark: 0 }); + const writer = ws.getWriter(); + return Promise.all([ + promise_rejects_exactly(t, error1, writer.ready, 'ready should be rejected'), + promise_rejects_exactly(t, error1, writer.write(), 'write() should be rejected') + ]); +}, 'write() on a stream with HWM 0 should not cause the ready Promise to resolve'); + +promise_test(t => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + writer.releaseLock(); + return promise_rejects_js(t, TypeError, writer.write(), 'write should reject'); +}, 'writing to a released writer should reject the returned promise'); diff --git a/test/js/third_party/wpt-streams/wpt-streams.test.ts b/test/js/third_party/wpt-streams/wpt-streams.test.ts new file mode 100644 index 000000000000..a79480ed98af --- /dev/null +++ b/test/js/third_party/wpt-streams/wpt-streams.test.ts @@ -0,0 +1,301 @@ +// Runs the vendored Web Platform Tests streams suite (streams/**/*.any.js) +// against Bun's Web Streams implementation. The .any.js files and the +// streams/resources/*.js helpers are byte-identical to upstream; every +// adaptation lives in ../wpt-testharness-shim.ts and this driver, following the +// test/js/third_party/wpt-h2 pattern. +// +// Vendored from web-platform-tests/wpt @ 1cfa3004f4ac74aa007591529aba9e9246b1f1bf +// (see UPSTREAM.md for the file list and exclusions). +// +// Every WPT subtest that does not pass on the current implementation is +// listed in expectations.json, keyed by " :: ". How the +// expectation value's prefix maps onto bun:test: +// +// CRASH... -> test.todo (body-less: the body aborts the whole process) +// TIMEOUT... -> test.todo (body-less: the body would cost its full budget) +// anything else (FAIL...) -> test.failing: the body still RUNS, its failure +// is expected, and a body that starts PASSING fails the run +// ("marked as failing but it passed") — the graduation signal. +// +// Everything not listed must pass. Regenerate the expectation data with: +// +// rm -f /root/wpt-fix-scratch/j.jsonl +// WPT_STREAMS_RECORD=/root/wpt-fix-scratch/j.jsonl bun bd test test/js/third_party/wpt-streams/wpt-streams.test.ts +// +// which appends one JSON line per subtest ({name, status, message}) to that +// path. The journal is append-only and a "RUNNING" line is written before +// each subtest body executes, so if a subtest brings the whole process down +// (a real bug class for streams + GC), the crashing subtest is the trailing +// RUNNING entry with no result. Add it to expectations.json with a value +// starting with "CRASH" and re-run: record mode never executes known-CRASH +// subtests, and never re-executes subtests that already have a result in the +// journal, so the sweep resumes and makes progress past every crasher. Once +// the sweep completes, rebuild expectations.json + RESULTS.md from the +// journal and update EXPECTED_FILES / EXPECTED_SUBTESTS below. + +import { afterAll, test as bunTest, describe, expect } from "bun:test"; +import { appendFileSync, existsSync, readdirSync, readFileSync } from "node:fs"; +import { dirname, join, relative, sep } from "node:path"; +import { setRegistrar, setSubtestTimeout, SUBTEST_TIMEOUT_MS, wptTest } from "../wpt-testharness-shim"; +import expectations from "./expectations.json"; + +const ROOT = import.meta.dir; +const expectedFailures = expectations.failures as Record; + +// These MUST be updated intentionally whenever the vendored set changes (a +// re-vendor, or adding/removing files). They pin exactly how many `.any.js` +// files were discovered and how many WPT subtests were registered, so a file +// that stops evaluating (or a subtest that stops being registered) turns the +// suite red instead of silently shrinking it while it stays green. +const EXPECTED_FILES = 69; +const EXPECTED_SUBTESTS = 1402; + +// Record mode: run everything except known process-crashers (no todos), never +// fail the bun test, and journal every result so expectations.json / +// RESULTS.md can be regenerated. +const recordPath = process.env.WPT_STREAMS_RECORD; +type Status = "PASS" | "FAIL" | "TIMEOUT" | "CRASH" | "RUNNING"; +function journal(name: string, status: Status, message?: string) { + appendFileSync(recordPath!, JSON.stringify({ name, status, message }) + "\n"); +} + +// The journal is also the resume point: subtests that already have a final +// result in it are not re-executed, so a sweep interrupted by a crashing +// subtest picks up where it left off once the crasher is quarantined. +const alreadyRecorded = new Set(); +if (recordPath && existsSync(recordPath)) { + for (const line of readFileSync(recordPath, "utf8").split("\n")) { + if (!line) continue; + const entry = JSON.parse(line); + if (entry.status !== "RUNNING") alreadyRecorded.add(entry.name); + } +} + +let registeredSubtests = 0; +const expectationHits = new Map(); + +let currentFile = ""; +const register = (name: string, run: () => Promise) => { + registeredSubtests++; + const key = `${currentFile} :: ${name}`; + const expected = expectedFailures[key]; + if (expected !== undefined) expectationHits.set(key, (expectationHits.get(key) ?? 0) + 1); + // A subtest that aborts the process (JSC assertion, segfault) can never be + // executed, in either mode; it is still reported. + const crashes = expected !== undefined && expected.startsWith("CRASH"); + if (recordPath) { + if (crashes) { + if (!alreadyRecorded.has(key)) journal(key, "CRASH", expected); + return void bunTest.todo(name); + } + if (alreadyRecorded.has(key)) return void bunTest.todo(name); + return void bunTest(name, async () => { + journal(key, "RUNNING"); + try { + await run(); + journal(key, "PASS"); + } catch (e: any) { + journal(key, e?.name === "WPTTimeout" ? "TIMEOUT" : "FAIL", String(e?.message ?? e)); + } + }); + } + if (expected === undefined) return void bunTest(name, run); + // TIMEOUT bodies would cost their full budget on every run; like CRASH + // bodies they are never executed in normal mode. + if (crashes || expected.startsWith("TIMEOUT")) return void bunTest.todo(name); + // Expected assertion failures still RUN: a body that starts passing turns + // into "marked as failing but it passed", which is the graduation signal. + bunTest.failing(name, run); +}; +setRegistrar(register); + +// idlharness's `idl_test()` fetches WebIDL definitions from `/interfaces/.idl` +// through `globalThis.fetch_spec` (the hook WPT also uses for its ShadowRealm +// runner). The vendored `.idl` files live next to the tests, so serve them from +// disk. `idlharness.js` runs inside `new Function` below, so its own script-scope +// `fetch_spec` declaration never reaches globalThis and this override wins. +(globalThis as any).fetch_spec = async (spec: string) => { + const path = join(ROOT, "interfaces", `${spec}.idl`); + if (!existsSync(path)) throw new Error(`fetch_spec: no vendored IDL for "${spec}" at ${path}`); + return { spec, idl: readFileSync(path, "utf8") }; +}; + +function* walk(dir: string): Generator { + for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { + const path = join(dir, entry.name); + if (entry.isDirectory()) yield* walk(path); + else yield path; + } +} + +const files = [...walk(join(ROOT, "streams"))].filter(f => f.endsWith(".any.js")); + +// `// META: script=` includes are classic scripts sharing the test file's +// global scope in WPT, so they are concatenated ahead of the test source. +// Absolute paths (`/common/gc.js`) resolve against the vendored WPT root. +// The 5 distinct include files are referenced 81 times, so memoize them. +const includeCache = new Map(); +function readInclude(path: string): string { + let source = includeCache.get(path); + if (source === undefined) includeCache.set(path, (source = readFileSync(path, "utf8"))); + return source; +} + +// Only `script` is acted on; `global`/`title`/`timeout` are recognized and +// ignored. Anything else means the vendored set now relies on a META key +// this runner does not understand, which must be a hard error. +const KNOWN_META_KEYS = new Set(["script", "global", "title", "timeout"]); +const META_RE = /^\/\/ META: ([^=]+)=(.*)$/; + +// Concatenate a test file with its `// META: script=` includes (classic scripts +// sharing the test's global scope in WPT). +function buildSource(file: string, rel: string): string { + const source = readFileSync(file, "utf8"); + const pieces: string[] = []; + for (const line of source.split("\n")) { + const match = META_RE.exec(line); + if (!match) continue; + const [, metaKey, metaValue] = match; + if (!KNOWN_META_KEYS.has(metaKey)) throw new Error(`${rel}: unknown \`// META: ${metaKey}=\` key`); + if (metaKey !== "script") continue; + let ref = metaValue.trim(); + // WPT's server aliases `/resources/WebIDLParser.js` to the webidl2 + // bundle checked in at `resources/webidl2/lib/webidl2.js`. + if (ref === "/resources/WebIDLParser.js") ref = "/resources/webidl2/lib/webidl2.js"; + pieces.push(readInclude(ref.startsWith("/") ? join(ROOT, ref.slice(1)) : join(dirname(file), ref))); + } + pieces.push(source); + return pieces.join("\n;\n"); +} + +// idlharness registers most of its subtests dynamically, from inside its own +// running `idl_test setup` promise_test (real testharness.js supports that; the +// 1:1 bun:test mapping below requires registration at evaluation time), and +// bun:test cannot accept tests registered after the run starts. Such files are +// executed inside ONE bun test through a registrar with upstream testharness +// semantics — `test()` bodies run synchronously at registration (idlharness's +// member closures capture `var` loop variables), `async_test` starts +// immediately, `promise_test`s are serialized — and every collected subtest is +// then adjudicated against expectations.json individually. +const DYNAMIC_REGISTRATION_FILES = new Set(["idlharness.any.js"]); + +type Collected = { name: string; error?: unknown }; +async function preExecute(file: string, rel: string): Promise { + const collected: Collected[] = []; + let queue = Promise.resolve(); + // idlharness declares `// META: timeout=long`; its setup subtest runs every + // member subtest inline, so it genuinely needs the long budget. + const previousTimeout = setSubtestTimeout(SUBTEST_TIMEOUT_MS * 8); + // Start a subtest and capture its outcome immediately (a rejection observed + // only when the queue reaches it would be reported as an unhandled error). + type Outcome = { error: unknown } | undefined; + const start = (run: () => Promise): Promise => { + try { + return Promise.resolve(run()).then( + () => undefined, + (e: unknown) => ({ error: e ?? new Error("unknown failure") }), + ); + } catch (e) { + return Promise.resolve({ error: e ?? new Error("unknown failure") }); + } + }; + setRegistrar((name, run, kind) => { + // Upstream testharness semantics: `test()` bodies execute synchronously at + // registration (idlharness's member closures read `var` loop variables) and + // `async_test` starts immediately; only `promise_test`s are serialized. + const started = kind === "promise_test" ? undefined : start(run); + queue = queue.then(async () => { + const outcome = await (started ?? start(run)); + collected.push(outcome === undefined ? { name } : { name, error: outcome.error }); + }); + }); + try { + new Function("test", buildSource(file, rel))(wptTest); + // Later subtests are registered while earlier ones run; drain until no new + // results appear. + let settled = -1; + while (collected.length !== settled) { + settled = collected.length; + await queue; + } + } catch (e) { + collected.push({ name: "harness: file failed to evaluate", error: e }); + } finally { + setRegistrar(register); + setSubtestTimeout(previousTimeout); + } + return collected; +} + +function registerDynamicFile(file: string, rel: string) { + bunTest( + rel, + async () => { + const collected = await preExecute(file, rel); + registeredSubtests += collected.length; + expect(collected.length).toBeGreaterThan(0); + const problems: string[] = []; + for (const r of collected) { + const key = `${rel} :: ${r.name}`; + const expected = expectedFailures[key]; + if (expected !== undefined) expectationHits.set(key, (expectationHits.get(key) ?? 0) + 1); + const failed = "error" in r && r.error !== undefined; + if (recordPath) { + journal(key, failed ? "FAIL" : "PASS", failed ? String((r.error as any)?.message ?? r.error) : undefined); + continue; + } + if (failed && expected === undefined) { + problems.push(`unexpected FAIL: ${r.name}: ${String((r.error as any)?.message ?? r.error)}`); + } else if (!failed && expected !== undefined) { + problems.push(`marked as failing in expectations.json but passed: ${r.name}`); + } + } + expect(problems).toEqual([]); + }, + 120_000, + ); +} + +for (const file of files) { + // Expectation keys are always `/`-separated so they are identical on + // every platform. + const rel = relative(ROOT, file).split(sep).join("/"); + + if (DYNAMIC_REGISTRATION_FILES.has(rel.split("/").pop()!)) { + registerDynamicFile(file, rel); + continue; + } + + describe(rel, () => { + currentFile = rel; + // A throw anywhere in here — an unresolvable `// META: script=` include, + // an unknown META key, a testharness API the shim only stubs, a syntax + // error — must be LOUD, never a silently shorter file. The synthetic + // subtest names the failure (and journals it in record mode) and the + // rethrow errors the whole describe; EXPECTED_SUBTESTS independently + // catches the shrink. + try { + // bun:test injects its own `test` binding into every module it + // transpiles, which would shadow the WPT-style test(fn, name) global. + // Evaluate the vendored sources inside a Function whose `test` + // parameter is the shim's synchronous test(); all other testharness + // identifiers resolve via globalThis (see ../wpt-testharness-shim.ts). + new Function("test", buildSource(file, rel))(wptTest); + } catch (e) { + register("harness: file failed to evaluate", () => Promise.reject(e)); + throw e; + } + }); +} + +afterAll(() => { + expect(files.length).toBe(EXPECTED_FILES); + expect(registeredSubtests).toBe(EXPECTED_SUBTESTS); +}); + +// Every expectations.json key must have matched exactly one registered +// subtest; a stale or renamed key would otherwise rot silently. +afterAll(() => { + const unmatched = Object.keys(expectedFailures).filter(key => expectationHits.get(key) !== 1); + expect(unmatched).toEqual([]); +}); diff --git a/test/js/third_party/wpt-testharness-shim.ts b/test/js/third_party/wpt-testharness-shim.ts new file mode 100644 index 000000000000..6051a6aa5bb6 --- /dev/null +++ b/test/js/third_party/wpt-testharness-shim.ts @@ -0,0 +1,618 @@ +// Minimal WPT testharness.js shim mapped onto bun:test, shared by the +// wpt-h2 and wpt-streams runners. It covers the surface their vendored +// .any.js files (and streams/resources/*.js) actually touch: +// +// test / promise_test / async_test +// assert_{equals,not_equals,true,false,array_equals,object_equals, +// unreached,throws_js,throws_exactly,throws_dom,greater_than} +// promise_rejects_{js,exactly,dom} +// t.step / t.step_func / t.step_func_done / t.unreached_func / t.add_cleanup +// step_timeout +// +// The vendored files are byte-identical to upstream; every adaptation lives +// here or in each suite's runner. Registration of subtests is delegated to +// the runner through `setRegistrar` so that each runner decides how a WPT +// subtest maps onto bun:test (todo/failing policy lives in the runner). +// +// Faithful WPT semantics the shim enforces (see wpt-streams.test.ts for how +// that runner maps expected failures): +// - `promise_test` bodies must return a thenable. +// - A subtest that times out still runs its `t.add_cleanup`s, so a hung +// body cannot leave patched globals installed for later subtests. +// - The shim's own bookkeeping never goes through user-patchable prototype +// methods, so the patched-global.any.js subtests observe only the +// implementation, never the harness. +// A rejection that ends up unhandled while a subtest runs fails that subtest +// too, but that is bun:test's own built-in behavior — see the "Unhandled +// rejections" section below for why the shim neither can nor needs to +// re-implement it. + +import { isASAN } from "harness"; + +/** How the runner receives each WPT subtest. `run` resolves on PASS and + * rejects on FAIL; a rejection whose Error.name is "WPTTimeout" is a hang. */ +export type SubtestKind = "test" | "promise_test" | "async_test"; +export type Registrar = (name: string, run: () => Promise, kind?: SubtestKind) => void; + +let registrar: Registrar = () => { + throw new Error("wpt testharness-shim: setRegistrar() was not called"); +}; +export function setRegistrar(r: Registrar) { + registrar = r; +} + +// Wall-clock budget for a single WPT subtest (body + cleanups). It must be +// smaller than bun:test's default per-test timeout (5000ms; this suite never +// overrides it) so a hang is always reported as a named `WPTTimeout` — and, +// in record mode, journaled — instead of bun killing the body mid-flight. +// ASAN/debug builds run several times slower, so they get 3x; that can only +// reduce false TIMEOUTs. 1500 * 3 = 4500ms leaves 500ms for cleanups. +export let SUBTEST_TIMEOUT_MS = 1500 * (isASAN ? 3 : 1); +// WPT's `// META: timeout=long` multiplies the budget; idlharness's `idl_test +// setup` runs every member subtest inline, so it needs the long budget. +export function setSubtestTimeout(ms: number): number { + const prev = SUBTEST_TIMEOUT_MS; + SUBTEST_TIMEOUT_MS = ms; + return prev; +} + +// --------------------------------------------------------------------------- +// assertion helpers (semantics follow upstream resources/testharness.js) + +class AssertionError extends Error { + constructor(message: string) { + super(message); + this.name = "AssertionError"; + } +} + +function fail(message: string): never { + throw new AssertionError(message); +} + +export function format_value(val: unknown): string { + if (Array.isArray(val)) return `[${(val as unknown[]).map(format_value).join(", ")}]`; + switch (typeof val) { + case "string": + return JSON.stringify(val); + case "symbol": + case "bigint": + case "function": + return String(val); + case "object": + if (val === null) return "null"; + try { + const ctor = (val as any).constructor?.name; + if (val instanceof Error) return `${(val as Error).name}: ${(val as Error).message}`; + return `object "${String(val)}" (${ctor})`; + } catch { + return "[object]"; + } + default: + return String(val); + } +} + +// Upstream testharness.js `same_value`: NaN equals NaN, but +0 and -0 are +// distinct (everything else is `===`). +function sameValue(x: unknown, y: unknown): boolean { + if ((y as any) !== (y as any)) return (x as any) !== (x as any); + if (x === 0 && y === 0) return 1 / (x as number) === 1 / (y as number); + return x === y; +} + +function assert_equals(actual: unknown, expected: unknown, description?: string) { + if (typeof actual !== typeof expected) { + fail( + `assert_equals: ${description ?? ""} expected (${typeof expected}) ${format_value(expected)} but got (${typeof actual}) ${format_value(actual)}`, + ); + } + if (!sameValue(actual, expected)) { + fail(`assert_equals: ${description ?? ""} expected ${format_value(expected)} but got ${format_value(actual)}`); + } +} + +function assert_not_equals(actual: unknown, expected: unknown, description?: string) { + if (sameValue(actual, expected)) { + fail(`assert_not_equals: ${description ?? ""} got disallowed value ${format_value(actual)}`); + } +} + +function assert_true(actual: unknown, description?: string) { + if (actual !== true) fail(`assert_true: ${description ?? ""} expected true got ${format_value(actual)}`); +} + +function assert_false(actual: unknown, description?: string) { + if (actual !== false) fail(`assert_false: ${description ?? ""} expected false got ${format_value(actual)}`); +} + +function assert_array_equals(actual: any, expected: any, description?: string) { + if (typeof actual !== "object" || actual === null || !("length" in actual)) { + fail(`assert_array_equals: ${description ?? ""} value is ${format_value(actual)}, expected array`); + } + if (actual.length !== expected.length) { + fail( + `assert_array_equals: ${description ?? ""} lengths differ, expected array ${format_value(expected)} length ${expected.length}, got ${format_value(actual)} length ${actual.length}`, + ); + } + for (let i = 0; i < actual.length; i++) { + const aHas = Object.prototype.hasOwnProperty.call(actual, i); + const eHas = Object.prototype.hasOwnProperty.call(expected, i); + if (aHas !== eHas) { + fail(`assert_array_equals: ${description ?? ""} property ${i}, property expected to be ${eHas} but was ${aHas}`); + } + if (!sameValue(actual[i], expected[i])) { + fail( + `assert_array_equals: ${description ?? ""} expected property ${i} to be ${format_value(expected[i])} but got ${format_value(actual[i])} (expected array ${format_value(expected)} got ${format_value(actual)})`, + ); + } + } +} + +// Byte-for-byte port of upstream testharness.js's assert_object_equals: walk the +// ACTUAL object's enumerable properties and recurse whenever actual[p] is a non-null +// object (regardless of expected[p]'s type), then require expected's properties to +// exist on actual. Browsers and Node run the suite under exactly these semantics. +function assert_object_equals(actual: any, expected: any, description?: string) { + if (typeof actual !== "object" || actual === null) { + fail(`assert_object_equals: ${description ?? ""} value is ${format_value(actual)}, expected object`); + } + const stack: unknown[] = []; + function check(a: any, e: any) { + stack.push(a); + for (const p in a) { + if (!Object.prototype.hasOwnProperty.call(e, p)) { + fail(`assert_object_equals: ${description ?? ""} unexpected property "${p}"`); + } + if (typeof a[p] === "object" && a[p] !== null) { + if (!stack.includes(a[p])) check(a[p], e[p]); + } else if (!Object.is(a[p], e[p])) { + fail( + `assert_object_equals: ${description ?? ""} property "${p}" expected ${format_value(e[p])} got ${format_value(a[p])}`, + ); + } + } + for (const p in e) { + if (!Object.prototype.hasOwnProperty.call(a, p)) { + fail(`assert_object_equals: ${description ?? ""} expected property "${p}" missing`); + } + } + stack.pop(); + } + check(actual, expected); +} + +function assert_own_property(object: any, property_name: any, description?: string) { + if (!Object.prototype.hasOwnProperty.call(object, property_name)) { + fail(`assert_own_property: ${description ?? ""} expected property ${format_value(property_name)} missing`); + } +} + +function assert_inherits(object: any, property_name: any, description?: string) { + const d = description ?? ""; + const isObj = (typeof object === "object" && object !== null) || typeof object === "function"; + if (!isObj) fail(`assert_inherits: ${d} provided value is not an object`); + if (!("hasOwnProperty" in object)) fail(`assert_inherits: ${d} provided value has no hasOwnProperty method`); + if (Object.prototype.hasOwnProperty.call(object, property_name)) { + fail(`assert_inherits: ${d} property ${format_value(property_name)} found on object expected in prototype chain`); + } + if (!(property_name in object)) { + fail(`assert_inherits: ${d} property ${format_value(property_name)} not found in prototype chain`); + } +} + +function assert_class_string(object: any, class_string: string, description?: string) { + const actual = {}.toString.call(object); + const expected = `[object ${class_string}]`; + if (!Object.is(actual, expected)) { + fail(`assert_class_string: ${description ?? ""} expected ${format_value(expected)} but got ${format_value(actual)}`); + } +} + +function assert_regexp_match(actual: any, expected: RegExp, description?: string) { + if (!expected.test(actual)) { + fail(`assert_regexp_match: ${description ?? ""} expected ${String(expected)} but got ${format_value(actual)}`); + } +} + +function assert_in_array(actual: any, expected: any[], description?: string) { + if (expected.indexOf(actual) === -1) { + fail( + `assert_in_array: ${description ?? ""} value ${format_value(actual)} not in array ${format_value(expected)}`, + ); + } +} + +function assert_greater_than(actual: any, expected: any, description?: string) { + if (!(typeof actual === "number" && actual > expected)) { + fail( + `assert_greater_than: ${description ?? ""} expected a number greater than ${format_value(expected)} but got ${format_value(actual)}`, + ); + } +} + +function assert_unreached(description?: string) { + fail(`assert_unreached: ${description ?? "reached unreachable code"}`); +} + +// --------------------------------------------------------------------------- +// assert_throws_* / promise_rejects_*: one checker per "what was thrown" +// contract, one driver for sync throws and one for rejections. + +type ThrownCheck = (e: unknown, context: string, description?: string) => void; + +const checkThrownJs = + (ctor: any): ThrownCheck => + (e: any, context, description) => { + // Mirrors testharness.js assert_throws_js_impl: an error-like object (name + message) + // of the right constructor. It deliberately does NOT require a `stack` property: + // engines may omit it for errors created with no JavaScript frames on the stack. + if (!(e instanceof Object) || !("name" in e) || !("message" in e)) { + fail(`${context}: ${description ?? ""} threw ${format_value(e)}, not an error type`); + } + if (!(e instanceof ctor)) { + fail(`${context}: ${description ?? ""} threw ${format_value(e)} (${e.name}), expected instance of ${ctor.name}`); + } + }; + +const checkThrownExactly = + (expected: unknown): ThrownCheck => + (e, context, description) => { + if (e !== expected) { + fail( + `${context}: ${description ?? ""} threw/rejected with ${format_value(e)} but we expected ${format_value(expected)}`, + ); + } + }; + +const checkThrownDom = + (name: string): ThrownCheck => + (e: any, context, description) => { + if (typeof e !== "object" || e === null || !(e instanceof DOMException)) { + fail(`${context}: ${description ?? ""} rejected/threw ${format_value(e)}, expected a DOMException`); + } + if (e.name !== name) { + fail(`${context}: ${description ?? ""} expected DOMException "${name}" but got "${e.name}"`); + } + }; + +function assertThrows(context: string, check: ThrownCheck, fn: () => unknown, description?: string) { + try { + fn(); + } catch (e) { + return void check(e, context, description); + } + fail(`${context}: ${description ?? ""} did not throw`); +} + +async function promiseRejects(context: string, check: ThrownCheck, promise: Promise, description?: string) { + let value: unknown; + try { + value = await promise; + } catch (e) { + return void check(e, context, description); + } + fail(`${context}: ${description ?? ""} ${format_value(value)} did not reject`); +} + +const assert_throws_js = (ctor: any, fn: () => unknown, description?: string) => + assertThrows("assert_throws_js", checkThrownJs(ctor), fn, description); +const assert_throws_exactly = (expected: unknown, fn: () => unknown, description?: string) => + assertThrows("assert_throws_exactly", checkThrownExactly(expected), fn, description); +const assert_throws_dom = (name: string, fn: () => unknown, description?: string) => + assertThrows("assert_throws_dom", checkThrownDom(name), fn, description); +const promise_rejects_js = (_t: unknown, ctor: any, promise: Promise, description?: string) => + promiseRejects("promise_rejects_js", checkThrownJs(ctor), promise, description); +const promise_rejects_exactly = (_t: unknown, expected: unknown, promise: Promise, description?: string) => + promiseRejects("promise_rejects_exactly", checkThrownExactly(expected), promise, description); +const promise_rejects_dom = (_t: unknown, name: string, promise: Promise, description?: string) => + promiseRejects("promise_rejects_dom", checkThrownDom(name), promise, description); + +// --------------------------------------------------------------------------- +// Test object handed to test()/promise_test() bodies. + +class WPTTest { + name: string; + cleanups: Array<() => unknown> = []; + // First error raised inside a t.step()/step_func() callback. WPT's step() + // swallows the exception (so stream machinery is not perturbed by an + // assertion failure inside e.g. an underlying sink method) and fails the + // subtest afterwards; we mirror that. + stepError: unknown = undefined; + hasStepError = false; + + constructor(name: string) { + this.name = name; + } + + step(fn: (...a: any[]) => T, thisObj?: unknown, ...args: any[]): T | undefined { + try { + return fn.apply(thisObj === undefined ? this : thisObj, args); + } catch (e) { + if (!this.hasStepError) { + this.hasStepError = true; + this.stepError = e; + } + // An exception inside a step fails the test immediately in WPT; for + // async_test that also completes it (otherwise `done()` never runs). + this.done(); + return undefined; + } + } + + step_func(fn: (...a: any[]) => unknown, thisObj?: unknown) { + const t = this; + return function (this: unknown, ...args: any[]) { + return t.step(fn, thisObj === undefined ? this : thisObj, ...args); + }; + } + + step_func_done(fn?: (...a: any[]) => unknown, thisObj?: unknown) { + const t = this; + return function (this: unknown, ...args: any[]) { + if (fn) t.step(fn, thisObj === undefined ? this : thisObj, ...args); + t.done(); + }; + } + + unreached_func(description?: string) { + return this.step_func(() => assert_unreached(description)); + } + + step_timeout(fn: (...a: any[]) => unknown, timeout: number, ...args: any[]) { + return setTimeout( + this.step_func(() => fn(...args)), + timeout, + ); + } + + add_cleanup(fn: () => unknown) { + this.cleanups.push(fn); + } + + // async_test completion signal: resolved by t.done() (or by a failing step). + readonly #done = Promise.withResolvers(); + get donePromise(): Promise { + return this.#done.promise; + } + done() { + this.#done.resolve(); + } + + // Cleanups run exactly once: the timeout path runs them eagerly, and the + // abandoned body's own `finally` must not run them a second time. + #ranCleanups = false; + async runCleanups() { + if (this.#ranCleanups) return; + this.#ranCleanups = true; + for (const fn of this.cleanups) { + await fn(); + } + } + + throwIfStepFailed() { + if (this.hasStepError) throw this.stepError; + } +} + +// --------------------------------------------------------------------------- +// Unhandled rejections. +// +// bun:test itself already implements per-subtest unhandled-rejection failure, +// unconditionally: under `bun test`, VirtualMachine::unhandled_rejection() +// short-circuits every unhandled rejection into the test runner (which fails +// the currently active test) BEFORE `process`/`self` `unhandledRejection` +// listeners are ever consulted (src/jsc/VirtualMachine.rs, `isBunTest`). Two +// consequences this shim depends on and that were verified empirically: +// 1. A `process.on("unhandledRejection"/"rejectionHandled")` listener NEVER +// fires inside `bun test`, so a shim-level tracker built on those events +// is dead code. The runner's old process-global no-op handler was +// likewise dead: it never suppressed anything. +// 2. bun:test is STRICTER than WPT here: WPT forgives a rejection that gets +// a handler attached later (`rejectionHandled`); bun:test does not. That +// strictness cannot be relaxed from userland. It currently causes zero +// failures across the vendored suite. +// The only thing the shim adds is the trailing task drain in `runToDrained` +// below, which holds the subtest open for two extra turns so settle-adjacent +// fallout from the body is attributed to the subtest that caused it. + +const macrotask = () => new Promise(r => setTimeout(r, 0)); + +async function runToDrained(run: () => Promise): Promise { + let failure: unknown; + let failed = false; + try { + await run(); + } catch (e) { + failure = e; + failed = true; + } + await macrotask(); + await macrotask(); + if (failed) throw failure; +} + +// --------------------------------------------------------------------------- +// test()/promise_test()/async_test() registration. Each subtest is handed to +// the runner as an async `run` closure; the runner maps it onto bun:test. + +// This function must not call `.then`/`.catch`/`.finally` on any promise: the +// patched-global.any.js subtests replace `Promise.prototype.then` inside their +// bodies, and the harness's own bookkeeping must not be observable through (or +// broken by) user-patched prototypes. `await` never consults `.then` on a +// native promise, so every chain here goes through an async function instead. +function withTimeout(t: WPTTest, body: Promise): Promise { + const { promise, resolve, reject } = Promise.withResolvers(); + const timer = setTimeout(async () => { + // Run the cleanups before reporting the hang: an abandoned body must not + // leave patched globals (e.g. an Object.prototype getter) installed. + try { + await t.runCleanups(); + } catch {} + const err = new Error(`WPT subtest "${t.name}" did not settle within ${SUBTEST_TIMEOUT_MS}ms`); + err.name = "WPTTimeout"; + reject(err); + }, SUBTEST_TIMEOUT_MS); + (async () => { + try { + await body; + resolve(); + } catch (e) { + reject(e); + } finally { + clearTimeout(timer); + } + })(); + return promise; +} + +function runSubtest(fn: (t: WPTTest) => unknown, name: string, requireThenable: boolean): Promise { + const t = new WPTTest(name); + return runToDrained(() => + withTimeout( + t, + (async () => { + try { + const result = fn(t); + if ( + requireThenable && + (result === null || result === undefined || typeof (result as any).then !== "function") + ) { + throw new AssertionError( + `promise_test: test body must return a 'thenable' object (returned ${format_value(result)})`, + ); + } + await result; + // Let a t.step_func firing in the settle-adjacent window land + // before deciding whether a step failed. + await macrotask(); + t.throwIfStepFailed(); + } finally { + await t.runCleanups(); + } + })(), + ), + ); +} + +// Exported (not installed on globalThis) because bun:test injects its own +// `test` binding into every module it loads; the runner feeds this in as a +// Function-constructor parameter instead. WPT's sync test() also accepts +// (name) or (fn) alone, but the vendored streams files always pass (fn, name). +const registerSubtest = (requireThenable: boolean) => (fn: (t: WPTTest) => unknown, name: string) => + registrar(name, () => runSubtest(fn, name, requireThenable), requireThenable ? "promise_test" : "test"); + +export const wptTest = registerSubtest(false); + +const g = globalThis as any; + +// promise_test bodies MUST return a thenable (upstream fails them otherwise); +// the sync test() must not, which is what makes the two non-identical. +g.promise_test = registerSubtest(true); + +// async_test(fn, name): the body runs synchronously and the subtest completes +// when t.done() fires (or a step throws, which marks it failed and done). +// async_test(fn, name) runs the body and completes on t.done(); the upstream +// single-argument form async_test(name) creates and RETURNS the Test object so +// the caller can drive it manually with t.step()/t.done() (idlharness does this +// for every member test). +g.async_test = (fnOrName: ((t: WPTTest) => unknown) | string, name?: string) => { + if (typeof fnOrName === "string") { + const t = new WPTTest(fnOrName); + registrar( + fnOrName, + () => + runToDrained(() => + withTimeout( + t, + (async () => { + try { + await t.donePromise; + await macrotask(); + t.throwIfStepFailed(); + } finally { + await t.runCleanups(); + } + })(), + ), + ), + "async_test", + ); + return t; + } + const fn = fnOrName; + const testName = name!; + registrar(testName, () => { + const t = new WPTTest(testName); + return runToDrained(() => + withTimeout( + t, + (async () => { + try { + const done = t.donePromise; + fn(t); + await done; + await macrotask(); + t.throwIfStepFailed(); + } finally { + await t.runCleanups(); + } + })(), + ), + ); + }, "async_test"); + return undefined; +}; + +g.step_timeout = (fn: (...a: any[]) => unknown, timeout: number, ...args: any[]) => setTimeout(fn, timeout, ...args); + +g.assert_equals = assert_equals; +g.assert_not_equals = assert_not_equals; +g.assert_true = assert_true; +g.assert_false = assert_false; +g.assert_array_equals = assert_array_equals; +g.assert_object_equals = assert_object_equals; +g.assert_greater_than = assert_greater_than; +g.assert_own_property = assert_own_property; +g.assert_inherits = assert_inherits; +g.assert_class_string = assert_class_string; +g.assert_regexp_match = assert_regexp_match; +g.assert_in_array = assert_in_array; +g.assert_unreached = assert_unreached; +g.assert_throws_js = assert_throws_js; +g.assert_throws_exactly = assert_throws_exactly; +g.assert_throws_dom = assert_throws_dom; +g.promise_rejects_js = promise_rejects_js; +g.promise_rejects_exactly = promise_rejects_exactly; +g.promise_rejects_dom = promise_rejects_dom; +g.format_value = format_value; + +// testharness.js APIs the shim deliberately does not implement. None of the +// vendored files use them today; a future re-vendor that does must fail +// loudly per call instead of silently truncating a file. +for (const name of [ + "setup", + "promise_setup", + "add_completion_callback", + "subsetTest", + "fetch_tests_from_worker", + "single_test", + "assert_implements", + "assert_implements_optional", +]) { + g[name] = () => { + throw new Error(`wpt shim: ${name}() is not implemented`); + }; +} + +// The .any.js "self" global. In Bun `self` already aliases globalThis; make it +// explicit so resource scripts assigning `self.foo = ...` create globals. +g.self = globalThis; + +// /common/gc.js prefers the standardized TestUtils.gc() when present; wire it +// to Bun's synchronous full collection. +g.TestUtils = { + gc: async () => { + Bun.gc(true); + }, +}; diff --git a/test/js/web/encoding/text-decoder-stream.test.ts b/test/js/web/encoding/text-decoder-stream.test.ts index f3c42f5b5567..5e289eccd6cf 100644 --- a/test/js/web/encoding/text-decoder-stream.test.ts +++ b/test/js/web/encoding/text-decoder-stream.test.ts @@ -180,3 +180,13 @@ import { readableStreamFromArray } from "harness"; }).toThrow(Error); }); } + +// Web IDL: `new TextDecoderStream(label, options)` treats undefined/null options as {}. +test("TextDecoderStream accepts undefined and null options", () => { + for (const options of [undefined, null]) { + const stream = new TextDecoderStream("utf-8", options); + expect(stream.fatal).toBe(false); + expect(stream.ignoreBOM).toBe(false); + } + expect(new TextDecoderStream("utf-8", { fatal: true }).fatal).toBe(true); +}); diff --git a/test/js/web/fetch/body-clone.test.ts b/test/js/web/fetch/body-clone.test.ts index df1b13b18354..65a49c404544 100644 --- a/test/js/web/fetch/body-clone.test.ts +++ b/test/js/web/fetch/body-clone.test.ts @@ -660,7 +660,7 @@ test("new Request(request) with a locked stream body throws a catchable TypeErro const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect({ stdout: stdout.trim().split("\n"), stderr, exitCode }).toEqual({ - stdout: ["caught TypeError: ReadableStream is locked", "done"], + stdout: ["caught TypeError: Invalid state: ReadableStream is locked", "done"], stderr: "", exitCode: 0, }); diff --git a/test/js/web/fetch/body.test.ts b/test/js/web/fetch/body.test.ts index e2b9f665ae73..8169eb9baf4a 100644 --- a/test/js/web/fetch/body.test.ts +++ b/test/js/web/fetch/body.test.ts @@ -736,3 +736,26 @@ describe.concurrent("string body consumption does not leak", () => { }); } }); + +// https://github.com/oven-sh/bun/issues/6860 +describe("constructing a body from an unusable ReadableStream", () => { + const bytes = () => + new ReadableStream({ + start(c) { + c.enqueue(new TextEncoder().encode("x")); + c.close(); + }, + }); + test("a disturbed stream throws a TypeError", async () => { + const rs = bytes(); + await new Response(rs).text(); + expect(() => new Response(rs)).toThrow(TypeError); + expect(() => new Request("http://example.com/", { method: "POST", body: rs, duplex: "half" })).toThrow(TypeError); + }); + test("a locked stream throws a TypeError", () => { + const rs = bytes(); + rs.getReader(); + expect(() => new Response(rs)).toThrow(TypeError); + expect(() => new Request("http://example.com/", { method: "POST", body: rs, duplex: "half" })).toThrow(TypeError); + }); +}); diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 931c4b8685e8..a5ff4cd38938 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -149,7 +149,7 @@ describe.concurrent("fetch() with streaming", () => { await promise; }); - it("rejects with ERR_STREAM_CANNOT_PIPE when the request body stream is already locked", async () => { + it("throws a TypeError when the request body stream is already locked", async () => { using server = Bun.serve({ port: 0, async fetch(req) { @@ -163,15 +163,14 @@ describe.concurrent("fetch() with streaming", () => { controller.close(); }, }); - // Lock the stream before fetch consumes it. fetch must reject at the pipe - // boundary rather than proceeding as if the stream were usable. + // A locked (or disturbed) body init is rejected at Request construction with a + // TypeError (fetch spec; Node agrees on the error). Like Bun's other fetch + // argument errors, it surfaces synchronously. stream.getReader(); - const promise = fetch(server.url, { method: "POST", body: stream }); - await expect(promise).rejects.toMatchObject({ - code: "ERR_STREAM_CANNOT_PIPE", - message: "Stream already used, please create a new one", - }); + expect(() => fetch(server.url, { method: "POST", body: stream })).toThrow( + expect.objectContaining({ name: "TypeError", message: "Body object should not be disturbed or locked" }), + ); }); it("can deflate with and without headers #4478", async () => { diff --git a/test/js/web/streams/streams.test.js b/test/js/web/streams/streams.test.js index c3a1dbe85065..0efa039c9e9b 100644 --- a/test/js/web/streams/streams.test.js +++ b/test/js/web/streams/streams.test.js @@ -573,6 +573,244 @@ it("ReadableStream (default)", async () => { expect(chunks[0].join("")).toBe(Buffer.from("abdefgh").join("")); }); +describe("multi-chunk consumers produce exactly the concatenated bytes", () => { + const source = chunks => + new ReadableStream({ + start(controller) { + for (const chunk of chunks) controller.enqueue(chunk); + controller.close(); + }, + }); + const base = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + const cases = { + "many typed-array views with offsets": { + chunks: () => [base.subarray(1, 4), base.subarray(0, 0), base.subarray(4, 9), base.subarray(9)], + expected: [1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + "mixed ArrayBuffer, Uint8Array, and DataView": { + chunks: () => [base.slice(0, 3).buffer, base.subarray(3, 6), new DataView(base.buffer, 6, 4)], + expected: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + }, + "strings mixed with bytes": { + chunks: () => ["ab", new Uint8Array([1, 2]), "cd"], + expected: [...Buffer.from("ab"), 1, 2, ...Buffer.from("cd")], + }, + "only strings": { + chunks: () => ["hé", "llo"], + expected: [...Buffer.from("héllo")], + }, + }; + for (const [name, { chunks, expected }] of Object.entries(cases)) { + it(name, async () => { + expect(Array.from(await Bun.readableStreamToBytes(source(chunks())))).toEqual(expected); + expect(Array.from(new Uint8Array(await Bun.readableStreamToArrayBuffer(source(chunks()))))).toEqual(expected); + expect(Array.from(await new Response(source(chunks())).bytes())).toEqual(expected); + expect(Array.from(new Uint8Array(await new Response(source(chunks())).arrayBuffer()))).toEqual(expected); + }); + } + + const textCases = { + "only strings": { chunks: () => ["hé", "llo"], text: "héllo" }, + "strings mixed with bytes": { chunks: () => ["ab", new Uint8Array([49, 50]), "cd"], text: "ab12cd" }, + "many typed-array views": { + chunks: () => [new TextEncoder().encode("a\u00e9"), new TextEncoder().encode("b")], + text: "aéb", + }, + "single string with a BOM": { chunks: () => ["\uFEFFabc"], text: "abc" }, + "a BOM split across string chunks": { chunks: () => ["\uFEFF", "\uFEFFabc"], text: "abc" }, + "a BOM string chunk before bytes": { chunks: () => ["\uFEFF", new TextEncoder().encode("abc")], text: "abc" }, + "lone surrogate in a string chunk": { chunks: () => ["a\uD800b"], text: "a\uD800b" }, + "a BOM string chunk after bytes": { chunks: () => [new TextEncoder().encode("ab"), "\uFEFFcd"], text: "abcd" }, + "a surrogate pair split across string chunks after bytes": { + chunks: () => [new TextEncoder().encode("x"), "\uD83D", "\uDE00"], + text: "x\u{1F600}", + }, + "invalid UTF-8 bytes": { chunks: () => [new Uint8Array([0x61, 0xff, 0x62])], text: "a\uFFFDb" }, + }; + for (const [name, { chunks, text }] of Object.entries(textCases)) { + it(`text: ${name}`, async () => { + expect(await Bun.readableStreamToText(source(chunks()))).toBe(text); + expect(await new Response(source(chunks())).text()).toBe(text); + }); + } + + it("a direct stream's buffered write reaches a waiting reader at the end of the tick", async () => { + // No explicit flush() and pull never returns: only the controller's end-of-tick + // flush can deliver the chunk. + const rs = new ReadableStream({ + type: "direct", + pull(c) { + c.write("tick"); + return new Promise(() => {}); + }, + }); + const reader = rs.getReader(); + const result = await Promise.race([reader.read(), Bun.sleep(1000).then(() => "TIMEOUT")]); + expect(result).not.toBe("TIMEOUT"); + expect(new TextDecoder().decode(result.value)).toBe("tick"); + }); + + it("an async generator Response body delivers each yield to a JS reader as it is produced", async () => { + async function* gen() { + for (let i = 0; i < 3; i++) { + yield `c${i};`; + await Bun.sleep(30); + } + } + const reader = new Response(gen()).body.getReader(); + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(new TextDecoder().decode(value)); + } + // Batched-to-one delivery means the end-of-tick flush regressed. + expect(chunks.length).toBeGreaterThanOrEqual(3); + expect(chunks.join("")).toBe("c0;c1;c2;"); + }); + + it("canceling a direct stream's reader settles its pending read", async () => { + const rs = new ReadableStream({ + type: "direct", + async pull() { + await new Promise(() => {}); + }, + }); + const reader = rs.getReader(); + const read = reader.read(); + await reader.cancel("bye"); + // https://github.com/oven-sh/bun/pull/33193: this read hung forever. + const result = await read; + expect(result.done).toBe(true); + }); + + it("releasing a direct stream's reader during an async pull does not crash close", async () => { + const rs = new ReadableStream({ + type: "direct", + async pull(c) { + await Promise.resolve(); + c.write(new Uint8Array(10)); + c.end(); + }, + }); + const reader = rs.getReader(); + const read = reader.read().catch(e => e); + reader.releaseLock(); + await read; + await Bun.sleep(0); + // The flushed final chunk is delivered to the NEXT reader. + const { value } = await rs.getReader().read(); + expect(value.byteLength).toBe(10); + }); + + it("a patched Object.prototype.then that releases the reader mid-resolution does not crash", async () => { + let releaseNow = null; + Object.defineProperty(Object.prototype, "then", { + configurable: true, + get() { + if (releaseNow) { + const release = releaseNow; + releaseNow = null; + try { + release(); + } catch {} + } + return undefined; + }, + }); + try { + let ctrl; + const rs = new ReadableStream({ + type: "bytes", + start(c) { + ctrl = c; + }, + }); + const reader = rs.getReader(); + const read = reader.read().catch(() => {}); + releaseNow = () => reader.releaseLock(); + ctrl.enqueue(new Uint8Array(8)); + await read; + + let ctrl2; + const rs2 = new ReadableStream({ + type: "bytes", + start(c) { + ctrl2 = c; + }, + }); + const byobReader = rs2.getReader({ mode: "byob" }); + const a = byobReader.read(new Uint8Array(4)).catch(() => {}); + const b = byobReader.read(new Uint8Array(4)).catch(() => {}); + ctrl2.close(); + releaseNow = () => byobReader.releaseLock(); + ctrl2.byobRequest?.respond(0); + await Promise.all([a, b]); + } finally { + delete Object.prototype.then; + } + expect(true).toBe(true); + }); + + it("new ReadableStreamDefaultReader(lazyNativeStream) materializes it like getReader()", async () => { + using dir = tempDir("reader-ctor", { "data.txt": "reader-ctor-data" }); + const reader = new ReadableStreamDefaultReader(Bun.file(join(String(dir), "data.txt")).stream()); + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + expect(Buffer.concat(chunks).toString()).toBe("reader-ctor-data"); + }); + + it("a queuing strategy size() result is coerced like Node (valueOf)", async () => { + let calls = 0; + const rs = new ReadableStream( + { + start(c) { + c.enqueue("a"); + c.close(); + }, + }, + { highWaterMark: 5, size: () => ({ valueOf: () => (calls++, 2) }) }, + ); + expect(await Bun.readableStreamToText(rs)).toBe("a"); + expect(calls).toBe(1); + const written = []; + const ws = new WritableStream( + { + write(c) { + written.push(c); + }, + }, + { highWaterMark: 5, size: () => ({ valueOf: () => 3 }) }, + ); + const writer = ws.getWriter(); + await writer.write("z"); + expect(written).toEqual(["z"]); + }); + + it("text: an invalid chunk rejects rather than throwing", async () => { + const p = Bun.readableStreamToText(source([42])); + expect(p).toBeInstanceOf(Promise); + await expect(p).rejects.toThrow(expect.objectContaining({ name: "TypeError" })); + await expect(new Response(source([42])).text()).rejects.toThrow(expect.objectContaining({ name: "TypeError" })); + }); + + it("a detached chunk throws", () => { + const chunk = new Uint8Array([1, 2, 3]); + structuredClone(chunk.buffer, { transfer: [chunk.buffer] }); + // The chunk array is available synchronously, so the failure is synchronous too. + expect(() => Bun.readableStreamToBytes(source([new Uint8Array([9]), chunk]))).toThrow( + expect.objectContaining({ + code: "ERR_INVALID_STATE", + message: "Invalid state: Cannot validate on a detached buffer", + }), + ); + }); +}); + it("readableStreamToArray", async () => { var queue = [Buffer.from("abdefgh")]; var stream = new ReadableStream({ @@ -842,16 +1080,20 @@ it("ReadableStream rejects pending reads when the lock is released", async () => let read = reader.read(); reader.releaseLock(); - expect(read).rejects.toThrow( + // Released locks reject pending reads and `closed` with a TypeError (WHATWG), + // carrying Node's ERR_INVALID_STATE code and messages (node compatibility). + await expect(read).rejects.toThrow( expect.objectContaining({ - name: "AbortError", - code: "ERR_STREAM_RELEASE_LOCK", + name: "TypeError", + code: "ERR_INVALID_STATE", + message: "Invalid state: Releasing reader", }), ); - expect(reader.closed).rejects.toThrow( + await expect(reader.closed).rejects.toThrow( expect.objectContaining({ - name: "AbortError", - code: "ERR_STREAM_RELEASE_LOCK", + name: "TypeError", + code: "ERR_INVALID_STATE", + message: "Invalid state: Reader released", }), ); @@ -1372,3 +1614,313 @@ it("ReadableStream BYOB read pending at cancel() resolves with undefined", async expect(value).toBeUndefined(); await reader.closed; }); + +describe("pipeTo from a byte source", () => { + it("delivers the enqueued chunks and resolves", async () => { + const rs = new ReadableStream({ + type: "bytes", + start(c) { + c.enqueue(new Uint8Array([1, 2, 3])); + c.enqueue(new Uint8Array([4, 5])); + c.close(); + }, + }); + const chunks = []; + await rs.pipeTo( + new WritableStream({ + write(chunk) { + chunks.push(Array.from(chunk)); + }, + }), + ); + expect(chunks).toEqual([ + [1, 2, 3], + [4, 5], + ]); + }); + + it("pipeThrough an identity TransformStream forwards the chunks", async () => { + const rs = new ReadableStream({ + type: "bytes", + start(c) { + c.enqueue(new Uint8Array([1, 2, 3])); + c.enqueue(new Uint8Array([4, 5])); + c.close(); + }, + }); + const reader = rs.pipeThrough(new TransformStream()).getReader(); + const chunks = []; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + chunks.push(Array.from(value)); + } + expect(chunks).toEqual([ + [1, 2, 3], + [4, 5], + ]); + }); + + it("a pull-based byte source responding via byobRequest delivers its bytes", async () => { + let written = 0; + const rs = new ReadableStream({ + type: "bytes", + autoAllocateChunkSize: 4, + pull(controller) { + if (written >= 8) { + controller.close(); + return; + } + const view = controller.byobRequest.view; + // respond() detaches `view`'s buffer, so its byteLength reads 0 afterwards. + const byteLength = view.byteLength; + for (let i = 0; i < byteLength; i++) { + view[i] = written + i; + } + controller.byobRequest?.respond(byteLength); + written += byteLength; + }, + }); + const received = []; + await rs.pipeTo( + new WritableStream({ + write(chunk) { + received.push(...chunk); + }, + }), + ); + expect(received).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + }); + + it("preventClose: false closes the destination when the byte source closes", async () => { + const rs = new ReadableStream({ + type: "bytes", + start(c) { + c.enqueue(new Uint8Array([9])); + c.close(); + }, + }); + const chunks = []; + let closed = false; + await rs.pipeTo( + new WritableStream({ + write(chunk) { + chunks.push(Array.from(chunk)); + }, + close() { + closed = true; + }, + }), + { preventClose: false }, + ); + expect(chunks).toEqual([[9]]); + expect(closed).toBe(true); + }); +}); + +// Async stack frames on stream errors created inside native reactions (no JS frames of +// their own): the `for await` and `pipeTo` awaiters must get the awaiting function's frames. +function serveStalledBody() { + // One flushed chunk, then the body stalls until the test releases it (a pull left + // parked at process exit would leave the aborted request's native sink alive). + const { promise: parked, resolve: unpark } = Promise.withResolvers(); + const server = Bun.serve({ + port: 0, + idleTimeout: 0, + async fetch() { + return new Response( + new ReadableStream({ + type: "direct", + async pull(c) { + c.write("part1"); + await c.flush(); + await parked; + c.end(); + }, + }), + { headers: { "Content-Length": "100000" } }, + ); + }, + }); + return { server, unpark }; +} + +test("for await over a stream that errors natively includes async stack frames", async () => { + const { server, unpark } = serveStalledBody(); + async function level2() { + const res = await fetch(server.url); + const iterator = res.body[Symbol.asyncIterator](); + await iterator.next(); + // The connection dies while the loop below is awaiting the next chunk, so the + // error is created from a native callback with no JavaScript frames of its own. + server.stop(true); + while (!(await iterator.next()).done) {} + } + async function level1() { + await level2(); + } + let caught; + try { + await level1(); + } catch (e) { + caught = e; + } finally { + unpark(); + await Bun.sleep(0); + server.stop(true); + } + expect(caught).toBeDefined(); + expect(caught.stack).toContain("at async level2"); + expect(caught.stack).toContain("at async level1"); +}); + +test("pipeTo from a stream that errors natively includes async stack frames", async () => { + const { server, unpark } = serveStalledBody(); + async function level2() { + const res = await fetch(server.url); + await res.body.pipeTo( + new WritableStream({ + write() { + server.stop(true); + }, + }), + ); + } + async function level1() { + await level2(); + } + let caught; + try { + await level1(); + } catch (e) { + caught = e; + } finally { + unpark(); + await Bun.sleep(0); + server.stop(true); + } + expect(caught).toBeDefined(); + expect(caught.stack).toContain("at async level2"); + expect(caught.stack).toContain("at async level1"); +}); + +// https://github.com/oven-sh/bun/issues/6860 +describe("Bun.readableStreamTo* on an already used stream", () => { + const consumers = [ + "readableStreamToText", + "readableStreamToArrayBuffer", + "readableStreamToBytes", + "readableStreamToJSON", + "readableStreamToArray", + "readableStreamToBlob", + ]; + const makeStream = () => + new ReadableStream({ + start(c) { + c.enqueue(new TextEncoder().encode('"hello"')); + c.close(); + }, + }); + + for (const consumer of consumers) { + test(`${consumer} rejects after the stream was consumed by a Bun helper`, async () => { + const stream = makeStream(); + await Bun.readableStreamToText(stream); + await expect(Bun[consumer](stream)).rejects.toThrow("ReadableStream has already been used"); + }); + } + + test("rejects after the stream was consumed through a reader", async () => { + const stream = makeStream(); + const reader = stream.getReader(); + while (!(await reader.read()).done) {} + reader.releaseLock(); + await expect(Bun.readableStreamToText(stream)).rejects.toThrow("ReadableStream has already been used"); + }); + + test("rejects after the stream was cancelled", async () => { + const stream = makeStream(); + await stream.cancel(); + await expect(Bun.readableStreamToArrayBuffer(stream)).rejects.toThrow("ReadableStream has already been used"); + }); + + test("still reports a locked stream as locked", async () => { + const stream = makeStream(); + const reader = stream.getReader(); + await expect(Bun.readableStreamToText(stream)).rejects.toThrow("ReadableStream is locked"); + reader.releaseLock(); + }); + + test("new Response(stream) after consumption still throws", async () => { + const stream = makeStream(); + await Bun.readableStreamToText(stream); + expect(() => new Response(stream).arrayBuffer()).toThrow(); + }); +}); + +// Text assembly past the string limit must throw a catchable out-of-memory error, never +// abort the process. The synthetic allocation limit makes the path testable without +// multi-gigabyte inputs; a subprocess isolates the lowered limit. +describe("text consumers reject strings over the string allocation limit", () => { + const runInSubprocess = async source => { + const script = ` + import { setSyntheticAllocationLimitForTesting } from "bun:internal-for-testing"; + setSyntheticAllocationLimitForTesting(32 * 1024 * 1024); + const big = "x".repeat(8 * 1024 * 1024); + let caught; + try { + ${source} + } catch (e) { + caught = e; + } + if (!caught) throw new Error("expected an out-of-memory error"); + console.log(caught.message); + `; + const proc = Bun.spawn({ cmd: [bunExe(), "-e", script], env: bunEnv, stdout: "pipe", stderr: "pipe" }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; + }; + + test("Bun.readableStreamToText", async () => { + const { stdout, stderr, exitCode } = await runInSubprocess(` + const stream = new ReadableStream({ + start(c) { + for (let i = 0; i < 6; i++) c.enqueue(big); + c.close(); + }, + }); + await Bun.readableStreamToText(stream); + `); + expect({ stdout: stdout.trim(), exitCode }).toEqual({ stdout: "Out of memory", exitCode: 0 }); + }); + + test("direct stream text sink", async () => { + const { stdout, stderr, exitCode } = await runInSubprocess(` + const stream = new ReadableStream({ + type: "direct", + pull(c) { + for (let i = 0; i < 6; i++) c.write(big); + c.end(); + }, + }); + await Bun.readableStreamToText(stream); + `); + expect({ stdout: stdout.trim(), exitCode }).toEqual({ stdout: "Out of memory", exitCode: 0 }); + }); + + test("mixed string and binary chunks", async () => { + const { stdout, stderr, exitCode } = await runInSubprocess(` + const stream = new ReadableStream({ + start(c) { + for (let i = 0; i < 6; i++) { + c.enqueue(big); + c.enqueue(new Uint8Array(1)); + } + c.close(); + }, + }); + await Bun.readableStreamToText(stream); + `); + expect({ stdout: stdout.trim(), exitCode }).toEqual({ stdout: "Out of memory", exitCode: 0 }); + }); +});