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:
parent
ce890f2ae7
commit
a4f27c4d57
16 changed files with 4217 additions and 53 deletions
|
@ -1,52 +1,99 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a
|
||||
// copy of this software and associated documentation files (the
|
||||
// "Software"), to deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify, merge, publish,
|
||||
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
// persons to whom the Software is furnished to do so, subject to the
|
||||
// following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
// Copyright Node.js contributors. All rights reserved. MIT License.
|
||||
/************ NOT IMPLEMENTED
|
||||
* ERR_INVALID_ARG_VALUE
|
||||
* ERR_INVALID_MODULE_SPECIFIER
|
||||
* ERR_INVALID_PACKAGE_TARGET
|
||||
* ERR_INVALID_URL_SCHEME
|
||||
* ERR_MANIFEST_ASSERT_INTEGRITY
|
||||
* ERR_MISSING_ARGS
|
||||
* ERR_MODULE_NOT_FOUND
|
||||
* ERR_PACKAGE_PATH_NOT_EXPORTED
|
||||
* ERR_QUICSESSION_VERSION_NEGOTIATION
|
||||
* ERR_REQUIRE_ESM
|
||||
* ERR_SOCKET_BAD_PORT
|
||||
* ERR_TLS_CERT_ALTNAME_INVALID
|
||||
* ERR_UNHANDLED_ERROR
|
||||
* ERR_WORKER_INVALID_EXEC_ARGV
|
||||
* ERR_WORKER_PATH
|
||||
* ERR_QUIC_ERROR
|
||||
* ERR_SOCKET_BUFFER_SIZE //System error, shouldn't ever happen inside Deno
|
||||
* ERR_SYSTEM_ERROR //System error, shouldn't ever happen inside Deno
|
||||
* ERR_TTY_INIT_FAILED //System error, shouldn't ever happen inside Deno
|
||||
* ERR_INVALID_PACKAGE_CONFIG // package.json stuff, probably useless
|
||||
*************/
|
||||
|
||||
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
|
||||
// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L299
|
||||
// Ref: https://github.com/nodejs/node/blob/50d28d4b3a616b04537feff014aa70437f064e30/lib/internal/errors.js#L325
|
||||
// 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.
|
||||
constructor(name: string, code: string, message: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
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";
|
||||
|
||||
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:
|
||||
// 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
|
||||
|
@ -342,3 +384,100 @@ export const errorMap = new Map<number, [string, string]>(
|
|||
? linux
|
||||
: 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}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
260
std/node/_stream/async_iterator.ts
Normal file
260
std/node/_stream/async_iterator.ts
Normal 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;
|
249
std/node/_stream/async_iterator_test.ts
Normal file
249
std/node/_stream/async_iterator_test.ts
Normal 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);
|
||||
});
|
183
std/node/_stream/buffer_list.ts
Normal file
183
std/node/_stream/buffer_list.ts
Normal 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;
|
||||
}
|
||||
}
|
3
std/node/_stream/duplex.ts
Normal file
3
std/node/_stream/duplex.ts
Normal 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[]) => {};
|
240
std/node/_stream/end-of-stream.ts
Normal file
240
std/node/_stream/end-of-stream.ts
Normal 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
102
std/node/_stream/from.ts
Normal 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
1248
std/node/_stream/readable.ts
Normal file
File diff suppressed because it is too large
Load diff
489
std/node/_stream/readable_test.ts
Normal file
489
std/node/_stream/readable_test.ts
Normal 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);
|
||||
});
|
79
std/node/_stream/stream.ts
Normal file
79
std/node/_stream/stream.ts
Normal 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;
|
4
std/node/_stream/symbols.ts
Normal file
4
std/node/_stream/symbols.ts
Normal 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");
|
952
std/node/_stream/writable.ts
Normal file
952
std/node/_stream/writable.ts
Normal 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 };
|
209
std/node/_stream/writable_test.ts
Normal file
209
std/node/_stream/writable_test.ts
Normal 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));
|
||||
});
|
|
@ -1,15 +1,11 @@
|
|||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
export { promisify } from "./_util/_util_promisify.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";
|
||||
export { types };
|
||||
|
||||
const NumberIsSafeInteger = Number.isSafeInteger;
|
||||
const {
|
||||
ERR_OUT_OF_RANGE,
|
||||
ERR_INVALID_ARG_TYPE,
|
||||
} = codes;
|
||||
|
||||
const DEFAULT_INSPECT_OPTIONS = {
|
||||
showHidden: false,
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -44,8 +44,7 @@ const {
|
|||
keys: ObjectKeys,
|
||||
} = Object;
|
||||
|
||||
import { codes } from "./_errors.ts";
|
||||
const { ERR_INVALID_ARG_TYPE } = codes;
|
||||
import { ERR_INVALID_ARG_TYPE } from "./_errors.ts";
|
||||
|
||||
let blue = "";
|
||||
let green = "";
|
||||
|
|
Loading…
Reference in a new issue