mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-17 05:09:24 +08:00
1525 lines
45 KiB
TypeScript
1525 lines
45 KiB
TypeScript
// Native AbortSignal + WHATWG readable-stream ownership coverage. The request body
|
|
// is produced in two turns, the response body is consumed through the
|
|
// default reader, and a timeout aborts a live native transfer.
|
|
function effectfulResponseMember(): "text" {
|
|
console.log("computed response key evaluated");
|
|
return "text";
|
|
}
|
|
|
|
async function callComputedResponseMember(response: Response): Promise<void> {
|
|
await response[effectfulResponseMember()]();
|
|
}
|
|
|
|
try {
|
|
await callComputedResponseMember(null as any);
|
|
} catch {
|
|
console.log("computed response receiver rejected");
|
|
}
|
|
|
|
function effectfulHeadersMember(): "get" {
|
|
console.log("computed headers key evaluated");
|
|
return "get";
|
|
}
|
|
|
|
function effectfulHeadersArgument(): string {
|
|
console.log("computed headers argument evaluated");
|
|
return "x-kind";
|
|
}
|
|
|
|
function callComputedHeadersMember(headers: Headers): void {
|
|
headers[effectfulHeadersMember()](effectfulHeadersArgument());
|
|
}
|
|
|
|
try {
|
|
callComputedHeadersMember(null as any);
|
|
} catch {
|
|
console.log("computed headers receiver rejected");
|
|
}
|
|
|
|
try {
|
|
new ReadableStream(null!);
|
|
console.log("null source unexpectedly accepted");
|
|
} catch (error) {
|
|
console.log("null source:", (error as Error).name);
|
|
}
|
|
|
|
let initialPullCalls = 0;
|
|
const initialPullStream = new ReadableStream<number>({
|
|
pull() {
|
|
initialPullCalls++;
|
|
},
|
|
});
|
|
console.log("initial pull sync:", initialPullCalls);
|
|
await Promise.resolve();
|
|
console.log("initial pull checkpoint:", initialPullCalls);
|
|
void initialPullStream;
|
|
|
|
const enqueuedIdentityBox = { value: 1 };
|
|
const enqueuedIdentityStream = new ReadableStream<typeof enqueuedIdentityBox>({
|
|
start(controller) {
|
|
controller.enqueue(enqueuedIdentityBox);
|
|
controller.close();
|
|
},
|
|
});
|
|
const enqueuedIdentityPart =
|
|
await enqueuedIdentityStream.getReader().read();
|
|
if (!enqueuedIdentityPart.done) enqueuedIdentityPart.value.value = 2;
|
|
console.log(
|
|
"controller enqueue record identity:",
|
|
enqueuedIdentityPart.done
|
|
? false
|
|
: enqueuedIdentityPart.value === enqueuedIdentityBox,
|
|
enqueuedIdentityBox.value,
|
|
);
|
|
|
|
type LiveUnionBox = { value: number };
|
|
type LiveUnionValue = LiveUnionBox | string;
|
|
|
|
async function checkEnqueuedUnionIdentity(
|
|
value: LiveUnionValue,
|
|
original: LiveUnionBox,
|
|
): Promise<void> {
|
|
const stream = new ReadableStream<LiveUnionValue>({
|
|
start(controller) {
|
|
controller.enqueue(value);
|
|
controller.close();
|
|
},
|
|
});
|
|
const part = await stream.getReader().read();
|
|
if (!part.done && typeof part.value !== "string") {
|
|
part.value.value = 3;
|
|
}
|
|
console.log(
|
|
"controller enqueue union identity:",
|
|
part.done ? false : part.value === original,
|
|
original.value,
|
|
);
|
|
}
|
|
|
|
const enqueuedUnionBox = { value: 1 };
|
|
await checkEnqueuedUnionIdentity(enqueuedUnionBox, enqueuedUnionBox);
|
|
|
|
const streamCancelState: { observed: LiveUnionValue } = { observed: "missing" };
|
|
const directCancelStream = new ReadableStream<LiveUnionValue>({
|
|
cancel(reason) {
|
|
streamCancelState.observed = reason as LiveUnionValue;
|
|
},
|
|
});
|
|
const directStreamCancelReason: LiveUnionBox = { value: 1 };
|
|
await directCancelStream.cancel(directStreamCancelReason);
|
|
const streamCancelObserved = streamCancelState.observed;
|
|
if (typeof streamCancelObserved !== "string") streamCancelObserved.value = 2;
|
|
console.log(
|
|
"stream cancel reason identity:",
|
|
streamCancelObserved === directStreamCancelReason,
|
|
directStreamCancelReason.value,
|
|
);
|
|
|
|
const readerCancelState: { observed: LiveUnionValue } = { observed: "missing" };
|
|
const directReaderCancelStream = new ReadableStream<LiveUnionValue>({
|
|
cancel(reason) {
|
|
readerCancelState.observed = reason as LiveUnionValue;
|
|
},
|
|
});
|
|
const directReader = directReaderCancelStream.getReader();
|
|
const directReaderCancelReason: LiveUnionBox = { value: 3 };
|
|
await directReader.cancel(directReaderCancelReason);
|
|
const readerCancelObserved = readerCancelState.observed;
|
|
if (typeof readerCancelObserved !== "string") readerCancelObserved.value = 4;
|
|
console.log(
|
|
"reader cancel reason identity:",
|
|
readerCancelObserved === directReaderCancelReason,
|
|
directReaderCancelReason.value,
|
|
);
|
|
|
|
async function checkComputedControllerIdentity(select: boolean): Promise<void> {
|
|
let controller!: ReadableStreamDefaultController<LiveUnionBox>;
|
|
const stream = new ReadableStream<LiveUnionBox>({
|
|
start(value) {
|
|
controller = value;
|
|
},
|
|
});
|
|
const box = { value: 1 };
|
|
const member: "enqueue" | "error" = select ? "enqueue" : "error";
|
|
controller[member](box);
|
|
box.value = 5;
|
|
controller.close();
|
|
const part = await stream.getReader().read();
|
|
console.log(
|
|
"computed controller identity:",
|
|
part.done ? false : part.value === box,
|
|
part.done ? -1 : part.value.value,
|
|
);
|
|
}
|
|
|
|
await checkComputedControllerIdentity(true);
|
|
|
|
function checkAbortUnionIdentity(
|
|
value: LiveUnionValue,
|
|
original: LiveUnionBox,
|
|
): void {
|
|
const reason = AbortSignal.abort(value).reason as LiveUnionValue;
|
|
if (typeof reason !== "string") reason.value = 4;
|
|
console.log(
|
|
"abort reason union identity:",
|
|
reason === original,
|
|
original.value,
|
|
);
|
|
}
|
|
|
|
const abortUnionBox = { value: 1 };
|
|
checkAbortUnionIdentity(abortUnionBox, abortUnionBox);
|
|
|
|
const enqueuedIdentityBytes = Buffer.from([1]);
|
|
const enqueuedBytesStream = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(enqueuedIdentityBytes);
|
|
controller.close();
|
|
},
|
|
});
|
|
const enqueuedBytesPart = await enqueuedBytesStream.getReader().read();
|
|
if (!enqueuedBytesPart.done) enqueuedBytesPart.value[0] = 2;
|
|
console.log(
|
|
"controller enqueue bytes identity:",
|
|
enqueuedBytesPart.done
|
|
? false
|
|
: enqueuedBytesPart.value === enqueuedIdentityBytes,
|
|
enqueuedIdentityBytes[0],
|
|
);
|
|
|
|
const undefinedOptionsStream: any = ReadableStream.from([3]);
|
|
const undefinedOptionsPart =
|
|
await undefinedOptionsStream.getReader(undefined).read();
|
|
const emptyOptionsStream: any = ReadableStream.from([4]);
|
|
const emptyOptionsPart = await emptyOptionsStream.getReader({}).read();
|
|
console.log(
|
|
"default reader options:",
|
|
undefinedOptionsPart.value,
|
|
emptyOptionsPart.value,
|
|
);
|
|
|
|
// Draining a pre-queued chunk creates demand even with no second read.
|
|
let replenishingPulls = 0;
|
|
const replenishingStream = new ReadableStream<number>({
|
|
start(controller) {
|
|
controller.enqueue(1);
|
|
},
|
|
pull(controller) {
|
|
replenishingPulls++;
|
|
controller.close();
|
|
},
|
|
});
|
|
const replenishingReader = replenishingStream.getReader();
|
|
await Promise.resolve();
|
|
const replenishedPart = await replenishingReader.read();
|
|
await Promise.resolve();
|
|
console.log(
|
|
"pull after queued read:",
|
|
replenishedPart.value,
|
|
replenishingPulls,
|
|
);
|
|
|
|
const requestBody = new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(Buffer.from("stream-"));
|
|
setTimeout(() => {
|
|
controller.enqueue(Buffer.from("body"));
|
|
controller.close();
|
|
}, 5);
|
|
},
|
|
});
|
|
|
|
const posted = await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
body: requestBody,
|
|
duplex: "half",
|
|
});
|
|
console.log(await posted.json());
|
|
console.log("consumed request locked:", requestBody.locked);
|
|
|
|
const arrayRequestBody: any = ["array", "body"];
|
|
const arrayPosted = await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
body: arrayRequestBody,
|
|
duplex: "half",
|
|
});
|
|
const arrayPostResult = (await arrayPosted.json()) as {
|
|
body: string;
|
|
contentType: string | null;
|
|
};
|
|
console.log(
|
|
"array request body:",
|
|
arrayPostResult.body,
|
|
arrayPostResult.contentType,
|
|
);
|
|
try {
|
|
requestBody.getReader();
|
|
console.log("consumed request reader unexpectedly acquired");
|
|
} catch (error) {
|
|
console.log("consumed request reader:", (error as Error).name);
|
|
}
|
|
|
|
const prelockedRequestBody = ReadableStream.from([
|
|
Buffer.from("prelocked request"),
|
|
]);
|
|
const prelockedRequestReader = prelockedRequestBody.getReader();
|
|
try {
|
|
await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
body: prelockedRequestBody,
|
|
duplex: "half",
|
|
});
|
|
console.log("prelocked request unexpectedly sent");
|
|
} catch (error) {
|
|
console.log("prelocked request:", (error as Error).name);
|
|
}
|
|
prelockedRequestReader.releaseLock();
|
|
|
|
// A promised pull stays serialized until that promise settles. The
|
|
// second read queues demand while the first pull is still awaiting.
|
|
let activePulls = 0;
|
|
let maxActivePulls = 0;
|
|
let pullCount = 0;
|
|
const promisedPulls = new ReadableStream<Uint8Array>({
|
|
async pull(controller) {
|
|
activePulls++;
|
|
maxActivePulls = Math.max(maxActivePulls, activePulls);
|
|
const n = ++pullCount;
|
|
controller.enqueue(Buffer.from([n]));
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 10));
|
|
activePulls--;
|
|
if (n === 2) controller.close();
|
|
},
|
|
});
|
|
const pullReader = promisedPulls.getReader();
|
|
await pullReader.read();
|
|
await pullReader.read();
|
|
await pullReader.closed;
|
|
console.log("max active pulls:", maxActivePulls);
|
|
|
|
let activeThenablePulls = 0;
|
|
let maxActiveThenablePulls = 0;
|
|
let thenablePullCount = 0;
|
|
const thenablePullSource = {
|
|
pull(controller: ReadableStreamDefaultController<number>) {
|
|
activeThenablePulls++;
|
|
maxActiveThenablePulls = Math.max(
|
|
maxActiveThenablePulls,
|
|
activeThenablePulls,
|
|
);
|
|
const n = ++thenablePullCount;
|
|
controller.enqueue(n);
|
|
return {
|
|
then(resolve: () => void) {
|
|
setTimeout(() => {
|
|
activeThenablePulls--;
|
|
if (n === 2) controller.close();
|
|
resolve();
|
|
}, 5);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
const thenablePulls = new ReadableStream<number>(thenablePullSource);
|
|
const thenablePullReader = thenablePulls.getReader();
|
|
async function readThenablePull(): Promise<ReadableStreamReadResult<number>> {
|
|
return await thenablePullReader.read();
|
|
}
|
|
const firstThenableRead = readThenablePull();
|
|
const secondThenableRead = readThenablePull();
|
|
await firstThenableRead;
|
|
await secondThenableRead;
|
|
await thenablePullReader.closed;
|
|
console.log("max active thenable pulls:", maxActiveThenablePulls);
|
|
|
|
let requestPull = 0;
|
|
const promisedRequestBody = new ReadableStream<Uint8Array>({
|
|
async pull(controller) {
|
|
requestPull++;
|
|
controller.enqueue(
|
|
Buffer.from(requestPull === 1 ? "promised-" : "request"),
|
|
);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
if (requestPull === 2) controller.close();
|
|
},
|
|
});
|
|
const promisedPost = await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
body: promisedRequestBody,
|
|
duplex: "half",
|
|
});
|
|
console.log(await promisedPost.json());
|
|
|
|
let abortedRequestPulls = 0;
|
|
let abortedRequestCancels = 0;
|
|
const abortedRequestBody = new ReadableStream<Uint8Array>({
|
|
async pull(controller) {
|
|
const call = ++abortedRequestPulls;
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 30));
|
|
controller.enqueue(Buffer.from("late"));
|
|
if (call === 2) controller.close();
|
|
},
|
|
cancel() {
|
|
abortedRequestCancels++;
|
|
},
|
|
});
|
|
try {
|
|
await fetch(`${process.argv[2]}/slow`, {
|
|
method: "POST",
|
|
body: abortedRequestBody,
|
|
duplex: "half",
|
|
signal: AbortSignal.timeout(5),
|
|
});
|
|
} catch (error) {
|
|
console.log("aborted request:", (error as Error).name);
|
|
}
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 70));
|
|
console.log(
|
|
"aborted request source:",
|
|
abortedRequestPulls,
|
|
abortedRequestCancels,
|
|
abortedRequestBody.locked,
|
|
);
|
|
|
|
const temporaryRead = await new ReadableStream<Uint8Array>({
|
|
start(controller) {
|
|
controller.enqueue(Buffer.from("temporary"));
|
|
controller.close();
|
|
},
|
|
}).getReader().read();
|
|
console.log(
|
|
"temporary reader:",
|
|
temporaryRead.done ? "done" : new TextDecoder().decode(temporaryRead.value),
|
|
);
|
|
|
|
let concurrentValue = 0;
|
|
const concurrentReader = new ReadableStream<number>({
|
|
async pull(controller) {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
concurrentValue++;
|
|
controller.enqueue(concurrentValue);
|
|
if (concurrentValue === 2) controller.close();
|
|
},
|
|
}).getReader();
|
|
async function readConcurrent(): Promise<ReadableStreamReadResult<number>> {
|
|
return await concurrentReader.read();
|
|
}
|
|
const concurrentFirstPromise = readConcurrent();
|
|
const concurrentSecondPromise = readConcurrent();
|
|
const concurrentFirst = await concurrentFirstPromise;
|
|
const concurrentSecond = await concurrentSecondPromise;
|
|
console.log(
|
|
"concurrent reads:",
|
|
concurrentFirst.value,
|
|
concurrentSecond.value,
|
|
);
|
|
|
|
const releasedReader = new ReadableStream<number>({
|
|
async start() {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
},
|
|
}).getReader();
|
|
const releasedRead: unknown = releasedReader.read();
|
|
const oldReleasedClosed: unknown = releasedReader.closed;
|
|
releasedReader.releaseLock();
|
|
const newReleasedClosed: unknown = releasedReader.closed;
|
|
try {
|
|
await oldReleasedClosed;
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("released old closed:", caught.name, caught.message);
|
|
}
|
|
try {
|
|
await releasedRead;
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("released read:", caught.name, caught.message);
|
|
}
|
|
try {
|
|
await newReleasedClosed;
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("released new closed:", caught.name, caught.message);
|
|
}
|
|
const releasedCancel: unknown = releasedReader.cancel();
|
|
console.log("released cancel returned");
|
|
try {
|
|
await releasedCancel;
|
|
} catch (error) {
|
|
console.log("released cancel rejected:", (error as Error).name);
|
|
}
|
|
|
|
const liveValues = [1];
|
|
const liveValuesReader = ReadableStream.from(liveValues).getReader();
|
|
liveValues[0] = 2;
|
|
liveValues.push(3);
|
|
const liveFirst = await liveValuesReader.read();
|
|
const liveSecond = await liveValuesReader.read();
|
|
const liveDone = await liveValuesReader.read();
|
|
console.log(
|
|
"stream from live array:",
|
|
liveFirst.value,
|
|
liveSecond.value,
|
|
liveDone.done,
|
|
);
|
|
|
|
const tupleValues = [6, 7] as const;
|
|
const tupleReader = ReadableStream.from(tupleValues).getReader();
|
|
const tupleFirst = await tupleReader.read();
|
|
const tupleSecond = await tupleReader.read();
|
|
const tupleDone = await tupleReader.read();
|
|
console.log(
|
|
"stream from readonly tuple:",
|
|
tupleFirst.value,
|
|
tupleSecond.value,
|
|
tupleDone.done,
|
|
);
|
|
|
|
const streamIdentityBox = { value: 1 };
|
|
const streamIdentityReader =
|
|
ReadableStream.from([streamIdentityBox]).getReader();
|
|
const streamIdentityPart = await streamIdentityReader.read();
|
|
if (!streamIdentityPart.done) {
|
|
streamIdentityPart.value.value = 2;
|
|
}
|
|
console.log(
|
|
"stream from record identity:",
|
|
streamIdentityPart.done,
|
|
streamIdentityPart.done
|
|
? false
|
|
: streamIdentityPart.value === streamIdentityBox,
|
|
streamIdentityBox.value,
|
|
);
|
|
|
|
const bracketReader = ReadableStream.from(["bracket"]).getReader();
|
|
const bracketPart = await bracketReader["read"]();
|
|
console.log("bracket reader read:", bracketPart.done, bracketPart.value);
|
|
|
|
const surplusReader: any = ReadableStream.from([11]).getReader();
|
|
console.log(
|
|
"reader surplus argument:",
|
|
JSON.stringify(await surplusReader.read("ignored")),
|
|
);
|
|
const surplusSignal: any = AbortSignal.any([]);
|
|
console.log(
|
|
"throwIfAborted surplus argument:",
|
|
surplusSignal.throwIfAborted("ignored"),
|
|
);
|
|
|
|
interface StreamUnionBox {
|
|
value: number;
|
|
}
|
|
|
|
const streamUnionBox: StreamUnionBox = { value: 9 };
|
|
const streamUnionValues: Array<StreamUnionBox | null> = [streamUnionBox];
|
|
const streamUnionPart =
|
|
await ReadableStream.from(streamUnionValues).getReader().read();
|
|
if (streamUnionPart.done) {
|
|
console.log("stream from union identity:", true, false, -1);
|
|
} else {
|
|
console.log(
|
|
"stream from union identity:",
|
|
false,
|
|
streamUnionPart.value === streamUnionBox,
|
|
streamUnionPart.value?.value ?? -1,
|
|
);
|
|
}
|
|
|
|
const dynamicStreamReader: any =
|
|
ReadableStream.from([{ value: 10 }]).getReader();
|
|
const dynamicStreamPart: any = await dynamicStreamReader.read();
|
|
console.log(
|
|
"dynamic stream result:",
|
|
String(dynamicStreamPart.value),
|
|
JSON.stringify(dynamicStreamPart),
|
|
);
|
|
console.log(
|
|
"dynamic stream record predicates:",
|
|
typeof dynamicStreamPart.value === "object",
|
|
!!dynamicStreamPart.value,
|
|
);
|
|
|
|
const dynamicPredicateArrayReader: any =
|
|
ReadableStream.from([[1]]).getReader();
|
|
const dynamicPredicateArrayPart: any =
|
|
await dynamicPredicateArrayReader.read();
|
|
console.log(
|
|
"dynamic stream array predicates:",
|
|
typeof dynamicPredicateArrayPart.value === "object",
|
|
!!dynamicPredicateArrayPart.value,
|
|
Array.isArray(dynamicPredicateArrayPart.value),
|
|
);
|
|
|
|
const widenedRecordStream: ReadableStream<unknown> =
|
|
ReadableStream.from([{ value: 7 }]);
|
|
const widenedRecordPart = await widenedRecordStream.getReader().read();
|
|
console.log(
|
|
"stream from widened record:",
|
|
widenedRecordPart.done,
|
|
widenedRecordPart.done
|
|
? "done"
|
|
: JSON.stringify(widenedRecordPart.value),
|
|
);
|
|
|
|
interface StreamBaseValue {
|
|
value: number;
|
|
}
|
|
|
|
interface StreamDerivedValue extends StreamBaseValue {
|
|
extra: number;
|
|
}
|
|
|
|
const structurallyDerived: StreamDerivedValue = { value: 8, extra: 9 };
|
|
const structurallyWidenedStream: ReadableStream<StreamBaseValue> =
|
|
ReadableStream.from([structurallyDerived]);
|
|
const structurallyWidenedPart =
|
|
await structurallyWidenedStream.getReader().read();
|
|
console.log(
|
|
"stream from structurally widened record:",
|
|
structurallyWidenedPart.done,
|
|
structurallyWidenedPart.done ? "done" : structurallyWidenedPart.value.value,
|
|
);
|
|
|
|
const repeatedStructuralValue: StreamDerivedValue = { value: 12, extra: 13 };
|
|
const repeatedStructuralReader =
|
|
(ReadableStream.from([
|
|
repeatedStructuralValue,
|
|
repeatedStructuralValue,
|
|
]) as ReadableStream<StreamBaseValue>).getReader();
|
|
const repeatedStructuralFirst = await repeatedStructuralReader.read();
|
|
repeatedStructuralValue.value = 19;
|
|
const repeatedStructuralSecond = await repeatedStructuralReader.read();
|
|
console.log(
|
|
"stream widened repeated identity:",
|
|
repeatedStructuralFirst.done || repeatedStructuralSecond.done
|
|
? false
|
|
: repeatedStructuralFirst.value === repeatedStructuralSecond.value,
|
|
repeatedStructuralSecond.done ? -1 : repeatedStructuralSecond.value.value,
|
|
);
|
|
|
|
const nestedStructuralValue: StreamDerivedValue[] = [
|
|
{ value: 25, extra: 26 },
|
|
];
|
|
const nestedStructuralReader =
|
|
(ReadableStream.from([
|
|
nestedStructuralValue,
|
|
nestedStructuralValue,
|
|
]) as ReadableStream<StreamBaseValue[]>).getReader();
|
|
const nestedStructuralFirst = await nestedStructuralReader.read();
|
|
nestedStructuralValue[0]!.value = 27;
|
|
const nestedStructuralSecond = await nestedStructuralReader.read();
|
|
console.log(
|
|
"stream nested array widening:",
|
|
nestedStructuralFirst.done || nestedStructuralSecond.done
|
|
? false
|
|
: nestedStructuralFirst.value === nestedStructuralSecond.value,
|
|
nestedStructuralSecond.done
|
|
? -1
|
|
: nestedStructuralSecond.value[0]!.value,
|
|
);
|
|
|
|
const repeatedDynamicValue = { value: 14 };
|
|
const repeatedDynamicReader: any =
|
|
ReadableStream.from([
|
|
repeatedDynamicValue,
|
|
repeatedDynamicValue,
|
|
]).getReader();
|
|
const repeatedDynamicFirst: any = await repeatedDynamicReader.read();
|
|
const repeatedDynamicSecond: any = await repeatedDynamicReader.read();
|
|
repeatedDynamicFirst.value.value = 15;
|
|
console.log(
|
|
"dynamic stream repeated identity:",
|
|
repeatedDynamicFirst.value === repeatedDynamicSecond.value,
|
|
repeatedDynamicSecond.value.value,
|
|
);
|
|
|
|
const liveDynamicValue = { value: 16 };
|
|
const liveDynamicReader: any = ReadableStream.from([
|
|
liveDynamicValue,
|
|
liveDynamicValue,
|
|
]).getReader();
|
|
const liveDynamicFirst: any = await liveDynamicReader.read();
|
|
console.log("dynamic stream live first:", JSON.stringify(liveDynamicFirst.value));
|
|
liveDynamicValue.value = 17;
|
|
const liveDynamicSecond: any = await liveDynamicReader.read();
|
|
console.log("dynamic stream live refresh:", JSON.stringify(liveDynamicSecond.value));
|
|
liveDynamicSecond.value.value = 18;
|
|
console.log("dynamic stream live commit:", liveDynamicValue.value);
|
|
|
|
const dynamicArrayValue = [21];
|
|
const dynamicArrayReader: any = ReadableStream.from([
|
|
dynamicArrayValue,
|
|
dynamicArrayValue,
|
|
]).getReader();
|
|
const dynamicArrayFirst: any = await dynamicArrayReader.read();
|
|
dynamicArrayFirst.value[0] = 22;
|
|
const dynamicArraySecond: any = await dynamicArrayReader.read();
|
|
console.log(
|
|
"dynamic stream array commit:",
|
|
dynamicArrayValue[0],
|
|
dynamicArraySecond.value[0],
|
|
dynamicArrayFirst.value === dynamicArraySecond.value,
|
|
);
|
|
|
|
const dynamicArrayMethodValue = [31];
|
|
const dynamicArrayMethodReader: any = ReadableStream.from([
|
|
dynamicArrayMethodValue,
|
|
]).getReader();
|
|
const dynamicArrayMethodPart: any = await dynamicArrayMethodReader.read();
|
|
const dynamicArrayMethodResult = dynamicArrayMethodPart.value.push(32);
|
|
console.log(
|
|
"dynamic stream array method:",
|
|
dynamicArrayMethodResult,
|
|
dynamicArrayMethodValue.join(","),
|
|
);
|
|
|
|
const dynamicArrayCallbackValue = [33];
|
|
const dynamicArrayCallbackReader: any = ReadableStream.from([
|
|
dynamicArrayCallbackValue,
|
|
]).getReader();
|
|
const dynamicArrayCallbackPart: any = await dynamicArrayCallbackReader.read();
|
|
let dynamicArrayCallbackCalls = 0;
|
|
dynamicArrayCallbackPart.value.forEach(
|
|
(_value: number, _index: number, array: number[]) => {
|
|
dynamicArrayCallbackCalls++;
|
|
array.push(34);
|
|
},
|
|
);
|
|
console.log(
|
|
"dynamic stream array callback:",
|
|
dynamicArrayCallbackCalls,
|
|
dynamicArrayCallbackValue.join(","),
|
|
);
|
|
|
|
const dynamicNestedValue = { nested: { value: 23 } };
|
|
const dynamicNestedReader: any = ReadableStream.from([
|
|
dynamicNestedValue,
|
|
dynamicNestedValue,
|
|
]).getReader();
|
|
const dynamicNestedFirst: any = await dynamicNestedReader.read();
|
|
const retainedDynamicNested: any = dynamicNestedFirst.value.nested;
|
|
retainedDynamicNested.value = 24;
|
|
const dynamicNestedSecond: any = await dynamicNestedReader.read();
|
|
console.log(
|
|
"dynamic stream nested commit:",
|
|
dynamicNestedValue.nested.value,
|
|
dynamicNestedSecond.value.nested.value,
|
|
retainedDynamicNested === dynamicNestedSecond.value.nested,
|
|
);
|
|
|
|
const widenedStringStream: ReadableStream<unknown> = ReadableStream.from([
|
|
"same",
|
|
["sa", "me"].join(""),
|
|
]);
|
|
const widenedStringReader = widenedStringStream.getReader();
|
|
const widenedStringFirst = await widenedStringReader.read();
|
|
const widenedStringSecond = await widenedStringReader.read();
|
|
console.log(
|
|
"stream widened string primitives:",
|
|
widenedStringFirst.done ? "done" : typeof widenedStringFirst.value,
|
|
widenedStringFirst.done || widenedStringSecond.done
|
|
? false
|
|
: widenedStringFirst.value === widenedStringSecond.value,
|
|
);
|
|
|
|
const unionStringValues: Array<string | null> = [
|
|
"same",
|
|
["sa", "me"].join(""),
|
|
];
|
|
const widenedUnionStringStream: ReadableStream<unknown> =
|
|
ReadableStream.from(unionStringValues);
|
|
const widenedUnionStringReader = widenedUnionStringStream.getReader();
|
|
const widenedUnionStringFirst = await widenedUnionStringReader.read();
|
|
const widenedUnionStringSecond = await widenedUnionStringReader.read();
|
|
console.log(
|
|
"stream widened union string primitives:",
|
|
widenedUnionStringFirst.done
|
|
? "done"
|
|
: typeof widenedUnionStringFirst.value,
|
|
widenedUnionStringFirst.done || widenedUnionStringSecond.done
|
|
? false
|
|
: widenedUnionStringFirst.value === widenedUnionStringSecond.value,
|
|
);
|
|
|
|
const liveBytes = new Uint8Array([4]);
|
|
const liveBytesReader = ReadableStream.from(liveBytes).getReader();
|
|
liveBytes[0] = 5;
|
|
const liveByte = await liveBytesReader.read();
|
|
console.log("stream from live bytes:", liveByte.value);
|
|
|
|
const stringReader = ReadableStream.from("😀a").getReader();
|
|
const stringFirst = await stringReader.read();
|
|
const stringSecond = await stringReader.read();
|
|
const stringDone = await stringReader.read();
|
|
console.log(
|
|
"stream from string:",
|
|
stringFirst.value,
|
|
stringSecond.value,
|
|
stringDone.done,
|
|
);
|
|
|
|
const streamed = await fetch(`${process.argv[2]}/chunked`);
|
|
const reader = streamed.body!.getReader();
|
|
const chunks: Uint8Array[] = [];
|
|
for (;;) {
|
|
const part = await reader.read();
|
|
if (part.done) break;
|
|
chunks.push(part.value);
|
|
}
|
|
console.log(new TextDecoder().decode(Buffer.concat(chunks)), streamed.bodyUsed);
|
|
|
|
const lockedCancelResponse = await fetch(`${process.argv[2]}/chunked`);
|
|
const lockedCancelBody = lockedCancelResponse.body!;
|
|
async function collectLockedCancelResponse(): Promise<string> {
|
|
return await lockedCancelResponse.text();
|
|
}
|
|
const lockedCancelText = collectLockedCancelResponse();
|
|
try {
|
|
await lockedCancelBody.cancel();
|
|
console.log("locked response cancel unexpectedly resolved");
|
|
} catch (error) {
|
|
console.log("locked response cancel:", (error as Error).name);
|
|
}
|
|
console.log("locked response text:", await lockedCancelText);
|
|
|
|
const closedCancelResponse = await fetch(
|
|
`${process.argv[2]}/headers-source`,
|
|
);
|
|
const closedCancelBefore = closedCancelResponse.bodyUsed;
|
|
await closedCancelResponse.body!.cancel();
|
|
console.log(
|
|
"closed response cancel:",
|
|
closedCancelBefore,
|
|
closedCancelResponse.bodyUsed,
|
|
);
|
|
|
|
const collected = await fetch(`${process.argv[2]}/text`);
|
|
await collected.text();
|
|
console.log("collected response locked:", collected.body!.locked);
|
|
try {
|
|
collected.body!.getReader();
|
|
console.log("collected response reader unexpectedly acquired");
|
|
} catch (error) {
|
|
console.log("collected response reader:", (error as Error).name);
|
|
}
|
|
|
|
const pressureKey = process.argv[3] ?? "static-stream";
|
|
const pressured = await fetch(
|
|
`${process.argv[2]}/backpressure?key=${pressureKey}`,
|
|
);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 250));
|
|
const pressureState = await (
|
|
await fetch(`${process.argv[2]}/backpressure-state?key=${pressureKey}`)
|
|
).text();
|
|
console.log("response backpressure:", pressureState);
|
|
await pressured.body!.cancel();
|
|
|
|
const gzipPressureBefore = process.resourceUsage().maxRSS;
|
|
const gzipPressured = await fetch(`${process.argv[2]}/gzip-pressure`);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 250));
|
|
const gzipPressureGrowth =
|
|
process.resourceUsage().maxRSS - gzipPressureBefore;
|
|
console.log(
|
|
"compressed response backpressure:",
|
|
gzipPressureGrowth < 32 * 1024,
|
|
);
|
|
await gzipPressured.body!.cancel();
|
|
|
|
const signal = AbortSignal.any([AbortSignal.timeout(20)]);
|
|
AbortSignal.abort().addEventListener("custom", () => {
|
|
console.log("custom abort event unexpectedly fired");
|
|
});
|
|
console.log("custom abort listener registered");
|
|
let abortEvent = false;
|
|
signal.addEventListener("abort", () => {
|
|
abortEvent = true;
|
|
console.log("abort-first");
|
|
}, { once: true });
|
|
signal.addEventListener("abort", () => {
|
|
console.log("abort-second");
|
|
}, { once: true });
|
|
try {
|
|
await fetch(`${process.argv[2]}/slow`, { signal });
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log(abortEvent, signal.aborted, caught.name, caught.message);
|
|
}
|
|
|
|
try {
|
|
AbortSignal.abort(new Error("manual stop")).throwIfAborted();
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log(caught.name, caught.message);
|
|
}
|
|
|
|
const abortIdentityReason = { value: 1 };
|
|
const abortIdentitySignal = AbortSignal.abort(abortIdentityReason);
|
|
const observedAbortReason = abortIdentitySignal.reason as { value: number };
|
|
observedAbortReason.value = 2;
|
|
console.log(
|
|
"abort reason identity:",
|
|
observedAbortReason === abortIdentityReason,
|
|
abortIdentityReason.value,
|
|
);
|
|
|
|
const abortIdentityBytes = Buffer.from([5]);
|
|
const observedAbortBytes =
|
|
AbortSignal.abort(abortIdentityBytes).reason as Uint8Array;
|
|
observedAbortBytes[0] = 6;
|
|
console.log(
|
|
"abort reason bytes identity:",
|
|
observedAbortBytes === abortIdentityBytes,
|
|
abortIdentityBytes[0],
|
|
);
|
|
|
|
const identitySignal = AbortSignal.timeout(0);
|
|
let identityCalls = 0;
|
|
const identityListener = () => {
|
|
identityCalls++;
|
|
};
|
|
identitySignal.addEventListener("abort", identityListener);
|
|
identitySignal.addEventListener("abort", identityListener);
|
|
identitySignal.removeEventListener("abort", identityListener);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("removed abort listener:", identityCalls);
|
|
|
|
function addComputedAbortListener(
|
|
target: AbortSignal,
|
|
listener: { handleEvent(event: Event): void },
|
|
select: boolean,
|
|
): void {
|
|
const member: "addEventListener" | "removeEventListener" = select
|
|
? "addEventListener"
|
|
: "removeEventListener";
|
|
target[member]("abort", listener);
|
|
}
|
|
|
|
const computedIdentitySignal = AbortSignal.timeout(0);
|
|
let computedIdentityCalls = 0;
|
|
const computedIdentityListener = {
|
|
handleEvent(_event: Event) {
|
|
computedIdentityCalls++;
|
|
},
|
|
};
|
|
addComputedAbortListener(
|
|
computedIdentitySignal,
|
|
computedIdentityListener,
|
|
true,
|
|
);
|
|
computedIdentitySignal.removeEventListener("abort", computedIdentityListener);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("computed removed abort listener:", computedIdentityCalls);
|
|
|
|
const mutationSignal = AbortSignal.timeout(0);
|
|
let mutationCalls = 0;
|
|
const selfRemovingListener = () => {
|
|
mutationCalls++;
|
|
mutationSignal.removeEventListener("abort", selfRemovingListener);
|
|
};
|
|
mutationSignal.addEventListener("abort", selfRemovingListener);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("self-removing abort listener:", mutationCalls);
|
|
|
|
const dispatchSignal = AbortSignal.timeout(0);
|
|
let dispatchEvent!: Event;
|
|
let dispatchCalls = 0;
|
|
dispatchSignal.addEventListener("abort", (event: Event) => {
|
|
dispatchEvent = event;
|
|
dispatchCalls++;
|
|
});
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log(
|
|
"manual abort dispatch:",
|
|
dispatchSignal.dispatchEvent(dispatchEvent),
|
|
dispatchCalls,
|
|
);
|
|
|
|
const truthyOptionsSignal = AbortSignal.timeout(0);
|
|
let truthyOptionsEvent!: Event;
|
|
let truthyOptionsCalls = 0;
|
|
const truthyOptionsListener = (event: Event) => {
|
|
truthyOptionsEvent = event;
|
|
truthyOptionsCalls++;
|
|
};
|
|
const truthyListenerOptions = JSON.parse('{"capture":1,"once":1}');
|
|
truthyOptionsSignal.addEventListener(
|
|
"abort",
|
|
truthyOptionsListener,
|
|
truthyListenerOptions,
|
|
);
|
|
truthyOptionsSignal.addEventListener("abort", truthyOptionsListener, true);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
truthyOptionsSignal.dispatchEvent(truthyOptionsEvent);
|
|
console.log("truthy abort listener options:", truthyOptionsCalls);
|
|
|
|
const stoppedDispatchSignal = AbortSignal.timeout(0);
|
|
let stoppedDispatchEvent!: Event;
|
|
const stoppedDispatchCalls: string[] = [];
|
|
stoppedDispatchSignal.addEventListener(
|
|
"abort",
|
|
(event: Event) => {
|
|
stoppedDispatchEvent = event;
|
|
stoppedDispatchCalls.push("first");
|
|
event.stopImmediatePropagation();
|
|
},
|
|
{ once: true },
|
|
);
|
|
stoppedDispatchSignal.addEventListener("abort", () => {
|
|
stoppedDispatchCalls.push("second");
|
|
});
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
stoppedDispatchSignal.dispatchEvent(stoppedDispatchEvent);
|
|
console.log("stopped abort redispatch:", stoppedDispatchCalls.join(","));
|
|
|
|
const eventSignal = AbortSignal.timeout(0);
|
|
eventSignal.addEventListener("abort", (event: Event) => {
|
|
console.log(
|
|
"abort event:",
|
|
event.type,
|
|
event.target === eventSignal,
|
|
event.currentTarget === eventSignal,
|
|
event.srcElement === eventSignal,
|
|
event.bubbles,
|
|
event.cancelable,
|
|
event.composed,
|
|
event.defaultPrevented,
|
|
event.eventPhase,
|
|
event.isTrusted,
|
|
event.timeStamp >= 0,
|
|
event.cancelBubble,
|
|
event.returnValue,
|
|
event.composedPath().length,
|
|
);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
console.log(
|
|
"abort event propagation:",
|
|
event.defaultPrevented,
|
|
event.cancelBubble,
|
|
event.returnValue,
|
|
);
|
|
setTimeout(() => {
|
|
console.log(
|
|
"abort event after dispatch:",
|
|
event.target === eventSignal,
|
|
event.currentTarget === null,
|
|
event.srcElement === eventSignal,
|
|
event.eventPhase,
|
|
event.cancelBubble,
|
|
event.composedPath().length,
|
|
);
|
|
}, 0);
|
|
});
|
|
eventSignal.addEventListener("abort", () => {
|
|
console.log("abort listener after stopPropagation");
|
|
});
|
|
eventSignal.addEventListener("abort", null);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
|
|
const immediateSignal = AbortSignal.timeout(0);
|
|
const immediateHandlers: string[] = [];
|
|
immediateSignal.addEventListener("abort", (event: Event) => {
|
|
immediateHandlers.push("first");
|
|
event.stopImmediatePropagation();
|
|
});
|
|
immediateSignal.addEventListener("abort", () => {
|
|
immediateHandlers.push("second");
|
|
});
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("abort stop immediate:", immediateHandlers.join(","));
|
|
|
|
const captureSignal = AbortSignal.timeout(0);
|
|
let captureCalls = 0;
|
|
const captureListener = () => {
|
|
captureCalls++;
|
|
};
|
|
captureSignal.addEventListener("abort", captureListener, false);
|
|
captureSignal.addEventListener("abort", captureListener, true);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("capture listener identity:", captureCalls);
|
|
|
|
const objectSignal = AbortSignal.timeout(0);
|
|
let objectCalls = 0;
|
|
const objectListener = {
|
|
handleEvent(event: Event) {
|
|
if (event.type === "abort") objectCalls++;
|
|
},
|
|
};
|
|
objectSignal.addEventListener("abort", objectListener);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("object abort listener:", objectCalls);
|
|
|
|
const updatedObjectSignal = AbortSignal.timeout(0);
|
|
let updatedObjectCalls = "";
|
|
const updatedObjectListener = {
|
|
handleEvent(_event: Event) {
|
|
updatedObjectCalls += "old";
|
|
},
|
|
};
|
|
updatedObjectSignal.addEventListener("abort", updatedObjectListener);
|
|
updatedObjectListener.handleEvent = (_event: Event) => {
|
|
updatedObjectCalls += "new";
|
|
};
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("updated object abort listener:", updatedObjectCalls);
|
|
|
|
const removedObjectSignal = AbortSignal.timeout(0);
|
|
let removedObjectCalls = 0;
|
|
const removedObjectListener = {
|
|
handleEvent() {
|
|
removedObjectCalls++;
|
|
},
|
|
};
|
|
removedObjectSignal.addEventListener("abort", removedObjectListener);
|
|
removedObjectSignal.addEventListener("abort", removedObjectListener);
|
|
removedObjectSignal.removeEventListener("abort", removedObjectListener);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("removed object abort listener:", removedObjectCalls);
|
|
|
|
const distinctObjectSignal = AbortSignal.timeout(0);
|
|
let distinctObjectCalls = 0;
|
|
const sharedObjectHandler = () => {
|
|
distinctObjectCalls++;
|
|
};
|
|
const firstObjectListener = { handleEvent: sharedObjectHandler };
|
|
const secondObjectListener = { handleEvent: sharedObjectHandler };
|
|
distinctObjectSignal.addEventListener("abort", firstObjectListener);
|
|
distinctObjectSignal.addEventListener("abort", secondObjectListener);
|
|
distinctObjectSignal.removeEventListener("abort", firstObjectListener);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("distinct object abort listener:", distinctObjectCalls);
|
|
|
|
const orderedSignal = AbortSignal.timeout(0);
|
|
const orderedHandlers: string[] = [];
|
|
orderedSignal.addEventListener("abort", () => {
|
|
orderedHandlers.push("listener");
|
|
});
|
|
orderedSignal.onabort = () => {
|
|
orderedHandlers.push("onabort");
|
|
};
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("abort handler order:", orderedHandlers.join(","));
|
|
|
|
const noncallableOnabortSignal: any = AbortSignal.timeout(0);
|
|
noncallableOnabortSignal.onabort = 42;
|
|
console.log("noncallable onabort value:", noncallableOnabortSignal.onabort);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("noncallable onabort ignored:", noncallableOnabortSignal.aborted);
|
|
|
|
const listenerGate = AbortSignal.timeout(0);
|
|
const gatedTarget = AbortSignal.timeout(10);
|
|
let gatedCalls = 0;
|
|
gatedTarget.addEventListener(
|
|
"abort",
|
|
() => {
|
|
gatedCalls++;
|
|
},
|
|
{ signal: listenerGate },
|
|
);
|
|
const preAbortedTarget = AbortSignal.timeout(0);
|
|
preAbortedTarget.addEventListener(
|
|
"abort",
|
|
() => {
|
|
gatedCalls++;
|
|
},
|
|
{ signal: AbortSignal.abort() },
|
|
);
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 20));
|
|
console.log("abort listener signal:", gatedCalls);
|
|
|
|
for (const delay of [-1, Number.NaN, Number.POSITIVE_INFINITY, 4294967296]) {
|
|
try {
|
|
AbortSignal.timeout(delay);
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("invalid timeout:", caught.name, caught.message);
|
|
}
|
|
}
|
|
|
|
try {
|
|
const missingDuplex = ReadableStream.from([Buffer.from("no-duplex")]);
|
|
await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
body: missingDuplex,
|
|
});
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("missing duplex:", caught.name, caught.message);
|
|
}
|
|
|
|
let startReady = false;
|
|
const asyncStart = new ReadableStream<Uint8Array>({
|
|
async start() {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
startReady = true;
|
|
},
|
|
pull(controller) {
|
|
console.log("pull after start:", startReady);
|
|
controller.close();
|
|
},
|
|
});
|
|
await asyncStart.getReader().read();
|
|
|
|
let releaseQueuedStart = (): void => {};
|
|
const queuedDuringStart = new ReadableStream<string>({
|
|
start(controller) {
|
|
controller.enqueue("queued");
|
|
return new Promise<void>((resolve) => {
|
|
releaseQueuedStart = resolve;
|
|
});
|
|
},
|
|
});
|
|
let queuedStartObserved = "pending";
|
|
const queuedStartRead = queuedDuringStart.getReader().read();
|
|
void queuedStartRead.then((part) => {
|
|
queuedStartObserved = part.done ? "done" : `read:${part.value}`;
|
|
});
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
console.log("queued during pending start:", queuedStartObserved);
|
|
releaseQueuedStart();
|
|
await queuedStartRead;
|
|
|
|
let thenableStartReady = false;
|
|
const thenableStart = new ReadableStream<Uint8Array>({
|
|
start() {
|
|
return {
|
|
then(resolve: () => void) {
|
|
setTimeout(() => {
|
|
thenableStartReady = true;
|
|
resolve();
|
|
}, 5);
|
|
},
|
|
};
|
|
},
|
|
pull(controller) {
|
|
console.log("pull after thenable start:", thenableStartReady);
|
|
controller.close();
|
|
},
|
|
});
|
|
await thenableStart.getReader().read();
|
|
|
|
let cancelFinished = false;
|
|
const asyncCancel = new ReadableStream<Uint8Array>({
|
|
async cancel() {
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
cancelFinished = true;
|
|
},
|
|
});
|
|
await asyncCancel.cancel();
|
|
console.log("cancel awaited:", cancelFinished);
|
|
|
|
let thenableCancelFinished = false;
|
|
const thenableCancelSource = {
|
|
cancel() {
|
|
return {
|
|
then(resolve: () => void) {
|
|
setTimeout(() => {
|
|
thenableCancelFinished = true;
|
|
resolve();
|
|
}, 5);
|
|
},
|
|
};
|
|
},
|
|
};
|
|
const thenableCancel = new ReadableStream<Uint8Array>(thenableCancelSource);
|
|
await thenableCancel.cancel();
|
|
console.log("thenable cancel awaited:", thenableCancelFinished);
|
|
|
|
const queued = ReadableStream.from([
|
|
Buffer.from("one"),
|
|
Buffer.from("two"),
|
|
]);
|
|
const queuedReader = queued.getReader();
|
|
let queuedClosed = false;
|
|
async function watchQueuedClose(): Promise<void> {
|
|
await queuedReader.closed;
|
|
queuedClosed = true;
|
|
}
|
|
void watchQueuedClose();
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
console.log("closed before drain:", queuedClosed);
|
|
await queuedReader.read();
|
|
await queuedReader.read();
|
|
await queuedReader.read();
|
|
await queuedReader.closed;
|
|
console.log("closed after drain:", queuedClosed);
|
|
|
|
// A pull that schedules its enqueue for later must not be re-entered merely
|
|
// because a reader is waiting. Node makes one follow-up pull after the
|
|
// delayed enqueue drains into that reader.
|
|
let delayedPullCount = 0;
|
|
const delayedReader = new ReadableStream<number>({
|
|
pull(controller) {
|
|
const call = ++delayedPullCount;
|
|
if (call === 1) {
|
|
setTimeout(() => {
|
|
controller.enqueue(7);
|
|
controller.close();
|
|
}, 5);
|
|
} else if (call === 3) {
|
|
controller.error(new Error("pull re-entered before enqueue"));
|
|
}
|
|
},
|
|
}).getReader();
|
|
const delayedRead = await delayedReader.read();
|
|
console.log("delayed pull:", delayedRead.value, delayedPullCount);
|
|
|
|
let closedCancelCalls = 0;
|
|
const alreadyClosed = new ReadableStream<number>({
|
|
start(controller) {
|
|
controller.close();
|
|
},
|
|
cancel() {
|
|
closedCancelCalls++;
|
|
},
|
|
});
|
|
await alreadyClosed.cancel();
|
|
console.log("closed cancel:", closedCancelCalls);
|
|
|
|
let cancelCloseController!: ReadableStreamDefaultController<number>;
|
|
let cancelCloseCalls = 0;
|
|
const cancelCloseRequested = new ReadableStream<number>({
|
|
start(controller) {
|
|
cancelCloseController = controller;
|
|
controller.enqueue(1);
|
|
controller.close();
|
|
},
|
|
cancel() {
|
|
cancelCloseCalls++;
|
|
},
|
|
});
|
|
await cancelCloseRequested.cancel();
|
|
const cancelCloseReader = cancelCloseRequested.getReader();
|
|
let cancelCloseReaderClosed = false;
|
|
async function watchCancelCloseReader(): Promise<void> {
|
|
await cancelCloseReader.closed;
|
|
cancelCloseReaderClosed = true;
|
|
}
|
|
void watchCancelCloseReader();
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
console.log(
|
|
"cancel close-requested:",
|
|
cancelCloseController.desiredSize,
|
|
cancelCloseCalls,
|
|
cancelCloseReaderClosed,
|
|
);
|
|
|
|
let erroredCancelCalls = 0;
|
|
const alreadyErrored = new ReadableStream<number>({
|
|
start(controller) {
|
|
controller.error(new Error("cancel boom"));
|
|
},
|
|
cancel() {
|
|
erroredCancelCalls++;
|
|
},
|
|
});
|
|
try {
|
|
await alreadyErrored.cancel();
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log(
|
|
"errored cancel:",
|
|
caught.name,
|
|
caught.message,
|
|
erroredCancelCalls,
|
|
);
|
|
}
|
|
|
|
const desiredSizes: Array<number | null> = [];
|
|
const desiredSizeStream = new ReadableStream<number>({
|
|
start(controller) {
|
|
desiredSizes.push(controller.desiredSize);
|
|
controller.enqueue(1);
|
|
desiredSizes.push(controller.desiredSize);
|
|
controller.enqueue(2);
|
|
desiredSizes.push(controller.desiredSize);
|
|
controller.close();
|
|
desiredSizes.push(controller.desiredSize);
|
|
},
|
|
});
|
|
console.log(
|
|
"desired sizes:",
|
|
JSON.stringify(desiredSizes),
|
|
desiredSizeStream.locked,
|
|
);
|
|
|
|
const omittedChunk = new ReadableStream<undefined>({
|
|
start(controller) {
|
|
controller.enqueue();
|
|
controller.close();
|
|
},
|
|
});
|
|
const omittedPart = await omittedChunk.getReader().read();
|
|
console.log("omitted enqueue:", omittedPart.done, omittedPart.value === undefined);
|
|
|
|
try {
|
|
new ReadableStream<number>({
|
|
start(controller) {
|
|
controller.close();
|
|
controller.close();
|
|
},
|
|
});
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("double close:", caught.name, caught.message);
|
|
}
|
|
|
|
let delayedRequestPullCount = 0;
|
|
const delayedRequestBody = new ReadableStream<Uint8Array>({
|
|
async start() {
|
|
// Let fetch attach as the consumer before the first pull.
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 1));
|
|
},
|
|
pull(controller) {
|
|
const call = ++delayedRequestPullCount;
|
|
if (call === 1) {
|
|
setTimeout(() => {
|
|
controller.enqueue(Buffer.from("delayed request"));
|
|
controller.close();
|
|
}, 5);
|
|
} else if (call === 3) {
|
|
controller.error(new Error("request pull re-entered before enqueue"));
|
|
}
|
|
},
|
|
});
|
|
const delayedRequestResponse = await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
body: delayedRequestBody,
|
|
duplex: "half",
|
|
});
|
|
console.log(
|
|
"delayed request pull:",
|
|
await delayedRequestResponse.json(),
|
|
delayedRequestPullCount,
|
|
);
|
|
|
|
try {
|
|
await fetch(`${process.argv[2]}/redirect-stream-302`, {
|
|
method: "POST",
|
|
body: ReadableStream.from([Buffer.from("redirected stream")]),
|
|
duplex: "half",
|
|
});
|
|
console.log("stream 302 redirect unexpectedly followed");
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("stream 302 redirect:", caught.name, caught.message);
|
|
}
|
|
|
|
const stream303 = await fetch(`${process.argv[2]}/redirect-stream-303`, {
|
|
method: "POST",
|
|
body: ReadableStream.from([Buffer.from("redirected stream")]),
|
|
duplex: "half",
|
|
});
|
|
console.log("stream 303 redirect:", await stream303.json());
|
|
|
|
const matchedStreamLength = await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
headers: { "content-length": "2" },
|
|
body: ReadableStream.from([Buffer.from("hi")]),
|
|
duplex: "half",
|
|
});
|
|
console.log(
|
|
"matched stream content-length:",
|
|
await matchedStreamLength.json(),
|
|
);
|
|
|
|
try {
|
|
await fetch(`${process.argv[2]}/post-echo`, {
|
|
method: "POST",
|
|
headers: { "content-length": "5" },
|
|
body: ReadableStream.from([Buffer.from("hi")]),
|
|
duplex: "half",
|
|
signal: AbortSignal.timeout(200),
|
|
});
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log("stream content-length mismatch:", caught.name, caught.message);
|
|
}
|
|
|
|
let failingRequestPulls = 0;
|
|
const failingRequestBody = new ReadableStream<Uint8Array>({
|
|
async pull(controller) {
|
|
failingRequestPulls++;
|
|
controller.enqueue(Buffer.from("partial upload"));
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 25));
|
|
throw new Error("late upload failure");
|
|
},
|
|
});
|
|
try {
|
|
await fetch(`${process.argv[2]}/upload-failure`, {
|
|
method: "POST",
|
|
body: failingRequestBody,
|
|
duplex: "half",
|
|
});
|
|
console.log("failing request unexpectedly resolved");
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log(
|
|
"failing request:",
|
|
caught.name,
|
|
caught.message,
|
|
caught instanceof TypeError,
|
|
failingRequestPulls,
|
|
);
|
|
}
|
|
|
|
const truncatedResponse = await fetch(
|
|
`${process.argv[2]}/truncated-response`,
|
|
);
|
|
try {
|
|
await truncatedResponse.text();
|
|
console.log("truncated response unexpectedly read");
|
|
} catch (error) {
|
|
const caught = error as Error;
|
|
console.log(
|
|
"truncated response:",
|
|
caught.name,
|
|
caught.message,
|
|
caught instanceof TypeError,
|
|
caught instanceof DOMException,
|
|
);
|
|
}
|
|
|
|
let selfCapturingStream: ReadableStream<number>;
|
|
selfCapturingStream = new ReadableStream<number>({
|
|
pull(controller) {
|
|
console.log("self-capturing stream:", selfCapturingStream.locked);
|
|
controller.close();
|
|
},
|
|
});
|
|
await selfCapturingStream.getReader().read();
|
|
|
|
const selfCapturingSignal = AbortSignal.timeout(0);
|
|
selfCapturingSignal.addEventListener("abort", () => {
|
|
console.log("self-capturing abort:", selfCapturingSignal.aborted);
|
|
});
|
|
await new Promise<void>((resolve) => setTimeout(resolve, 5));
|
|
|
|
// These callbacks deliberately leave their owners open and capture the
|
|
// owner handles. The native teardown must sever both callback cycles before
|
|
// the sanitized RC audit runs.
|
|
function leaveOpenStreamCycle(): void {
|
|
let openStream!: ReadableStream<number>;
|
|
openStream = new ReadableStream<number>({
|
|
pull() {
|
|
void openStream.locked;
|
|
},
|
|
});
|
|
}
|
|
function leaveNeverAbortingSignalCycle(): void {
|
|
const openSignal = AbortSignal.any([]);
|
|
openSignal.addEventListener("abort", () => {
|
|
void openSignal.aborted;
|
|
});
|
|
}
|
|
leaveOpenStreamCycle();
|
|
leaveNeverAbortingSignalCycle();
|
|
await Promise.resolve();
|
|
|
|
// Dropping the only Response/body reference must not strand the transfer
|
|
// behind the native stream's one-chunk backpressure pause.
|
|
await fetch(
|
|
`${process.argv[2]}/backpressure?key=${pressureKey}-abandoned`,
|
|
);
|
|
console.log("abandoned response");
|