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

feat(std/node): Add Readable Stream / Writable Stream / errors support (#7569)

This commit is contained in:
Steven Guerrero 2020-11-21 16:13:18 -05:00 committed by GitHub
parent ce890f2ae7
commit a4f27c4d57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 4217 additions and 53 deletions

View file

@ -1,52 +1,99 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright Node.js contributors. All rights reserved. MIT License.
/************ NOT IMPLEMENTED
// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors. * ERR_INVALID_ARG_VALUE
* ERR_INVALID_MODULE_SPECIFIER
// Permission is hereby granted, free of charge, to any person obtaining a * ERR_INVALID_PACKAGE_TARGET
// copy of this software and associated documentation files (the * ERR_INVALID_URL_SCHEME
// "Software"), to deal in the Software without restriction, including * ERR_MANIFEST_ASSERT_INTEGRITY
// without limitation the rights to use, copy, modify, merge, publish, * ERR_MISSING_ARGS
// distribute, sublicense, and/or sell copies of the Software, and to permit * ERR_MODULE_NOT_FOUND
// persons to whom the Software is furnished to do so, subject to the * ERR_PACKAGE_PATH_NOT_EXPORTED
// following conditions: * ERR_QUICSESSION_VERSION_NEGOTIATION
* ERR_REQUIRE_ESM
// The above copyright notice and this permission notice shall be included * ERR_SOCKET_BAD_PORT
// in all copies or substantial portions of the Software. * ERR_TLS_CERT_ALTNAME_INVALID
* ERR_UNHANDLED_ERROR
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS * ERR_WORKER_INVALID_EXEC_ARGV
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * ERR_WORKER_PATH
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN * ERR_QUIC_ERROR
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * ERR_SOCKET_BUFFER_SIZE //System error, shouldn't ever happen inside Deno
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR * ERR_SYSTEM_ERROR //System error, shouldn't ever happen inside Deno
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE * ERR_TTY_INIT_FAILED //System error, shouldn't ever happen inside Deno
// USE OR OTHER DEALINGS IN THE SOFTWARE. * ERR_INVALID_PACKAGE_CONFIG // package.json stuff, probably useless
*************/
import { unreachable } from "../testing/asserts.ts"; import { unreachable } from "../testing/asserts.ts";
// It will do so until we'll have Node errors completely ported (#5944): /**
* All error instances in Node have additional methods and properties
* This export class is meant to be extended by these instances abstracting native JS error instances
*/
export class NodeErrorAbstraction extends Error {
code: string;
// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L251 constructor(name: string, code: string, message: string) {
// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L299 super(message);
// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L325 this.code = code;
// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L943
class ERR_INVALID_ARG_TYPE extends TypeError {
code = "ERR_INVALID_ARG_TYPE";
constructor(a1: string, a2: string, a3: unknown) {
super(
`The "${a1}" argument must be of type ${a2.toLocaleLowerCase()}. Received ${typeof a3} (${a3})`,
);
const { name } = this;
// Add the error code to the name to include it in the stack trace.
this.name = `${name} [${this.code}]`;
// Access the stack to generate the error message including the error code from the name.
this.stack;
// Reset the name to the actual name.
this.name = name; this.name = name;
//This number changes dependending on the name of this class
//20 characters as of now
this.stack = this.stack && `${name} [${this.code}]${this.stack.slice(20)}`;
}
toString() {
return `${this.name} [${this.code}]: ${this.message}`;
} }
} }
class ERR_OUT_OF_RANGE extends RangeError { export class NodeError extends NodeErrorAbstraction {
constructor(code: string, message: string) {
super(Error.prototype.name, code, message);
}
}
export class NodeSyntaxError extends NodeErrorAbstraction
implements SyntaxError {
constructor(code: string, message: string) {
super(SyntaxError.prototype.name, code, message);
Object.setPrototypeOf(this, SyntaxError.prototype);
}
}
export class NodeRangeError extends NodeErrorAbstraction {
constructor(code: string, message: string) {
super(RangeError.prototype.name, code, message);
Object.setPrototypeOf(this, RangeError.prototype);
}
}
export class NodeTypeError extends NodeErrorAbstraction implements TypeError {
constructor(code: string, message: string) {
super(TypeError.prototype.name, code, message);
Object.setPrototypeOf(this, TypeError.prototype);
}
}
export class NodeURIError extends NodeErrorAbstraction implements URIError {
constructor(code: string, message: string) {
super(URIError.prototype.name, code, message);
Object.setPrototypeOf(this, URIError.prototype);
}
}
export class ERR_INVALID_ARG_TYPE extends NodeTypeError {
constructor(a1: string, a2: string | string[], a3: unknown) {
super(
"ERR_INVALID_ARG_TYPE",
`The "${a1}" argument must be of type ${
typeof a2 === "string"
? a2.toLocaleLowerCase()
: a2.map((x) => x.toLocaleLowerCase()).join(", ")
}. Received ${typeof a3} (${a3})`,
);
}
}
export class ERR_OUT_OF_RANGE extends RangeError {
code = "ERR_OUT_OF_RANGE"; code = "ERR_OUT_OF_RANGE";
constructor(str: string, range: string, received: unknown) { constructor(str: string, range: string, received: unknown) {
@ -64,11 +111,6 @@ class ERR_OUT_OF_RANGE extends RangeError {
} }
} }
export const codes = {
ERR_INVALID_ARG_TYPE,
ERR_OUT_OF_RANGE,
};
// In Node these values are coming from libuv: // In Node these values are coming from libuv:
// Ref: https://github.com/libuv/libuv/blob/v1.x/include/uv/errno.h // Ref: https://github.com/libuv/libuv/blob/v1.x/include/uv/errno.h
// Ref: https://github.com/nodejs/node/blob/524123fbf064ff64bb6fcd83485cfc27db932f68/lib/internal/errors.js#L383 // Ref: https://github.com/nodejs/node/blob/524123fbf064ff64bb6fcd83485cfc27db932f68/lib/internal/errors.js#L383
@ -342,3 +384,100 @@ export const errorMap = new Map<number, [string, string]>(
? linux ? linux
: unreachable(), : unreachable(),
); );
export class ERR_METHOD_NOT_IMPLEMENTED extends NodeError {
constructor(x: string) {
super(
"ERR_METHOD_NOT_IMPLEMENTED",
`The ${x} method is not implemented`,
);
}
}
export class ERR_MULTIPLE_CALLBACK extends NodeError {
constructor() {
super(
"ERR_MULTIPLE_CALLBACK",
`Callback called multiple times`,
);
}
}
export class ERR_STREAM_ALREADY_FINISHED extends NodeError {
constructor(x: string) {
super(
"ERR_STREAM_ALREADY_FINISHED",
`Cannot call ${x} after a stream was finished`,
);
}
}
export class ERR_STREAM_CANNOT_PIPE extends NodeError {
constructor() {
super(
"ERR_STREAM_CANNOT_PIPE",
`Cannot pipe, not readable`,
);
}
}
export class ERR_STREAM_DESTROYED extends NodeError {
constructor(x: string) {
super(
"ERR_STREAM_DESTROYED",
`Cannot call ${x} after a stream was destroyed`,
);
}
}
export class ERR_STREAM_NULL_VALUES extends NodeTypeError {
constructor() {
super(
"ERR_STREAM_NULL_VALUES",
`May not write null values to stream`,
);
}
}
export class ERR_STREAM_PREMATURE_CLOSE extends NodeError {
constructor() {
super(
"ERR_STREAM_PREMATURE_CLOSE",
`Premature close`,
);
}
}
export class ERR_STREAM_PUSH_AFTER_EOF extends NodeError {
constructor() {
super(
"ERR_STREAM_PUSH_AFTER_EOF",
`stream.push() after EOF`,
);
}
}
export class ERR_STREAM_UNSHIFT_AFTER_END_EVENT extends NodeError {
constructor() {
super(
"ERR_STREAM_UNSHIFT_AFTER_END_EVENT",
`stream.unshift() after end event`,
);
}
}
export class ERR_STREAM_WRITE_AFTER_END extends NodeError {
constructor() {
super(
"ERR_STREAM_WRITE_AFTER_END",
`write after end`,
);
}
}
export class ERR_UNKNOWN_ENCODING extends NodeTypeError {
constructor(x: string) {
super(
"ERR_UNKNOWN_ENCODING",
`Unknown encoding: ${x}`,
);
}
}
export class ERR_INVALID_OPT_VALUE extends NodeTypeError {
constructor(name: string, value: unknown) {
super(
"ERR_INVALID_OPT_VALUE",
`The value "${value}" is invalid for option "${name}"`,
);
}
}

View file

