Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

streams: implement TextEncoderStream and TextDecoderStream #39347

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions doc/api/webstreams.md
Original file line number Diff line number Diff line change
Expand Up @@ -1118,5 +1118,103 @@ added: REPLACEME
* `chunk` {any}
* Returns: {number}

### Class: `TextEncoderStream`
<!-- YAML
added: REPLACEME
-->

#### `new TextEncoderStream()`
<!-- YAML
added: REPLACEME
-->

Creates a new `TextEncoderStream` instance.
#### `textEncoderStream.encoding`
jasnell marked this conversation as resolved.
Show resolved Hide resolved
<!-- YAML
added: REPLACEME
-->

* Type: {string}

The encoding supported by the `TextEncoderStream` instance.

#### `textEncoderStream.readable`
<!-- YAML
added: REPLACEME
-->

* Type: {ReadableStream}

#### `textEncoderStream.writable`
<!-- YAML
added: REPLACEME
-->

* Type: {WritableStream}

### Class: `TextDecoderStream`
<!-- YAML
added: REPLACEME
-->

#### `new TextDecoderStream([encoding[, options]])`
<!-- YAML
added: REPLACEME
-->

* `encoding` {string} Identifies the `encoding` that this `TextDecoder` instance
supports. **Default:** `'utf-8'`.
* `options` {Object}
* `fatal` {boolean} `true` if decoding failures are fatal.
* `ignoreBOM` {boolean} When `true`, the `TextDecoderStream` will include the
byte order mark in the decoded result. When `false`, the byte order mark
will be removed from the output. This option is only used when `encoding` is
`'utf-8'`, `'utf-16be'` or `'utf-16le'`. **Default:** `false`.

Creates a new `TextDecoderStream` instance.

#### `textDecoderStream.encoding`
<!-- YAML
added: REPLACEME
-->

* Type: {string}

The encoding supported by the `TextDecoderStream` instance.

#### `textDecoderStream.fatal`
<!-- YAML
added: REPLACEME
-->

* Type: {boolean}

The value will be `true` if decoding errors result in a `TypeError` being
thrown.

#### `textDecoderStream.ignoreBOM`
<!-- YAML
added: REPLACEME
-->

* Type: {boolean}

The value will be `true` if the decoding result will include the byte order
mark.

#### `textDecoderStream.readable`
<!-- YAML
added: REPLACEME
-->

* Type: {ReadableStream}

#### `textDecoderStream.writable`
<!-- YAML
added: REPLACEME
-->

* Type: {WritableStream}

[Streams]: stream.md
[WHATWG Streams Standard]: https://streams.spec.whatwg.org/
236 changes: 236 additions & 0 deletions lib/internal/webstreams/encoding.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
'use strict';

const {
ObjectDefineProperties,
Symbol,
} = primordials;

const {
TextDecoder,
TextEncoder,
} = require('internal/encoding');

const {
TransformStream,
} = require('internal/webstreams/transformstream');

const {
kEnumerableProperty,
} = require('internal/webstreams/util');

const {
codes: {
ERR_INVALID_THIS,
},
} = require('internal/errors');

const {
inspect,
} = require('internal/util/inspect');

const {
customInspectSymbol: kInspect
} = require('internal/util');

const kHandle = Symbol('kHandle');
const kTransform = Symbol('kTransform');
const kType = Symbol('kType');

/**
* @typedef {import('./readablestream').ReadableStream} ReadableStream
* @typedef {import('./writablestream').WritableStream} WritableStream
*/

function isTextEncoderStream(value) {
return typeof value?.[kHandle] === 'object' &&
value?.[kType] === 'TextEncoderStream';
}

function isTextDecoderStream(value) {
return typeof value?.[kHandle] === 'object' &&
value?.[kType] === 'TextDecoderStream';
}

