char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
688e96e
main
1mod common; 2 3use common:: run; 4 5# [ test ] 6fn stream_transfers_preserve_backpressure_chunks_and_failure_propagation () { 7let output =run ( 8r#" 9let canceled; 10const source = new ReadableStream({ 11start(controller) { controller.enqueue({value: 1}); controller.close(); }, 12cancel(reason) { canceled = reason; }, 13}); 14const movedReadable = structuredClone(source, {transfer: [source]}); 15if (!source.locked || movedReadable === source) throw Error('Readable transfer lock'); 16try { movedReadable.getReader({mode: 'byob'}); throw Error('Transferred stream retained byte mode'); } 17catch (error) { if (!(error instanceof TypeError)) throw error; } 18let pulls = 0; 19const lazySource = new ReadableStream({pull(controller) { pulls++; controller.enqueue(1); }}); 20const lazyClone = structuredClone(lazySource, {transfer: [lazySource]}); 21if (pulls !== 0) throw Error('Readable transfer pulled before demand'); 22await lazyClone.getReader().read(); 23if (pulls === 0) throw Error('Readable transfer did not pull on demand'); 24const chunk = (await movedReadable.getReader().read()).value; 25if (chunk.value !== 1) throw Error('Readable transfer chunk'); 26const cancelSource = new ReadableStream({cancel(reason) { canceled = reason; }}); 27const movedCancel = structuredClone(cancelSource, {transfer: [cancelSource]}); 28const cancelReason = {cancel: true}; 29await movedCancel.cancel(cancelReason); 30if (canceled === cancelReason || !canceled.cancel) throw Error('Readable transfer cancel'); 31const cancelFailure = {cancelFailure: true}; 32const failedCancelSource = new ReadableStream({cancel() { throw cancelFailure; }}); 33const failedCancelClone = structuredClone(failedCancelSource, {transfer: [failedCancelSource]}); 34try { await failedCancelClone.cancel(); throw Error('Readable cancel rejection'); } 35catch (error) { if (error === cancelFailure || !error.cancelFailure) throw error; } 36 37const written = []; 38const writable = new WritableStream({write(value) { written.push(value); }}); 39 40const movedWritable = structuredClone(writable, {transfer: [writable]}); 41if (!writable.locked || movedWritable === writable) throw Error('Writable transfer lock'); 42const writer = movedWritable.getWriter(); 43await writer.write({value: 2}); 44await writer.close(); 45if (written.length !== 1 || written[0].value !== 2) throw Error('Writable transfer writes'); 46const closedSource = new WritableStream(), closedWriter = closedSource.getWriter(); 47await closedWriter.close(); 48closedWriter.releaseLock(); 49const closedClone = structuredClone(closedSource, {transfer: [closedSource]}); 50try { await closedClone.getWriter().write(1); throw Error('Closed writable transfer'); } 51catch (error) { if (!(error instanceof TypeError)) throw error; } 52const writeReason = {writeFailure: true}; 53const failedWritable = new WritableStream({write() { throw writeReason; }}); 54const movedFailedWritable = structuredClone(failedWritable, {transfer: [failedWritable]}); 55try { await movedFailedWritable.getWriter().write(1); throw Error('Writable transfer error'); } 56catch (error) { if (error === writeReason || !error.writeFailure) throw error; } 57let cloneWriteAbort; 58const uncloneableWriteSource = new WritableStream({abort(reason) { cloneWriteAbort = reason; }}); 59const uncloneableWriteClone = structuredClone(uncloneableWriteSource, {transfer: [uncloneableWriteSource]}); 60try { await uncloneableWriteClone.getWriter().write(() => {}); throw Error('Uncloneable writable chunk'); } 61catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; } 62if (!(cloneWriteAbort instanceof DOMException) || cloneWriteAbort.name !== 'DataCloneError') { 63throw Error('Writable clone failure did not abort the source'); 64} 65 66const uncommitted = new ReadableStream(), transferBuffer = new ArrayBuffer(1); 67try { 68structuredClone({stream: uncommitted, fail() {}}, {transfer: [uncommitted, transferBuffer]}); 69throw Error('Transfer serialization failure'); 70} catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; } 71if (transferBuffer.detached || uncommitted.locked) throw Error('Uncommitted stream transfer changed source'); 72const recovered = structuredClone(uncommitted, {transfer: [uncommitted]}); 73if (recovered === uncommitted || !uncommitted.locked) throw Error('Failed transfer poisoned later clone'); 74 75const transform = new TransformStream({transform(value, controller) { controller.enqueue(value * 2); }}); 76const movedTransform = structuredClone(transform, {transfer: [transform]}); 77if (Object.hasOwn(movedTransform, 'readable') || Object.hasOwn(movedTransform, 'writable')) { 78throw Error('Transform transfer replaced public accessors'); 79} 80const reader = movedTransform.readable.getReader(), transformWriter = movedTransform.writable.getWriter(); 81const firstWrite = transformWriter.write(3); 82if ((await reader.read()).value !== 6) throw Error('Transform transfer'); 83await firstWrite; 84reader.releaseLock(); 85transformWriter.releaseLock(); 86const movedAgain = structuredClone(movedTransform, {transfer: [movedTransform]}); 87const secondReader = movedAgain.readable.getReader(), secondWriter = movedAgain.writable.getWriter(); 88const secondWrite = secondWriter.write(4); 89if ((await secondReader.read()).value !== 8) throw Error('Retransferred transform'); 90await secondWrite; 91await secondWriter.close(); 92if (!(await secondReader.read()).done) throw Error('Retransferred transform close'); 93 94const reason = {failure: true}; 95const failedSource = new ReadableStream({pull() {throw reason;}}); 96const failed = structuredClone(failedSource, {transfer: [failedSource]}); 97try { await failed.getReader().read(); throw Error('Readable transfer error'); } 98catch (error) { if (error === reason || error.failure !== true) throw error; } 99let cloneFailureCancellation; 100const uncloneableChunkSource = new ReadableStream({ 101pull(controller) { controller.enqueue(() => {}); }, 102cancel(error) { cloneFailureCancellation = error; }, 103}); 104const uncloneableChunkClone = structuredClone(uncloneableChunkSource, {transfer: [uncloneableChunkSource]}); 105try { await uncloneableChunkClone.getReader().read(); throw Error('Uncloneable stream chunk'); } 106catch (error) { if (!(error instanceof DOMException) || error.name !== 'DataCloneError') throw error; } 107if (!(cloneFailureCancellation instanceof DOMException) || cloneFailureCancellation.name !== 'DataCloneError') { 108throw Error('Chunk clone failure did not cancel the source'); 109} 110 111const byteSource = new ReadableStream({ 112type: 'bytes', 113start(controller) { controller.enqueue(new Uint8Array([1, 2, 3])); controller.close(); }, 114}); 115const byteClone = structuredClone(byteSource, {transfer: [byteSource]}); 116const byteReader = byteClone.getReader(); 117const byteResult = await byteReader.read(); 118if (byteResult.done || byteResult.value.join() !== '1,2,3') throw Error('Transferred byte stream read'); 119if (!(await byteReader.read()).done) throw Error('Transferred byte stream EOF'); 120console.log('stream transfer'); 121"# , 122& [], 123); 124assert_eq! ( output. stdout , b"stream transfer\n" ); 125} 126 127# [ test ] 128fn release_and_internal_errors_do_not_create_unhandled_rejections () { 129let output =run ( 130r#" 131for (const make of [ 132() => new ReadableStream(), 133() => new ReadableStream({type:'bytes'}), 134() => new ReadableStream({start(c) {c.error('source failure');}}) 135]) { 136const reader = make().getReader(); 137reader.releaseLock(); 138} 139const byob = new ReadableStream({type:'bytes'}).getReader({mode:'byob'}); 140byob.releaseLock(); 141for (const make of [ 142() => new WritableStream(), 143() => new WritableStream({start(c) {c.error('sink failure');}}) 144]) { 145const writer = make().getWriter(); 146writer.releaseLock(); 147} 148await new Promise(resolve => setTimeout(resolve)); 149console.log('handled'); 150"# , 151& [], 152); 153assert_eq! ( output. stdout , b"handled\n" ); 154} 155 156# [ test ] 157fn pull_and_write_failures_preserve_reasons_and_observable_promises () { 158let output =run ( 159r#" 160const reason = {failure:true}; 161async function rejects(promise) { 162try {await promise; throw Error('Expected rejection');} 163catch (error) {if (error !== reason) throw error;} 164} 165for (const failure of [ 166() => {throw reason;}, 167() => Promise.reject(reason), 168() => ({then(resolve, reject) {reject(reason);}}) 169]) { 170const reader = new ReadableStream({pull:failure}).getReader(); 171await Promise.all([rejects(reader.read()), rejects(reader.closed)]); 172reader.releaseLock(); 173const writer = new WritableStream({write:failure}).getWriter(); 174await Promise.all([rejects(writer.write(1)), rejects(writer.closed)]); 175writer.releaseLock(); 176} 177console.log('reasons'); 178"# , 179& [], 180); 181assert_eq! ( output. stdout , b"reasons\n" ); 182} 183 184# [ test ] 185fn piping_propagates_source_and_sink_failures_without_leaking_internal_rejections () { 186let output =run ( 187r#" 188const reason = {failure:true}; 189let aborted, canceled; 190const failedSource = new ReadableStream({pull() {throw reason;}}); 191try { 192await failedSource.pipeTo(new WritableStream({abort(error) {aborted = error;}})); 193throw Error('Successful failed source'); 194} catch (error) {if (error !== reason) throw error;} 195if (aborted !== reason || failedSource.locked) throw Error('Source failure propagation'); 196const source = new ReadableStream({ 197pull(c) {c.enqueue(1);}, 198cancel(error) {canceled = error;} 199}); 200const failedSink = new WritableStream({write() {return Promise.reject(reason);}}); 201try {await source.pipeTo(failedSink); throw Error('Successful failed sink');} 202catch (error) {if (error !== reason) throw error;} 203if (canceled !== reason || source.locked || failedSink.locked) throw Error('Sink failure propagation'); 204console.log('piping'); 205"# , 206& [], 207); 208assert_eq! ( output. stdout , b"piping\n" ); 209} 210 211# [ test ] 212fn public_operation_promises_and_observer_promises_keep_their_identities () { 213let output =run ( 214r#" 215const reader = new ReadableStream({start(c) {c.close();}}).getReader(); 216const reads = [reader.read(), reader.read()]; 217if (reads[0] === reads[1]) throw Error('Shared read results'); 218if ((await Promise.all(reads)).some(result => !result.done)) throw Error('Closed reads'); 219const cancels = [reader.cancel(), reader.cancel()]; 220if (cancels[0] === cancels[1]) throw Error('Shared closed cancel results'); 221await Promise.all(cancels); 222reader.releaseLock(); 223 224const a = new ReadableStream().getReader(), b = new ReadableStream().getReader(); 225if (a.closed !== a.closed || a.closed === b.closed) throw Error('Reader closed identity'); 226const cancellations = [a.cancel(), b.cancel()]; 227if (cancellations[0] === cancellations[1]) throw Error('Shared cancel results'); 228await Promise.all(cancellations); 229a.releaseLock(); b.releaseLock(); 230 231const writer = new WritableStream().getWriter(); 232const other = new WritableStream().getWriter(); 233if (writer.ready !== writer.ready || writer.ready === other.ready || 234writer.closed !== writer.closed || writer.closed === other.closed) { 235throw Error('Writer observer identity'); 236} 237const writes = [writer.write(1), writer.write(2)]; 238if (writes[0] === writes[1]) throw Error('Shared write results'); 239await Promise.all(writes); 240await writer.close(); 241const aborts = [writer.abort(), writer.abort()]; 242if (aborts[0] === aborts[1]) throw Error('Shared closed abort results'); 243await Promise.all(aborts); 244await other.close(); 245writer.releaseLock(); other.releaseLock(); 246console.log('identity'); 247"# , 248& [], 249); 250assert_eq! ( output. stdout , b"identity\n" ); 251} 252 253# [ test ] 254fn default_transforms_keep_backpressure_independent_between_streams () { 255let output =run ( 256r#" 257const a = new TransformStream(), b = new TransformStream(); 258const ar = a.readable.getReader(), br = b.readable.getReader(); 259const aw = a.writable.getWriter(), bw = b.writable.getWriter(); 260let aWritten = false, bWritten = false, bReady = false; 261const aWrite = aw.write('a').then(() => {aWritten = true;}); 262const bWrite = bw.write('b').then(() => {bWritten = true;}); 263const aReady = aw.ready, bReadyPromise = bw.ready; 264if (aReady === bReadyPromise) throw Error('Shared backpressure promise'); 265bReadyPromise.then(() => {bReady = true;}); 266await new Promise(resolve => setTimeout(resolve)); 267if (aWritten || bWritten || bReady) throw Error('Missing initial backpressure'); 268if ((await ar.read()).value !== 'a') throw Error('First transform'); 269await aWrite; await aReady; 270if (!aWritten || bWritten || bReady) throw Error('Cross-stream backpressure'); 271if ((await br.read()).value !== 'b') throw Error('Second transform'); 272await bWrite; await bReadyPromise; 273await Promise.all([aw.close(), bw.close()]); 274if (!(await ar.read()).done || !(await br.read()).done) throw Error('Transform closure'); 275ar.releaseLock(); br.releaseLock(); aw.releaseLock(); bw.releaseLock(); 276console.log('backpressure'); 277"# , 278& [], 279); 280assert_eq! ( output. stdout , b"backpressure\n" ); 281} 282 283# [ test ] 284fn user_callback_thenables_are_assimilated_once_per_invocation () { 285let output =run ( 286r#" 287let accesses = 0, calls = 0; 288const writer = new WritableStream({ 289write(value) { 290calls++; 291return {get then() { 292accesses++; 293return resolve => queueMicrotask(resolve); 294}}; 295} 296}).getWriter(); 297await Promise.all([writer.write(1), writer.write(2), writer.write(3)]); 298await writer.close(); 299if (calls !== 3 || accesses !== 3) throw Error('Callback assimilation'); 300writer.releaseLock(); 301console.log('thenables'); 302"# , 303& [], 304); 305assert_eq! ( output. stdout , b"thenables\n" ); 306} 307 308# [ test ] 309fn internal_handling_does_not_suppress_unobserved_public_write_failures () { 310let ( _dir, child) = common:: spawn ( 311"new WritableStream({write() {throw Error('public write failure');}}).getWriter().write(1);" , 312); 313let output = common:: wait ( child); 314assert! ( !output. status . success ()); 315assert! ( String :: from_utf8_lossy ( & output. stderr ). contains ( "public write failure" )); 316} 317 318# [ test ] 319fn transferred_writable_ready_resolves_before_the_first_write () { 320let out =run ( 321r#" 322let value; 323const source=new WritableStream({write(chunk){value=chunk;}}); 324const moved=structuredClone(source,{transfer:[source]}); 325const writer=moved.getWriter(); 326await writer.ready; 327await writer.write('ready'); 328await writer.close(); 329if(value!=='ready')throw Error('Writable transfer readiness'); 330console.log('transfer ready'); 331"# , 332& [], 333); 334assert_eq! ( out. stdout , b"transfer ready\n" ); 335}