@ -0,0 +1,260 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import type { Buffer } from "../buffer.ts";
import finished from "./end-of-stream.ts";
import Readable from "./readable.ts";
import type Stream from "./stream.ts";
const kLastResolve = Symbol("lastResolve");
const kLastReject = Symbol("lastReject");
const kError = Symbol("error");
const kEnded = Symbol("ended");
const kLastPromise = Symbol("lastPromise");
const kHandlePromise = Symbol("handlePromise");
const kStream = Symbol("stream");
// TODO(Soremwar)
// Add Duplex streams
type IterableStreams = Stream | Readable;
type IterableItem = Buffer | string | Uint8Array | undefined;
type ReadableIteratorResult = IteratorResult<IterableItem>;
function initIteratorSymbols(
o: ReadableStreamAsyncIterator,
symbols: symbol[],
) {
const properties: PropertyDescriptorMap = {};
for (const sym in symbols) {
properties[sym] = {
configurable: false,
enumerable: false,
writable: true,
};
}
Object.defineProperties(o, properties);
}
// TODO(Soremwar)
// Bring back once requests are implemented
// function isRequest(stream: any) {
// return stream && stream.setHeader && typeof stream.abort === "function";
// }
//TODO(Soremwar)
//Should be any implementation of stream
// deno-lint-ignore no-explicit-any
function destroyer(stream: any, err?: Error | null) {
// TODO(Soremwar)
// Bring back once requests are implemented
// if (isRequest(stream)) return stream.abort();
// if (isRequest(stream.req)) return stream.req.abort();
if (typeof stream.destroy === "function") return stream.destroy(err);
if (typeof stream.close === "function") return stream.close();
}
function createIterResult(
value: IterableItem,
done: boolean,
): ReadableIteratorResult {
return { value, done };
}
function readAndResolve(iter: ReadableStreamAsyncIterator) {
const resolve = iter[kLastResolve];
if (resolve !== null) {
const data = iter[kStream].read();
if (data !== null) {
iter[kLastPromise] = null;
iter[kLastResolve] = null;
iter[kLastReject] = null;
resolve(createIterResult(data, false));
}
}
}
function onReadable(iter: ReadableStreamAsyncIterator) {
queueMicrotask(() => readAndResolve(iter));
}
function wrapForNext(
lastPromise: Promise<ReadableIteratorResult>,
iter: ReadableStreamAsyncIterator,
) {
return (
resolve: (value: ReadableIteratorResult) => void,
reject: (error: Error) => void,
) => {
lastPromise.then(() => {
if (iter[kEnded]) {
resolve(createIterResult(undefined, true));
return;
}
iter[kHandlePromise](resolve, reject);
}, reject);
};
}
function finish(self: ReadableStreamAsyncIterator, err?: Error) {
return new Promise(
(
resolve: (result: ReadableIteratorResult) => void,
reject: (error: Error) => void,
) => {
const stream = self[kStream];
finished(stream, (err) => {
if (err && err.code !== "ERR_STREAM_PREMATURE_CLOSE") {
reject(err);
} else {
resolve(createIterResult(undefined, true));
}
});
destroyer(stream, err);
},
);
}
const AsyncIteratorPrototype = Object.getPrototypeOf(
Object.getPrototypeOf(async function* () {}).prototype,
);
class ReadableStreamAsyncIterator
implements AsyncIterableIterator<IterableItem> {
[kEnded]: boolean;
[kError]: Error | null = null;
[kHandlePromise] = (
resolve: (value: ReadableIteratorResult) => void,
reject: (value: Error) => void,
) => {
const data = this[kStream].read();
if (data) {
this[kLastPromise] = null;
this[kLastResolve] = null;
this[kLastReject] = null;
resolve(createIterResult(data, false));
} else {
this[kLastResolve] = resolve;
this[kLastReject] = reject;
}
};
[kLastPromise]: null | Promise<ReadableIteratorResult>;
[kLastReject]: null | ((value: Error) => void) = null;
[kLastResolve]: null | ((value: ReadableIteratorResult) => void) = null;
[kStream]: Readable;
[Symbol.asyncIterator] = AsyncIteratorPrototype[Symbol.asyncIterator];
constructor(stream: Readable) {
this[kEnded] = stream.readableEnded || stream._readableState.endEmitted;
this[kStream] = stream;
initIteratorSymbols(this, [
kEnded,
kError,
kHandlePromise,
kLastPromise,
kLastReject,
kLastResolve,
kStream,
]);
}
get stream() {
return this[kStream];
}
next(): Promise<ReadableIteratorResult> {
const error = this[kError];
if (error !== null) {
return Promise.reject(error);
}
if (this[kEnded]) {
return Promise.resolve(createIterResult(undefined, true));
}
if (this[kStream].destroyed) {
return new Promise((resolve, reject) => {
if (this[kError]) {
reject(this[kError]);
} else if (this[kEnded]) {
resolve(createIterResult(undefined, true));
} else {
finished(this[kStream], (err) => {
if (err && err.code !== "ERR_STREAM_PREMATURE_CLOSE") {
reject(err);
} else {
resolve(createIterResult(undefined, true));
}
});
}
});
}
const lastPromise = this[kLastPromise];
let promise;
if (lastPromise) {
promise = new Promise(wrapForNext(lastPromise, this));
} else {
const data = this[kStream].read();
if (data !== null) {
return Promise.resolve(createIterResult(data, false));
}
promise = new Promise(this[kHandlePromise]);
}
this[kLastPromise] = promise;
return promise;
}
return(): Promise<ReadableIteratorResult> {
return finish(this);
}
throw(err: Error): Promise<ReadableIteratorResult> {
return finish(this, err);
}
}
const createReadableStreamAsyncIterator = (stream: IterableStreams) => {
// deno-lint-ignore no-explicit-any
if (typeof (stream as any).read !== "function") {
const src = stream;
stream = new Readable({ objectMode: true }).wrap(src);
finished(stream, (err) => destroyer(src, err));
}
const iterator = new ReadableStreamAsyncIterator(stream as Readable);
iterator[kLastPromise] = null;
finished(stream, { writable: false }, (err) => {
if (err && err.code !== "ERR_STREAM_PREMATURE_CLOSE") {
const reject = iterator[kLastReject];
if (reject !== null) {
iterator[kLastPromise] = null;
iterator[kLastResolve] = null;
iterator[kLastReject] = null;
reject(err);
}
iterator[kError] = err;
return;
}
const resolve = iterator[kLastResolve];
if (resolve !== null) {
iterator[kLastPromise] = null;
iterator[kLastResolve] = null;
iterator[kLastReject] = null;
resolve(createIterResult(undefined, true));
}
iterator[kEnded] = true;
});
stream.on("readable", onReadable.bind(null, iterator));
return iterator;
};
export default createReadableStreamAsyncIterator;

View file

@ -0,0 +1,249 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import Readable from "./readable.ts";
import Stream from "./stream.ts";
import toReadableAsyncIterator from "./async_iterator.ts";
import { deferred } from "../../async/mod.ts";
import { assertEquals, assertThrowsAsync } from "../../testing/asserts.ts";
Deno.test("Stream to async iterator", async () => {
let destroyExecuted = 0;
const destroyExecutedExpected = 1;
const destroyExpectedExecutions = deferred();
class AsyncIteratorStream extends Stream {
constructor() {
super();
}
destroy() {
destroyExecuted++;
if (destroyExecuted == destroyExecutedExpected) {
destroyExpectedExecutions.resolve();
}
}
[Symbol.asyncIterator] = Readable.prototype[Symbol.asyncIterator];
}
const stream = new AsyncIteratorStream();
queueMicrotask(() => {
stream.emit("data", "hello");
stream.emit("data", "world");
stream.emit("end");
});
let res = "";
for await (const d of stream) {
res += d;
}
assertEquals(res, "helloworld");
const destroyTimeout = setTimeout(
() => destroyExpectedExecutions.reject(),
1000,
);
await destroyExpectedExecutions;
clearTimeout(destroyTimeout);
assertEquals(destroyExecuted, destroyExecutedExpected);
});
Deno.test("Stream to async iterator throws on 'error' emitted", async () => {
let closeExecuted = 0;
const closeExecutedExpected = 1;
const closeExpectedExecutions = deferred();
let errorExecuted = 0;
const errorExecutedExpected = 1;
const errorExpectedExecutions = deferred();
class StreamImplementation extends Stream {
close() {
closeExecuted++;
if (closeExecuted == closeExecutedExpected) {
closeExpectedExecutions.resolve();
}
}
}
const stream = new StreamImplementation();
queueMicrotask(() => {
stream.emit("data", 0);
stream.emit("data", 1);
stream.emit("error", new Error("asd"));
});
toReadableAsyncIterator(stream)
.next()
.catch((err) => {
errorExecuted++;
if (errorExecuted == errorExecutedExpected) {
errorExpectedExecutions.resolve();
}
assertEquals(err.message, "asd");
});
const closeTimeout = setTimeout(
() => closeExpectedExecutions.reject(),
1000,
);
const errorTimeout = setTimeout(
() => errorExpectedExecutions.reject(),
1000,
);
await closeExpectedExecutions;
await errorExpectedExecutions;
clearTimeout(closeTimeout);
clearTimeout(errorTimeout);
assertEquals(closeExecuted, closeExecutedExpected);
assertEquals(errorExecuted, errorExecutedExpected);
});
Deno.test("Async iterator matches values of Readable", async () => {
const readable = new Readable({
objectMode: true,
read() {},
});
readable.push(0);
readable.push(1);
readable.push(null);
const iter = readable[Symbol.asyncIterator]();
assertEquals(
await iter.next().then(({ value }) => value),
0,
);
for await (const d of iter) {
assertEquals(d, 1);
}
});
Deno.test("Async iterator throws on Readable destroyed sync", async () => {
const message = "kaboom from read";
const readable = new Readable({
objectMode: true,
read() {
this.destroy(new Error(message));
},
});
await assertThrowsAsync(
async () => {
// deno-lint-ignore no-empty
for await (const k of readable) {}
},
Error,
message,
);
});
Deno.test("Async iterator throws on Readable destroyed async", async () => {
const message = "kaboom";
const readable = new Readable({
read() {},
});
const iterator = readable[Symbol.asyncIterator]();
readable.destroy(new Error(message));
await assertThrowsAsync(
iterator.next.bind(iterator),
Error,
message,
);
});
Deno.test("Async iterator finishes the iterator when Readable destroyed", async () => {
const readable = new Readable({
read() {},
});
readable.destroy();
const { done } = await readable[Symbol.asyncIterator]().next();
assertEquals(done, true);
});
Deno.test("Async iterator finishes all item promises when Readable destroyed", async () => {
const r = new Readable({
objectMode: true,
read() {
},
});
const b = r[Symbol.asyncIterator]();
const c = b.next();
const d = b.next();
r.destroy();
assertEquals(await c, { done: true, value: undefined });
assertEquals(await d, { done: true, value: undefined });
});
Deno.test("Async iterator: 'next' is triggered by Readable push", async () => {
const max = 42;
let readed = 0;
let received = 0;
const readable = new Readable({
objectMode: true,
read() {
this.push("hello");
if (++readed === max) {
this.push(null);
}
},
});
for await (const k of readable) {
received++;
assertEquals(k, "hello");
}
assertEquals(readed, received);
});
Deno.test("Async iterator: 'close' called on forced iteration end", async () => {
let closeExecuted = 0;
const closeExecutedExpected = 1;
const closeExpectedExecutions = deferred();
class IndestructibleReadable extends Readable {
constructor() {
super({
autoDestroy: false,
read() {},
});
}
close() {
closeExecuted++;
if (closeExecuted == closeExecutedExpected) {
closeExpectedExecutions.resolve();
}
readable.emit("close");
}
// deno-lint-ignore ban-ts-comment
//@ts-ignore
destroy = null;
}
const readable = new IndestructibleReadable();
readable.push("asd");
readable.push("asd");
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for await (const d of readable) {
break;
}
const closeTimeout = setTimeout(
() => closeExpectedExecutions.reject(),
1000,
);
await closeExpectedExecutions;
clearTimeout(closeTimeout);
assertEquals(closeExecuted, closeExecutedExpected);
});