class TextEncoderStream {
constructor() {
this[kType] = 'TextEncoderStream';
this[kHandle] = new TextEncoder();
this[kTransform] = new TransformStream({
transform: (chunk, controller) => {
const value = this[kHandle].encode(chunk);
if (value)
controller.enqueue(value);
},
flush: (controller) => {
const value = this[kHandle].encode();
if (value.byteLength > 0)
controller.enqueue(value);
controller.terminate();
},
});
}

/**
* @readonly
* @type {string}
*/
get encoding() {
if (!isTextEncoderStream(this))
throw new ERR_INVALID_THIS('TextEncoderStream');
return this[kHandle].encoding;
}

/**
* @readonly
* @type {ReadableStream}
*/
get readable() {
if (!isTextEncoderStream(this))
throw new ERR_INVALID_THIS('TextEncoderStream');
return this[kTransform].readable;
}

/**
* @readonly
* @type {WritableStream}
*/
get writable() {
if (!isTextEncoderStream(this))
throw new ERR_INVALID_THIS('TextEncoderStream');
return this[kTransform].writable;
}

[kInspect](depth, options) {
if (!isTextEncoderStream(this))
throw new ERR_INVALID_THIS('TextEncoderStream');
if (depth < 0)
return this;

const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1
};

return `${this[kType]} ${inspect({
encoding: this[kHandle].encoding,
readable: this[kTransform].readable,
writable: this[kTransform].writable,
}, opts)}`;
}
}

class TextDecoderStream {
/**
* @param {string} [encoding]
* @param {{
* fatal? : boolean,
* ignoreBOM? : boolean,
* }} [options]
*/
constructor(encoding = 'utf-8', options = {}) {
this[kType] = 'TextDecoderStream';
this[kHandle] = new TextDecoder(encoding, options);
this[kTransform] = new TransformStream({
transform: (chunk, controller) => {
const value = this[kHandle].decode(chunk, { stream: true });
if (value)
controller.enqueue(value);
},
flush: (controller) => {
const value = this[kHandle].decode();
if (value)
controller.enqueue(value);
controller.terminate();
},
});
}

/**
* @readonly
* @type {string}
*/
get encoding() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kHandle].encoding;
}

/**
* @readonly
* @type {boolean}
*/
get fatal() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kHandle].fatal;
}

/**
* @readonly
* @type {boolean}
*/
get ignoreBOM() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kHandle].ignoreBOM;
}

/**
* @readonly
* @type {ReadableStream}
*/
get readable() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kTransform].readable;
}

/**
* @readonly
* @type {WritableStream}
*/
get writable() {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
return this[kTransform].writable;
}

[kInspect](depth, options) {
if (!isTextDecoderStream(this))
throw new ERR_INVALID_THIS('TextDecoderStream');
if (depth < 0)
return this;

const opts = {
...options,
depth: options.depth == null ? null : options.depth - 1
};

return `${this[kType]} ${inspect({
encoding: this[kHandle].encoding,
fatal: this[kHandle].fatal,
ignoreBOM: this[kHandle].ignoreBOM,
readable: this[kTransform].readable,
writable: this[kTransform].writable,
}, opts)}`;
}
}

ObjectDefineProperties(TextEncoderStream.prototype, {
encoding: kEnumerableProperty,
readable: kEnumerableProperty,
writable: kEnumerableProperty,
});

ObjectDefineProperties(TextDecoderStream.prototype, {
encoding: kEnumerableProperty,
fatal: kEnumerableProperty,
ignoreBOM: kEnumerableProperty,
readable: kEnumerableProperty,
writable: kEnumerableProperty,
});

module.exports = {
TextEncoderStream,
TextDecoderStream,
};
7 changes: 7 additions & 0 deletions lib/stream/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const {
CountQueuingStrategy,
} = require('internal/webstreams/queuingstrategies');

const {
TextEncoderStream,
TextDecoderStream,
} = require('internal/webstreams/encoding');

module.exports = {
ReadableStream,
ReadableStreamDefaultReader,
Expand All @@ -45,4 +50,6 @@ module.exports = {
WritableStreamDefaultController,
ByteLengthQueuingStrategy,
CountQueuingStrategy,
TextEncoderStream,
TextDecoderStream,
};
Loading