View file

@ -0,0 +1,183 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import { Buffer } from "../buffer.ts";
type BufferListItem = {
data: Buffer | string | Uint8Array;
next: BufferListItem | null;
};
export default class BufferList {
head: BufferListItem | null = null;
tail: BufferListItem | null = null;
length: number;
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
push(v: Buffer | string | Uint8Array) {
const entry = { data: v, next: null };
if (this.length > 0) {
(this.tail as BufferListItem).next = entry;
} else {
this.head = entry;
}
this.tail = entry;
++this.length;
}
unshift(v: Buffer | string | Uint8Array) {
const entry = { data: v, next: this.head };
if (this.length === 0) {
this.tail = entry;
}
this.head = entry;
++this.length;
}
shift() {
if (this.length === 0) {
return;
}
const ret = (this.head as BufferListItem).data;
if (this.length === 1) {
this.head = this.tail = null;
} else {
this.head = (this.head as BufferListItem).next;
}
--this.length;
return ret;
}
clear() {
this.head = this.tail = null;
this.length = 0;
}
join(s: string) {
if (this.length === 0) {
return "";
}
let p: BufferListItem | null = (this.head as BufferListItem);
let ret = "" + p.data;
p = p.next;
while (p) {
ret += s + p.data;
p = p.next;
}
return ret;
}
concat(n: number) {
if (this.length === 0) {
return Buffer.alloc(0);
}
const ret = Buffer.allocUnsafe(n >>> 0);
let p = this.head;
let i = 0;
while (p) {
ret.set(p.data as Buffer, i);
i += p.data.length;
p = p.next;
}
return ret;
}
// Consumes a specified amount of bytes or characters from the buffered data.
consume(n: number, hasStrings: boolean) {
const data = (this.head as BufferListItem).data;
if (n < data.length) {
// `slice` is the same for buffers and strings.
const slice = data.slice(0, n);
(this.head as BufferListItem).data = data.slice(n);
return slice;
}
if (n === data.length) {
// First chunk is a perfect match.
return this.shift();
}
// Result spans more than one buffer.
return hasStrings ? this._getString(n) : this._getBuffer(n);
}
first() {
return (this.head as BufferListItem).data;
}
*[Symbol.iterator]() {
for (let p = this.head; p; p = p.next) {
yield p.data;
}
}
// Consumes a specified amount of characters from the buffered data.
_getString(n: number) {
let ret = "";
let p: BufferListItem | null = (this.head as BufferListItem);
let c = 0;
p = p.next as BufferListItem;
do {
const str = p.data;
if (n > str.length) {
ret += str;
n -= str.length;
} else {
if (n === str.length) {
ret += str;
++c;
if (p.next) {
this.head = p.next;
} else {
this.head = this.tail = null;
}
} else {
ret += str.slice(0, n);
this.head = p;
p.data = str.slice(n);
}
break;
}
++c;
p = p.next;
} while (p);
this.length -= c;
return ret;
}
// Consumes a specified amount of bytes from the buffered data.
_getBuffer(n: number) {
const ret = Buffer.allocUnsafe(n);
const retLen = n;
let p: BufferListItem | null = (this.head as BufferListItem);
let c = 0;
p = p.next as BufferListItem;
do {
const buf = p.data as Buffer;
if (n > buf.length) {
ret.set(buf, retLen - n);
n -= buf.length;
} else {
if (n === buf.length) {
ret.set(buf, retLen - n);
++c;
if (p.next) {
this.head = p.next;
} else {
this.head = this.tail = null;
}
} else {
ret.set(new Uint8Array(buf.buffer, buf.byteOffset, n), retLen - n);
this.head = p;
p.data = buf.slice(n);
}
break;
}
++c;
p = p.next;
} while (p);
this.length -= c;
return ret;
}
}

View file

@ -0,0 +1,3 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
// deno-lint-ignore no-explicit-any
export const errorOrDestroy = (...args: any[]) => {};

View file

@ -0,0 +1,240 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import { once } from "../_utils.ts";
import type Readable from "./readable.ts";
import type Stream from "./stream.ts";
import type { ReadableState } from "./readable.ts";
import type Writable from "./writable.ts";
import type { WritableState } from "./writable.ts";
import {
ERR_INVALID_ARG_TYPE,
ERR_STREAM_PREMATURE_CLOSE,
NodeErrorAbstraction,
} from "../_errors.ts";
type StreamImplementations = Readable | Stream | Writable;
// TODO(Soremwar)
// Bring back once requests are implemented
// function isRequest(stream: Stream) {
// return stream.setHeader && typeof stream.abort === "function";
// }
// deno-lint-ignore no-explicit-any
function isReadable(stream: any) {
return typeof stream.readable === "boolean" ||
typeof stream.readableEnded === "boolean" ||
!!stream._readableState;
}
// deno-lint-ignore no-explicit-any
function isWritable(stream: any) {
return typeof stream.writable === "boolean" ||
typeof stream.writableEnded === "boolean" ||
!!stream._writableState;
}
function isWritableFinished(stream: Writable) {
if (stream.writableFinished) return true;
const wState = stream._writableState;
if (!wState || wState.errored) return false;
return wState.finished || (wState.ended && wState.length === 0);
}
function nop() {}
function isReadableEnded(stream: Readable) {
if (stream.readableEnded) return true;
const rState = stream._readableState;
if (!rState || rState.errored) return false;
return rState.endEmitted || (rState.ended && rState.length === 0);
}
interface FinishedOptions {
error?: boolean;
readable?: boolean;
writable?: boolean;
}
/**
* Appends an ending callback triggered when a stream is no longer readable,
* writable or has experienced an error or a premature close event
*/
export default function eos(
stream: StreamImplementations,
options: FinishedOptions | null,
callback: (err?: NodeErrorAbstraction | null) => void,
): () => void;
export default function eos(
stream: StreamImplementations,
callback: (err?: NodeErrorAbstraction | null) => void,
): () => void;
export default function eos(
stream: StreamImplementations,
x: FinishedOptions | ((err?: NodeErrorAbstraction | null) => void) | null,
y?: (err?: NodeErrorAbstraction | null) => void,
) {
let opts: FinishedOptions;
let callback: (err?: NodeErrorAbstraction | null) => void;
if (!y) {
if (typeof x !== "function") {
throw new ERR_INVALID_ARG_TYPE("callback", "function", x);
}
opts = {};
callback = x;
} else {
if (!x || Array.isArray(x) || typeof x !== "object") {
throw new ERR_INVALID_ARG_TYPE("opts", "object", x);
}
opts = x;
if (typeof y !== "function") {
throw new ERR_INVALID_ARG_TYPE("callback", "function", y);
}
callback = y;
}
callback = once(callback);
const readable = opts.readable ?? isReadable(stream);
const writable = opts.writable ?? isWritable(stream);
// deno-lint-ignore no-explicit-any
const wState: WritableState | undefined = (stream as any)._writableState;
// deno-lint-ignore no-explicit-any
const rState: ReadableState | undefined = (stream as any)._readableState;
const validState = wState || rState;
const onlegacyfinish = () => {
if (!(stream as Writable).writable) {
onfinish();
}
};
let willEmitClose = (
validState?.autoDestroy &&
validState?.emitClose &&
validState?.closed === false &&
isReadable(stream) === readable &&
isWritable(stream) === writable
);
let writableFinished = (stream as Writable).writableFinished ||
wState?.finished;
const onfinish = () => {
writableFinished = true;
// deno-lint-ignore no-explicit-any
if ((stream as any).destroyed) {
willEmitClose = false;
}
if (willEmitClose && (!(stream as Readable).readable || readable)) {
return;
}
if (!readable || readableEnded) {
callback.call(stream);
}
};
let readableEnded = (stream as Readable).readableEnded || rState?.endEmitted;
const onend = () => {
readableEnded = true;
// deno-lint-ignore no-explicit-any
if ((stream as any).destroyed) {
willEmitClose = false;
}
if (willEmitClose && (!(stream as Writable).writable || writable)) {
return;
}
if (!writable || writableFinished) {
callback.call(stream);
}
};
const onerror = (err: NodeErrorAbstraction) => {
callback.call(stream, err);
};
const onclose = () => {
if (readable && !readableEnded) {
if (!isReadableEnded(stream as Readable)) {
return callback.call(stream, new ERR_STREAM_PREMATURE_CLOSE());
}
}
if (writable && !writableFinished) {
if (!isWritableFinished(stream as Writable)) {
return callback.call(stream, new ERR_STREAM_PREMATURE_CLOSE());
}
}
callback.call(stream);
};
// TODO(Soremwar)
// Bring back once requests are implemented
// const onrequest = () => {
// stream.req.on("finish", onfinish);
// };
// TODO(Soremwar)
// Bring back once requests are implemented
// if (isRequest(stream)) {
// stream.on("complete", onfinish);
// stream.on("abort", onclose);
// if (stream.req) {
// onrequest();
// } else {
// stream.on("request", onrequest);
// }
// } else
if (writable && !wState) {
stream.on("end", onlegacyfinish);
stream.on("close", onlegacyfinish);
}
// TODO(Soremwar)
// Bring back once requests are implemented
// if (typeof stream.aborted === "boolean") {
// stream.on("aborted", onclose);
// }
stream.on("end", onend);
stream.on("finish", onfinish);
if (opts.error !== false) stream.on("error", onerror);
stream.on("close", onclose);
const closed = (
wState?.closed ||
rState?.closed ||
wState?.errorEmitted ||
rState?.errorEmitted ||
// TODO(Soremwar)
// Bring back once requests are implemented
// (rState && stream.req && stream.aborted) ||
(
(!writable || wState?.finished) &&
(!readable || rState?.endEmitted)
)
);
if (closed) {
queueMicrotask(callback);
}
return function () {
callback = nop;
stream.removeListener("aborted", onclose);
stream.removeListener("complete", onfinish);
stream.removeListener("abort", onclose);
// TODO(Soremwar)
// Bring back once requests are implemented
// stream.removeListener("request", onrequest);
// if (stream.req) stream.req.removeListener("finish", onfinish);
stream.removeListener("end", onlegacyfinish);
stream.removeListener("close", onlegacyfinish);
stream.removeListener("finish", onfinish);
stream.removeListener("end", onend);
stream.removeListener("error", onerror);
stream.removeListener("close", onclose);
};
}

102
std/node/_stream/from.ts Normal file
View file

@ -0,0 +1,102 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import { Buffer } from "../buffer.ts";
import Readable from "./readable.ts";
import type { ReadableOptions } from "./readable.ts";
import { ERR_INVALID_ARG_TYPE, ERR_STREAM_NULL_VALUES } from "../_errors.ts";
export default function from(
// deno-lint-ignore no-explicit-any
iterable: Iterable<any> | AsyncIterable<any>,
opts?: ReadableOptions,
) {
let iterator:
// deno-lint-ignore no-explicit-any
| Iterator<any, any, undefined>
// deno-lint-ignore no-explicit-any
| AsyncIterator<any, any, undefined>;
if (typeof iterable === "string" || iterable instanceof Buffer) {
return new Readable({
objectMode: true,
...opts,
read() {
this.push(iterable);
this.push(null);
},
});
}
if (Symbol.asyncIterator in iterable) {
// deno-lint-ignore no-explicit-any
iterator = (iterable as AsyncIterable<any>)[Symbol.asyncIterator]();
} else if (Symbol.iterator in iterable) {
// deno-lint-ignore no-explicit-any
iterator = (iterable as Iterable<any>)[Symbol.iterator]();
} else {
throw new ERR_INVALID_ARG_TYPE("iterable", ["Iterable"], iterable);
}
const readable = new Readable({
objectMode: true,
highWaterMark: 1,
...opts,
});
// Reading boolean to protect against _read
// being called before last iteration completion.
let reading = false;
// needToClose boolean if iterator needs to be explicitly closed
let needToClose = false;
readable._read = function () {
if (!reading) {
reading = true;
next();
}
};
readable._destroy = function (error, cb) {
if (needToClose) {
needToClose = false;
close().then(
() => queueMicrotask(() => cb(error)),
(e) => queueMicrotask(() => cb(error || e)),
);
} else {
cb(error);
}
};
async function close() {
if (typeof iterator.return === "function") {
const { value } = await iterator.return();
await value;
}
}
async function next() {
try {
needToClose = false;
const { value, done } = await iterator.next();
needToClose = !done;
if (done) {
readable.push(null);
} else if (readable.destroyed) {
await close();
} else {
const res = await value;
if (res === null) {
reading = false;
throw new ERR_STREAM_NULL_VALUES();
} else if (readable.push(res)) {
next();
} else {
reading = false;
}
}
} catch (err) {
readable.destroy(err);
}
}
return readable;
}

1248
std/node/_stream/readable.ts Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,489 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import { Buffer } from "../buffer.ts";
import Readable from "../_stream/readable.ts";
import { once } from "../events.ts";
import { deferred } from "../../async/mod.ts";
import {
assert,
assertEquals,
assertStrictEquals,
} from "../../testing/asserts.ts";
Deno.test("Readable stream from iterator", async () => {
function* generate() {
yield "a";
yield "b";
yield "c";
}
const stream = Readable.from(generate());
const expected = ["a", "b", "c"];
for await (const chunk of stream) {
assertStrictEquals(chunk, expected.shift());
}
});
Deno.test("Readable stream from async iterator", async () => {
async function* generate() {
yield "a";
yield "b";
yield "c";
}
const stream = Readable.from(generate());
const expected = ["a", "b", "c"];
for await (const chunk of stream) {
assertStrictEquals(chunk, expected.shift());
}
});
Deno.test("Readable stream from promise", async () => {
const promises = [
Promise.resolve("a"),
Promise.resolve("b"),
Promise.resolve("c"),
];
const stream = Readable.from(promises);
const expected = ["a", "b", "c"];
for await (const chunk of stream) {
assertStrictEquals(chunk, expected.shift());
}
});
Deno.test("Readable stream from string", async () => {
const string = "abc";
const stream = Readable.from(string);
for await (const chunk of stream) {
assertStrictEquals(chunk, string);
}
});
Deno.test("Readable stream from Buffer", async () => {
const string = "abc";
const stream = Readable.from(Buffer.from(string));
for await (const chunk of stream) {
assertStrictEquals((chunk as Buffer).toString(), string);
}
});
Deno.test("Readable stream gets destroyed on error", async () => {
// deno-lint-ignore require-yield
async function* generate() {
throw new Error("kaboom");
}
const stream = Readable.from(generate());
stream.read();
const [err] = await once(stream, "error");
assertStrictEquals(err.message, "kaboom");
assertStrictEquals(stream.destroyed, true);
});
Deno.test("Readable stream works as Transform stream", async () => {
async function* generate(stream: Readable) {
for await (const chunk of stream) {
yield (chunk as string).toUpperCase();
}
}
const source = new Readable({
objectMode: true,
read() {
this.push("a");
this.push("b");
this.push("c");
this.push(null);
},
});
const stream = Readable.from(generate(source));
const expected = ["A", "B", "C"];
for await (const chunk of stream) {
assertStrictEquals(chunk, expected.shift());
}
});
Deno.test("Readable stream can be paused", () => {
const readable = new Readable();
// _read is a noop, here.
readable._read = () => {};
// Default state of a stream is not "paused"
assert(!readable.isPaused());
// Make the stream start flowing...
readable.on("data", () => {});
// still not paused.
assert(!readable.isPaused());
readable.pause();
assert(readable.isPaused());
readable.resume();
assert(!readable.isPaused());
});
Deno.test("Readable stream sets enconding correctly", () => {
const readable = new Readable({
read() {},
});
readable.setEncoding("utf8");
readable.push(new TextEncoder().encode("DEF"));
readable.unshift(new TextEncoder().encode("ABC"));
assertStrictEquals(readable.read(), "ABCDEF");
});
Deno.test("Readable stream sets encoding correctly", () => {
const readable = new Readable({
read() {},
});
readable.setEncoding("utf8");
readable.push(new TextEncoder().encode("DEF"));
readable.unshift(new TextEncoder().encode("ABC"));
assertStrictEquals(readable.read(), "ABCDEF");
});
Deno.test("Readable stream holds up a big push", async () => {
let readExecuted = 0;
const readExecutedExpected = 3;
const readExpectedExecutions = deferred();
let endExecuted = 0;
const endExecutedExpected = 1;
const endExpectedExecutions = deferred();
const str = "asdfasdfasdfasdfasdf";
const r = new Readable({
highWaterMark: 5,
encoding: "utf8",
});
let reads = 0;
function _read() {
if (reads === 0) {
setTimeout(() => {
r.push(str);
}, 1);
reads++;
} else if (reads === 1) {
const ret = r.push(str);
assertEquals(ret, false);
reads++;
} else {
r.push(null);
}
}
r._read = () => {
readExecuted++;
if (readExecuted == readExecutedExpected) {
readExpectedExecutions.resolve();
}
_read();
};
r.on("end", () => {
endExecuted++;
if (endExecuted == endExecutedExpected) {
endExpectedExecutions.resolve();
}
});
// Push some data in to start.
// We've never gotten any read event at this point.
const ret = r.push(str);
assert(!ret);
let chunk = r.read();
assertEquals(chunk, str);
chunk = r.read();
assertEquals(chunk, null);
r.once("readable", () => {
// This time, we'll get *all* the remaining data, because
// it's been added synchronously, as the read WOULD take
// us below the hwm, and so it triggered a _read() again,
// which synchronously added more, which we then return.
chunk = r.read();
assertEquals(chunk, str + str);
chunk = r.read();
assertEquals(chunk, null);
});
const readTimeout = setTimeout(
() => readExpectedExecutions.reject(),
1000,
);
const endTimeout = setTimeout(
() => endExpectedExecutions.reject(),
1000,
);
await readExpectedExecutions;
await endExpectedExecutions;
clearTimeout(readTimeout);
clearTimeout(endTimeout);
assertEquals(readExecuted, readExecutedExpected);
assertEquals(endExecuted, endExecutedExpected);
});
Deno.test("Readable stream: 'on' event", async () => {
async function* generate() {
yield "a";
yield "b";
yield "c";
}
const stream = Readable.from(generate());
let iterations = 0;
const expected = ["a", "b", "c"];
stream.on("data", (chunk) => {
iterations++;
assertStrictEquals(chunk, expected.shift());
});
await once(stream, "end");
assertStrictEquals(iterations, 3);
});
Deno.test("Readable stream: 'data' event", async () => {
async function* generate() {
yield "a";
yield "b";
yield "c";
}
const stream = Readable.from(generate(), { objectMode: false });
let iterations = 0;
const expected = ["a", "b", "c"];
stream.on("data", (chunk) => {
iterations++;
assertStrictEquals(chunk instanceof Buffer, true);
assertStrictEquals(chunk.toString(), expected.shift());
});
await once(stream, "end");
assertStrictEquals(iterations, 3);
});
Deno.test("Readable stream: 'data' event on non-object", async () => {
async function* generate() {
yield "a";
yield "b";
yield "c";
}
const stream = Readable.from(generate(), { objectMode: false });
let iterations = 0;
const expected = ["a", "b", "c"];
stream.on("data", (chunk) => {
iterations++;
assertStrictEquals(chunk instanceof Buffer, true);
assertStrictEquals(chunk.toString(), expected.shift());
});
await once(stream, "end");
assertStrictEquals(iterations, 3);
});
Deno.test("Readable stream: 'readable' event is emitted but 'read' is not on highWaterMark length exceeded", async () => {
let readableExecuted = 0;
const readableExecutedExpected = 1;
const readableExpectedExecutions = deferred();
const r = new Readable({
highWaterMark: 3,
});
r._read = () => {
throw new Error("_read must not be called");
};
r.push(Buffer.from("blerg"));
setTimeout(function () {
assert(!r._readableState.reading);
r.on("readable", () => {
readableExecuted++;
if (readableExecuted == readableExecutedExpected) {
readableExpectedExecutions.resolve();
}
});
}, 1);
const readableTimeout = setTimeout(
() => readableExpectedExecutions.reject(),
1000,
);
await readableExpectedExecutions;
clearTimeout(readableTimeout);
assertEquals(readableExecuted, readableExecutedExpected);
});
Deno.test("Readable stream: 'readable' and 'read' events are emitted on highWaterMark length not reached", async () => {
let readableExecuted = 0;
const readableExecutedExpected = 1;
const readableExpectedExecutions = deferred();
let readExecuted = 0;
const readExecutedExpected = 1;
const readExpectedExecutions = deferred();
const r = new Readable({
highWaterMark: 3,
});
r._read = () => {
readExecuted++;
if (readExecuted == readExecutedExpected) {
readExpectedExecutions.resolve();
}
};
r.push(Buffer.from("bl"));
setTimeout(function () {
assert(r._readableState.reading);
r.on("readable", () => {
readableExecuted++;
if (readableExecuted == readableExecutedExpected) {
readableExpectedExecutions.resolve();
}
});
}, 1);
const readableTimeout = setTimeout(
() => readableExpectedExecutions.reject(),
1000,
);
const readTimeout = setTimeout(
() => readExpectedExecutions.reject(),
1000,
);
await readableExpectedExecutions;
await readExpectedExecutions;
clearTimeout(readableTimeout);
clearTimeout(readTimeout);
assertEquals(readableExecuted, readableExecutedExpected);
assertEquals(readExecuted, readExecutedExpected);
});
Deno.test("Readable stream: 'readable' event is emitted but 'read' is not on highWaterMark length not reached and stream ended", async () => {
let readableExecuted = 0;
const readableExecutedExpected = 1;
const readableExpectedExecutions = deferred();
const r = new Readable({
highWaterMark: 30,
});
r._read = () => {
throw new Error("Must not be executed");
};
r.push(Buffer.from("blerg"));
//This ends the stream and triggers end
r.push(null);
setTimeout(function () {
// Assert we're testing what we think we are
assert(!r._readableState.reading);
r.on("readable", () => {
readableExecuted++;
if (readableExecuted == readableExecutedExpected) {
readableExpectedExecutions.resolve();
}
});
}, 1);
const readableTimeout = setTimeout(
() => readableExpectedExecutions.reject(),
1000,
);
await readableExpectedExecutions;
clearTimeout(readableTimeout);
assertEquals(readableExecuted, readableExecutedExpected);
});
Deno.test("Readable stream: 'read' is emitted on empty string pushed in non-object mode", async () => {
let endExecuted = 0;
const endExecutedExpected = 1;
const endExpectedExecutions = deferred();
const underlyingData = ["", "x", "y", "", "z"];
const expected = underlyingData.filter((data) => data);
const result: unknown[] = [];
const r = new Readable({
encoding: "utf8",
});
r._read = function () {
queueMicrotask(() => {
if (!underlyingData.length) {
this.push(null);
} else {
this.push(underlyingData.shift());
}
});
};
r.on("readable", () => {
const data = r.read();
if (data !== null) result.push(data);
});
r.on("end", () => {
endExecuted++;
if (endExecuted == endExecutedExpected) {
endExpectedExecutions.resolve();
}
assertEquals(result, expected);
});
const endTimeout = setTimeout(
() => endExpectedExecutions.reject(),
1000,
);
await endExpectedExecutions;
clearTimeout(endTimeout);
assertEquals(endExecuted, endExecutedExpected);
});
Deno.test("Readable stream: listeners can be removed", () => {
const r = new Readable();
r._read = () => {};
r.on("data", () => {});
r.removeAllListeners("data");
assertEquals(r.eventNames().length, 0);
});

View file

@ -0,0 +1,79 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import { Buffer } from "../buffer.ts";
import EventEmitter from "../events.ts";
import type Writable from "./writable.ts";
import { types } from "../util.ts";
class Stream extends EventEmitter {
constructor() {
super();
}
static _isUint8Array = types.isUint8Array;
static _uint8ArrayToBuffer = (chunk: Uint8Array) => Buffer.from(chunk);
pipe(dest: Writable, options: { end: boolean }) {
// deno-lint-ignore no-this-alias
const source = this;
//TODO(Soremwar)
//isStdio exist on stdin || stdout only, which extend from Duplex
//if (!dest._isStdio && (options?.end ?? true)) {
//Find an alternative to be able to pipe streams to stdin & stdout
//Port them as well?
if (options?.end ?? true) {
source.on("end", onend);
source.on("close", onclose);
}
let didOnEnd = false;
function onend() {
if (didOnEnd) return;
didOnEnd = true;
dest.end();
}
function onclose() {
if (didOnEnd) return;
didOnEnd = true;
if (typeof dest.destroy === "function") dest.destroy();
}
// Don't leave dangling pipes when there are errors.
function onerror(this: Stream, er: Error) {
cleanup();
if (this.listenerCount("error") === 0) {
throw er; // Unhandled stream error in pipe.
}
}
source.on("error", onerror);
dest.on("error", onerror);
// Remove all the event listeners that were added.
function cleanup() {
source.removeListener("end", onend);
source.removeListener("close", onclose);
source.removeListener("error", onerror);
dest.removeListener("error", onerror);
source.removeListener("end", cleanup);
source.removeListener("close", cleanup);
dest.removeListener("close", cleanup);
}
source.on("end", cleanup);
source.on("close", cleanup);
dest.on("close", cleanup);
dest.emit("pipe", source);
return dest;
}
}
export default Stream;

View file

@ -0,0 +1,4 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
export const kConstruct = Symbol("kConstruct");
export const kDestroy = Symbol("kDestroy");
export const kPaused = Symbol("kPaused");

View file

@ -0,0 +1,952 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import { Buffer } from "../buffer.ts";
import Stream from "./stream.ts";
import { captureRejectionSymbol } from "../events.ts";
import { kConstruct, kDestroy } from "./symbols.ts";
import {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_OPT_VALUE,
ERR_METHOD_NOT_IMPLEMENTED,
ERR_MULTIPLE_CALLBACK,
ERR_STREAM_ALREADY_FINISHED,
ERR_STREAM_CANNOT_PIPE,
ERR_STREAM_DESTROYED,
ERR_STREAM_NULL_VALUES,
ERR_STREAM_WRITE_AFTER_END,
ERR_UNKNOWN_ENCODING,
} from "../_errors.ts";
function nop() {}
//TODO(Soremwar)
//Bring in encodings
type write_v = (
// deno-lint-ignore no-explicit-any
chunks: Array<{ chunk: any; encoding: string }>,
callback: (error?: Error | null) => void,
) => void;
type AfterWriteTick = {
cb: (error?: Error) => void;
count: number;
state: WritableState;
stream: Writable;
};
const kOnFinished = Symbol("kOnFinished");
function destroy(this: Writable, err?: Error, cb?: () => void) {
const w = this._writableState;
if (w.destroyed) {
if (typeof cb === "function") {
cb();
}
return this;
}
if (err) {
// Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364
err.stack;
if (!w.errored) {
w.errored = err;
}
}
w.destroyed = true;
if (!w.constructed) {
this.once(kDestroy, (er) => {
_destroy(this, err || er, cb);
});
} else {
_destroy(this, err, cb);
}
return this;
}
function _destroy(
self: Writable,
err?: Error,
cb?: (error?: Error | null) => void,
) {
self._destroy(err || null, (err) => {
const w = self._writableState;
if (err) {
// Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364
err.stack;
if (!w.errored) {
w.errored = err;
}
}
w.closed = true;
if (typeof cb === "function") {
cb(err);
}
if (err) {
queueMicrotask(() => {
if (!w.errorEmitted) {
w.errorEmitted = true;
self.emit("error", err);
}
w.closeEmitted = true;
if (w.emitClose) {
self.emit("close");
}
});
} else {
queueMicrotask(() => {
w.closeEmitted = true;
if (w.emitClose) {
self.emit("close");
}
});
}
});
}
function errorOrDestroy(stream: Writable, err: Error, sync = false) {
const w = stream._writableState;
if (w.destroyed) {
return stream;
}
if (w.autoDestroy) {
stream.destroy(err);
} else if (err) {
// Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364
err.stack;
if (!w.errored) {
w.errored = err;
}
if (sync) {
queueMicrotask(() => {
if (w.errorEmitted) {
return;
}
w.errorEmitted = true;
stream.emit("error", err);
});
} else {
if (w.errorEmitted) {
return;
}
w.errorEmitted = true;
stream.emit("error", err);
}
}
}
function construct(stream: Writable, cb: (error: Error) => void) {
if (!stream._construct) {
return;
}
stream.once(kConstruct, cb);
const w = stream._writableState;
w.constructed = false;
queueMicrotask(() => {
let called = false;
stream._construct?.((err) => {
w.constructed = true;
if (called) {
err = new ERR_MULTIPLE_CALLBACK();
} else {
called = true;
}
if (w.destroyed) {
stream.emit(kDestroy, err);
} else if (err) {
errorOrDestroy(stream, err, true);
} else {
queueMicrotask(() => {
stream.emit(kConstruct);
});
}
});
});
}
//TODO(Soremwar)
//Bring encodings in
function writeOrBuffer(
stream: Writable,
state: WritableState,
// deno-lint-ignore no-explicit-any
chunk: any,
encoding: string,
callback: (error: Error) => void,
) {
const len = state.objectMode ? 1 : chunk.length;
state.length += len;
if (state.writing || state.corked || state.errored || !state.constructed) {
state.buffered.push({ chunk, encoding, callback });
if (state.allBuffers && encoding !== "buffer") {
state.allBuffers = false;
}
if (state.allNoop && callback !== nop) {
state.allNoop = false;
}
} else {
state.writelen = len;
state.writecb = callback;
state.writing = true;
state.sync = true;
stream._write(chunk, encoding, state.onwrite);
state.sync = false;
}
const ret = state.length < state.highWaterMark;
if (!ret) {
state.needDrain = true;
}
return ret && !state.errored && !state.destroyed;
}
//TODO(Soremwar)
//Bring encodings in
function doWrite(
stream: Writable,
state: WritableState,
writev: boolean,
len: number,
// deno-lint-ignore no-explicit-any
chunk: any,
encoding: string,
cb: (error: Error) => void,
) {
state.writelen = len;
state.writecb = cb;
state.writing = true;
state.sync = true;
if (state.destroyed) {
state.onwrite(new ERR_STREAM_DESTROYED("write"));
} else if (writev) {
(stream._writev as unknown as write_v)(chunk, state.onwrite);
} else {
stream._write(chunk, encoding, state.onwrite);
}
state.sync = false;
}
function onwriteError(
stream: Writable,
state: WritableState,
er: Error,
cb: (error: Error) => void,
) {
--state.pendingcb;
cb(er);
errorBuffer(state);
errorOrDestroy(stream, er);
}
function onwrite(stream: Writable, er?: Error | null) {
const state = stream._writableState;
const sync = state.sync;
const cb = state.writecb;
if (typeof cb !== "function") {
errorOrDestroy(stream, new ERR_MULTIPLE_CALLBACK());
return;
}
state.writing = false;
state.writecb = null;
state.length -= state.writelen;
state.writelen = 0;
if (er) {
// Avoid V8 leak, https://github.com/nodejs/node/pull/34103#issuecomment-652002364
er.stack;
if (!state.errored) {
state.errored = er;
}
if (sync) {
queueMicrotask(() => onwriteError(stream, state, er, cb));
} else {
onwriteError(stream, state, er, cb);
}
} else {
if (state.buffered.length > state.bufferedIndex) {
clearBuffer(stream, state);
}
if (sync) {
if (
state.afterWriteTickInfo !== null &&
state.afterWriteTickInfo.cb === cb
) {
state.afterWriteTickInfo.count++;
} else {
state.afterWriteTickInfo = {
count: 1,
cb: (cb as (error?: Error) => void),
stream,
state,
};
queueMicrotask(() =>
afterWriteTick(state.afterWriteTickInfo as AfterWriteTick)
);
}
} else {
afterWrite(stream, state, 1, cb as (error?: Error) => void);
}
}
}
function afterWriteTick({
cb,
count,
state,
stream,
}: AfterWriteTick) {
state.afterWriteTickInfo = null;
return afterWrite(stream, state, count, cb);
}
function afterWrite(
stream: Writable,
state: WritableState,
count: number,
cb: (error?: Error) => void,
) {
const needDrain = !state.ending && !stream.destroyed && state.length === 0 &&
state.needDrain;
if (needDrain) {
state.needDrain = false;
stream.emit("drain");
}
while (count-- > 0) {
state.pendingcb--;
cb();
}
if (state.destroyed) {
errorBuffer(state);
}
finishMaybe(stream, state);
}
/** If there's something in the buffer waiting, then invoke callbacks.*/
function errorBuffer(state: WritableState) {
if (state.writing) {
return;
}
for (let n = state.bufferedIndex; n < state.buffered.length; ++n) {
const { chunk, callback } = state.buffered[n];
const len = state.objectMode ? 1 : chunk.length;
state.length -= len;
callback(new ERR_STREAM_DESTROYED("write"));
}
for (const callback of state[kOnFinished].splice(0)) {
callback(new ERR_STREAM_DESTROYED("end"));
}
resetBuffer(state);
}
/** If there's something in the buffer waiting, then process it.*/
function clearBuffer(stream: Writable, state: WritableState) {
if (
state.corked ||
state.bufferProcessing ||
state.destroyed ||
!state.constructed
) {
return;
}
const { buffered, bufferedIndex, objectMode } = state;
const bufferedLength = buffered.length - bufferedIndex;
if (!bufferedLength) {
return;
}
const i = bufferedIndex;
state.bufferProcessing = true;
if (bufferedLength > 1 && stream._writev) {
state.pendingcb -= bufferedLength - 1;
const callback = state.allNoop ? nop : (err: Error) => {
for (let n = i; n < buffered.length; ++n) {
buffered[n].callback(err);
}
};
const chunks = state.allNoop && i === 0 ? buffered : buffered.slice(i);
doWrite(stream, state, true, state.length, chunks, "", callback);
resetBuffer(state);
} else {
do {
const { chunk, encoding, callback } = buffered[i];
const len = objectMode ? 1 : chunk.length;
doWrite(stream, state, false, len, chunk, encoding, callback);
} while (i < buffered.length && !state.writing);
if (i === buffered.length) {
resetBuffer(state);
} else if (i > 256) {
buffered.splice(0, i);
state.bufferedIndex = 0;
} else {
state.bufferedIndex = i;
}
}
state.bufferProcessing = false;
}
function finish(stream: Writable, state: WritableState) {
state.pendingcb--;
if (state.errorEmitted || state.closeEmitted) {
return;
}
state.finished = true;
for (const callback of state[kOnFinished].splice(0)) {
callback();
}
stream.emit("finish");
if (state.autoDestroy) {
stream.destroy();
}
}
function finishMaybe(stream: Writable, state: WritableState, sync?: boolean) {
if (needFinish(state)) {
prefinish(stream, state);
if (state.pendingcb === 0 && needFinish(state)) {
state.pendingcb++;
if (sync) {
queueMicrotask(() => finish(stream, state));
} else {
finish(stream, state);
}
}
}
}
function prefinish(stream: Writable, state: WritableState) {
if (!state.prefinished && !state.finalCalled) {
if (typeof stream._final === "function" && !state.destroyed) {
state.finalCalled = true;
state.sync = true;
state.pendingcb++;
stream._final((err) => {
state.pendingcb--;
if (err) {
for (const callback of state[kOnFinished].splice(0)) {
callback(err);
}
errorOrDestroy(stream, err, state.sync);
} else if (needFinish(state)) {
state.prefinished = true;
stream.emit("prefinish");
state.pendingcb++;
queueMicrotask(() => finish(stream, state));
}
});
state.sync = false;
} else {
state.prefinished = true;
stream.emit("prefinish");
}
}
}
function needFinish(state: WritableState) {
return (state.ending &&
state.constructed &&
state.length === 0 &&
!state.errored &&
state.buffered.length === 0 &&
!state.finished &&
!state.writing);
}
interface WritableOptions {
autoDestroy?: boolean;
decodeStrings?: boolean;
//TODO(Soremwar)
//Bring encodings in
defaultEncoding?: string;
destroy?(
this: Writable,
error: Error | null,
callback: (error: Error | null) => void,
): void;
emitClose?: boolean;
final?(this: Writable, callback: (error?: Error | null) => void): void;
highWaterMark?: number;
objectMode?: boolean;
//TODO(Soremwar)
//Bring encodings in
write?(
this: Writable,
// deno-lint-ignore no-explicit-any
chunk: any,
encoding: string,
callback: (error?: Error | null) => void,
): void;
//TODO(Soremwar)
//Bring encodings in
writev?(
this: Writable,
// deno-lint-ignore no-explicit-any
chunks: Array<{ chunk: any; encoding: string }>,
callback: (error?: Error | null) => void,
): void;
}
class WritableState {
[kOnFinished]: Array<(error?: Error) => void> = [];
afterWriteTickInfo: null | AfterWriteTick = null;
allBuffers = true;
allNoop = true;
autoDestroy: boolean;
//TODO(Soremwar)
//Bring in encodings
buffered: Array<{
allBuffers?: boolean;
// deno-lint-ignore no-explicit-any
chunk: any;
encoding: string;
callback: (error: Error) => void;
}> = [];
bufferedIndex = 0;
bufferProcessing = false;
closed = false;
closeEmitted = false;
constructed: boolean;
corked = 0;
decodeStrings: boolean;
defaultEncoding: string;
destroyed = false;
emitClose: boolean;
ended = false;
ending = false;
errored: Error | null = null;
errorEmitted = false;
finalCalled = false;
finished = false;
highWaterMark: number;
length = 0;
needDrain = false;
objectMode: boolean;
onwrite: (error?: Error | null) => void;
pendingcb = 0;
prefinished = false;
sync = true;
writecb: null | ((error: Error) => void) = null;
writable = true;
writelen = 0;
writing = false;
constructor(options: WritableOptions | undefined, stream: Writable) {
this.objectMode = !!options?.objectMode;
this.highWaterMark = options?.highWaterMark ??
(this.objectMode ? 16 : 16 * 1024);
if (Number.isInteger(this.highWaterMark) && this.highWaterMark >= 0) {
this.highWaterMark = Math.floor(this.highWaterMark);
} else {
throw new ERR_INVALID_OPT_VALUE("highWaterMark", this.highWaterMark);
}
this.decodeStrings = !options?.decodeStrings === false;
this.defaultEncoding = options?.defaultEncoding || "utf8";
this.onwrite = onwrite.bind(undefined, stream);
resetBuffer(this);
this.emitClose = options?.emitClose ?? true;
this.autoDestroy = options?.autoDestroy ?? true;
this.constructed = true;
}
getBuffer() {
return this.buffered.slice(this.bufferedIndex);
}
get bufferedRequestCount() {
return this.buffered.length - this.bufferedIndex;
}
}
function resetBuffer(state: WritableState) {
state.buffered = [];
state.bufferedIndex = 0;
state.allBuffers = true;
state.allNoop = true;
}
/** A bit simpler than readable streams.
* Implement an async `._write(chunk, encoding, cb)`, and it'll handle all
* the drain event emission and buffering.
*/
class Writable extends Stream {
_construct?: (cb: (error?: Error) => void) => void;
_final?: (
this: Writable,
callback: (error?: Error | null | undefined) => void,
) => void;
_writableState: WritableState;
_writev?: write_v | null = null;
constructor(options?: WritableOptions) {
super();
this._writableState = new WritableState(options, this);
if (options) {
if (typeof options.write === "function") {
this._write = options.write;
}
if (typeof options.writev === "function") {
this._writev = options.writev;
}
if (typeof options.destroy === "function") {
this._destroy = options.destroy;
}
if (typeof options.final === "function") {
this._final = options.final;
}
}
construct(this, () => {
const state = this._writableState;
if (!state.writing) {
clearBuffer(this, state);
}
finishMaybe(this, state);
});
}
[captureRejectionSymbol](err?: Error) {
this.destroy(err);
}
static WritableState = WritableState;
get destroyed() {
return this._writableState ? this._writableState.destroyed : false;
}
set destroyed(value) {
if (this._writableState) {
this._writableState.destroyed = value;
}
}
get writable() {
const w = this._writableState;
return !w.destroyed && !w.errored && !w.ending && !w.ended;
}
set writable(val) {
if (this._writableState) {
this._writableState.writable = !!val;
}
}
get writableFinished() {
return this._writableState ? this._writableState.finished : false;
}
get writableObjectMode() {
return this._writableState ? this._writableState.objectMode : false;
}
get writableBuffer() {
return this._writableState && this._writableState.getBuffer();
}
get writableEnded() {
return this._writableState ? this._writableState.ending : false;
}
get writableHighWaterMark() {
return this._writableState && this._writableState.highWaterMark;
}
get writableCorked() {
return this._writableState ? this._writableState.corked : 0;
}
get writableLength() {
return this._writableState && this._writableState.length;
}
_undestroy() {
const w = this._writableState;
w.constructed = true;
w.destroyed = false;
w.closed = false;
w.closeEmitted = false;
w.errored = null;
w.errorEmitted = false;
w.ended = false;
w.ending = false;
w.finalCalled = false;
w.prefinished = false;
w.finished = false;
}
_destroy(err: Error | null, cb: (error?: Error | null) => void) {
cb(err);
}
destroy(err?: Error, cb?: () => void) {
const state = this._writableState;
if (!state.destroyed) {
queueMicrotask(() => errorBuffer(state));
}
destroy.call(this, err, cb);
return this;
}
end(cb?: () => void): void;
// deno-lint-ignore no-explicit-any
end(chunk: any, cb?: () => void): void;
//TODO(Soremwar)
//Bring in encodings
// deno-lint-ignore no-explicit-any
end(chunk: any, encoding: string, cb?: () => void): void;
end(
// deno-lint-ignore no-explicit-any
x?: any | (() => void),
//TODO(Soremwar)
//Bring in encodings
y?: string | (() => void),
z?: () => void,
) {
const state = this._writableState;
// deno-lint-ignore no-explicit-any
let chunk: any | null;
//TODO(Soremwar)
//Bring in encodings
let encoding: string | null;
let cb: undefined | ((error?: Error) => void);
if (typeof x === "function") {
chunk = null;
encoding = null;
cb = x;
} else if (typeof y === "function") {
chunk = x;
encoding = null;
cb = y;
} else {
chunk = x;
encoding = y as string;
cb = z;
}
if (chunk !== null && chunk !== undefined) {
this.write(chunk, encoding);
}
if (state.corked) {
state.corked = 1;
this.uncork();
}
let err: Error | undefined;
if (!state.errored && !state.ending) {
state.ending = true;
finishMaybe(this, state, true);
state.ended = true;
} else if (state.finished) {
err = new ERR_STREAM_ALREADY_FINISHED("end");
} else if (state.destroyed) {
err = new ERR_STREAM_DESTROYED("end");
}
if (typeof cb === "function") {
if (err || state.finished) {
queueMicrotask(() => {
(cb as (error?: Error | undefined) => void)(err);
});
} else {
state[kOnFinished].push(cb);
}
}
return this;
}
//TODO(Soremwar)
//Bring in encodings
_write(
// deno-lint-ignore no-explicit-any
chunk: any,
encoding: string,
cb: (error?: Error | null) => void,
): void {
if (this._writev) {
this._writev([{ chunk, encoding }], cb);
} else {
throw new ERR_METHOD_NOT_IMPLEMENTED("_write()");
}
}
//This signature was changed to keep inheritance coherent
pipe(dest: Writable): Writable {
errorOrDestroy(this, new ERR_STREAM_CANNOT_PIPE());
return dest;
}
// deno-lint-ignore no-explicit-any
write(chunk: any, cb?: (error: Error | null | undefined) => void): boolean;
//TODO(Soremwar)
//Bring in encodings
write(
// deno-lint-ignore no-explicit-any
chunk: any,
encoding: string | null,
cb?: (error: Error | null | undefined) => void,
): boolean;
//TODO(Soremwar)
//Bring in encodings
write(
// deno-lint-ignore no-explicit-any
chunk: any,
x?: string | null | ((error: Error | null | undefined) => void),
y?: ((error: Error | null | undefined) => void),
) {
const state = this._writableState;
//TODO(Soremwar)
//Bring in encodings
let encoding: string;
let cb: (error?: Error | null) => void;
if (typeof x === "function") {
cb = x;
encoding = state.defaultEncoding;
} else {
if (!x) {
encoding = state.defaultEncoding;
} else if (x !== "buffer" && !Buffer.isEncoding(x)) {
throw new ERR_UNKNOWN_ENCODING(x);
} else {
encoding = x;
}
if (typeof y !== "function") {
cb = nop;
} else {
cb = y;
}
}
if (chunk === null) {
throw new ERR_STREAM_NULL_VALUES();
} else if (!state.objectMode) {
if (typeof chunk === "string") {
if (state.decodeStrings !== false) {
chunk = Buffer.from(chunk, encoding);
encoding = "buffer";
}
} else if (chunk instanceof Buffer) {
encoding = "buffer";
} else if (Stream._isUint8Array(chunk)) {
chunk = Stream._uint8ArrayToBuffer(chunk);
encoding = "buffer";
} else {
throw new ERR_INVALID_ARG_TYPE(
"chunk",
["string", "Buffer", "Uint8Array"],
chunk,
);
}
}
let err: Error | undefined;
if (state.ending) {
err = new ERR_STREAM_WRITE_AFTER_END();
} else if (state.destroyed) {
err = new ERR_STREAM_DESTROYED("write");
}
if (err) {
queueMicrotask(() => cb(err));
errorOrDestroy(this, err, true);
return false;
}
state.pendingcb++;
return writeOrBuffer(this, state, chunk, encoding, cb);
}
cork() {
this._writableState.corked++;
}
uncork() {
const state = this._writableState;
if (state.corked) {
state.corked--;
if (!state.writing) {
clearBuffer(this, state);
}
}
}
//TODO(Soremwar)
//Bring allowed encodings
setDefaultEncoding(encoding: string) {
// node::ParseEncoding() requires lower case.
if (typeof encoding === "string") {
encoding = encoding.toLowerCase();
}
if (!Buffer.isEncoding(encoding)) {
throw new ERR_UNKNOWN_ENCODING(encoding);
}
this._writableState.defaultEncoding = encoding;
return this;
}
}
export default Writable;
export { WritableState };

View file

@ -0,0 +1,209 @@
// Copyright Node.js contributors. All rights reserved. MIT License.
import { Buffer } from "../buffer.ts";
import finished from "./end-of-stream.ts";
import Writable from "../_stream/writable.ts";
import { deferred } from "../../async/mod.ts";
import {
assert,
assertEquals,
assertStrictEquals,
assertThrows,
} from "../../testing/asserts.ts";
Deno.test("Writable stream writes correctly", async () => {
let callback: undefined | ((error?: Error | null | undefined) => void);
let writeExecuted = 0;
const writeExecutedExpected = 1;
const writeExpectedExecutions = deferred();
let writevExecuted = 0;
const writevExecutedExpected = 1;
const writevExpectedExecutions = deferred();
const writable = new Writable({
write: (chunk, encoding, cb) => {
writeExecuted++;
if (writeExecuted == writeExecutedExpected) {
writeExpectedExecutions.resolve();
}
assert(chunk instanceof Buffer);
assertStrictEquals(encoding, "buffer");
assertStrictEquals(String(chunk), "ABC");
callback = cb;
},
writev: (chunks) => {
writevExecuted++;
if (writevExecuted == writevExecutedExpected) {
writevExpectedExecutions.resolve();
}
assertStrictEquals(chunks.length, 2);
assertStrictEquals(chunks[0].encoding, "buffer");
assertStrictEquals(chunks[1].encoding, "buffer");
assertStrictEquals(chunks[0].chunk + chunks[1].chunk, "DEFGHI");
},
});
writable.write(new TextEncoder().encode("ABC"));
writable.write(new TextEncoder().encode("DEF"));
writable.end(new TextEncoder().encode("GHI"));
callback?.();
const writeTimeout = setTimeout(
() => writeExpectedExecutions.reject(),
1000,
);
const writevTimeout = setTimeout(
() => writevExpectedExecutions.reject(),
1000,
);
await writeExpectedExecutions;
await writevExpectedExecutions;
clearTimeout(writeTimeout);
clearTimeout(writevTimeout);
assertEquals(writeExecuted, writeExecutedExpected);
assertEquals(writevExecuted, writevExecutedExpected);
});
Deno.test("Writable stream writes Uint8Array in object mode", async () => {
let writeExecuted = 0;
const writeExecutedExpected = 1;
const writeExpectedExecutions = deferred();
const ABC = new TextEncoder().encode("ABC");
const writable = new Writable({
objectMode: true,
write: (chunk, encoding, cb) => {
writeExecuted++;
if (writeExecuted == writeExecutedExpected) {
writeExpectedExecutions.resolve();
}
assert(!(chunk instanceof Buffer));
assert(chunk instanceof Uint8Array);
assertEquals(chunk, ABC);
assertEquals(encoding, "utf8");
cb();
},
});
writable.end(ABC);
const writeTimeout = setTimeout(
() => writeExpectedExecutions.reject(),
1000,
);
await writeExpectedExecutions;
clearTimeout(writeTimeout);
assertEquals(writeExecuted, writeExecutedExpected);
});
Deno.test("Writable stream throws on unexpected close", async () => {
let finishedExecuted = 0;
const finishedExecutedExpected = 1;
const finishedExpectedExecutions = deferred();
const writable = new Writable({
write: () => {},
});
writable.writable = false;
writable.destroy();
finished(writable, (err) => {
finishedExecuted++;
if (finishedExecuted == finishedExecutedExpected) {
finishedExpectedExecutions.resolve();
}
assertEquals(err?.code, "ERR_STREAM_PREMATURE_CLOSE");
});
const finishedTimeout = setTimeout(
() => finishedExpectedExecutions.reject(),
1000,
);
await finishedExpectedExecutions;
clearTimeout(finishedTimeout);
assertEquals(finishedExecuted, finishedExecutedExpected);
});
Deno.test("Writable stream finishes correctly", async () => {
let finishedExecuted = 0;
const finishedExecutedExpected = 1;
const finishedExpectedExecutions = deferred();
const w = new Writable({
write(_chunk, _encoding, cb) {
cb();
},
autoDestroy: false,
});
w.end("asd");
queueMicrotask(() => {
finished(w, () => {
finishedExecuted++;
if (finishedExecuted == finishedExecutedExpected) {
finishedExpectedExecutions.resolve();
}
});
});
const finishedTimeout = setTimeout(
() => finishedExpectedExecutions.reject(),
1000,
);
await finishedExpectedExecutions;
clearTimeout(finishedTimeout);
assertEquals(finishedExecuted, finishedExecutedExpected);
});
Deno.test("Writable stream finishes correctly after error", async () => {
let errorExecuted = 0;
const errorExecutedExpected = 1;
const errorExpectedExecutions = deferred();
let finishedExecuted = 0;
const finishedExecutedExpected = 1;
const finishedExpectedExecutions = deferred();
const w = new Writable({
write(_chunk, _encoding, cb) {
cb(new Error());
},
autoDestroy: false,
});
w.write("asd");
w.on("error", () => {
errorExecuted++;
if (errorExecuted == errorExecutedExpected) {
errorExpectedExecutions.resolve();
}
finished(w, () => {
finishedExecuted++;
if (finishedExecuted == finishedExecutedExpected) {
finishedExpectedExecutions.resolve();
}
});
});
const errorTimeout = setTimeout(
() => errorExpectedExecutions.reject(),
1000,
);
const finishedTimeout = setTimeout(
() => finishedExpectedExecutions.reject(),
1000,
);
await finishedExpectedExecutions;
await errorExpectedExecutions;
clearTimeout(finishedTimeout);
clearTimeout(errorTimeout);
assertEquals(finishedExecuted, finishedExecutedExpected);
assertEquals(errorExecuted, errorExecutedExpected);
});
Deno.test("Writable stream fails on 'write' null value", () => {
const writable = new Writable();
assertThrows(() => writable.write(null));
});

View file

@ -1,15 +1,11 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
export { promisify } from "./_util/_util_promisify.ts"; export { promisify } from "./_util/_util_promisify.ts";
export { callbackify } from "./_util/_util_callbackify.ts"; export { callbackify } from "./_util/_util_callbackify.ts";
import { codes, errorMap } from "./_errors.ts"; import { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE, errorMap } from "./_errors.ts";
import * as types from "./_util/_util_types.ts"; import * as types from "./_util/_util_types.ts";
export { types }; export { types };
const NumberIsSafeInteger = Number.isSafeInteger; const NumberIsSafeInteger = Number.isSafeInteger;
const {
ERR_OUT_OF_RANGE,
ERR_INVALID_ARG_TYPE,
} = codes;
const DEFAULT_INSPECT_OPTIONS = { const DEFAULT_INSPECT_OPTIONS = {
showHidden: false, showHidden: false,

View file

@ -131,3 +131,15 @@ export function validateIntegerRange(
); );
} }
} }
type OptionalSpread<T> = T extends undefined ? []
: [T];
export function once(callback: (...args: OptionalSpread<undefined>) => void) {
let called = false;
return function (this: unknown, ...args: OptionalSpread<undefined>) {
if (called) return;
called = true;
callback.apply(this, args);
};
}

View file

@ -44,8 +44,7 @@ const {
keys: ObjectKeys, keys: ObjectKeys,
} = Object; } = Object;
import { codes } from "./_errors.ts"; import { ERR_INVALID_ARG_TYPE } from "./_errors.ts";
const { ERR_INVALID_ARG_TYPE } = codes;
let blue = ""; let blue = "";
let green = ""; let green = "";