mirror of
https://github.com/nodejs/node.git
synced 2026-10-29 02:35:20 +08:00
The encode-and-enqueue transform walked the chunk code unit by code
unit, materializing a single-character string per index and building
the output with string concatenation. The only state that crosses
chunks is a trailing high (leading) surrogate, and TextEncoder.encode's
USVString conversion already replaces every interior lone surrogate
with U+FFFD, which is exactly what the spec loop produces. Join a
pending high surrogate with the incoming chunk, hold back a new
trailing high surrogate, and encode the rest in a single native call.
The streaming decode path also reuses a single options object instead
of allocating { stream: true } per chunk.
An encoding-streams benchmark is added since the suite had no
TextEncoderStream/TextDecoderStream row. Encoding improves by ~546%
with 1KB string chunks and ~20% with 16-character chunks; decode is
unchanged.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/65414
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Mattias Buelens <mattias@buelens.com>
220 lines
5.0 KiB
JavaScript
220 lines
5.0 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
ObjectDefineProperties,
|
|
String,
|
|
StringPrototypeCharCodeAt,
|
|
StringPrototypeSlice,
|
|
Uint8Array,
|
|
} = primordials;
|
|
|
|
const {
|
|
TextDecoder,
|
|
TextEncoder,
|
|
} = require('internal/encoding');
|
|
|
|
const {
|
|
TransformStream,
|
|
} = require('internal/webstreams/transformstream');
|
|
|
|
const { customInspect } = require('internal/webstreams/util');
|
|
|
|
const {
|
|
codes: {
|
|
ERR_INVALID_ARG_TYPE,
|
|
ERR_INVALID_THIS,
|
|
},
|
|
} = require('internal/errors');
|
|
|
|
const {
|
|
customInspectSymbol: kInspect,
|
|
kEmptyObject,
|
|
kEnumerableProperty,
|
|
} = require('internal/util');
|
|
|
|
// Shared per-chunk decode options; decode() only reads the flag.
|
|
const kDecodeStreamingOptions = { __proto__: null, stream: true };
|
|
|
|
/**
|
|
* @typedef {import('./readablestream').ReadableStream} ReadableStream
|
|
* @typedef {import('./writablestream').WritableStream} WritableStream
|
|
*/
|
|
|
|
class TextEncoderStream {
|
|
#pendingHighSurrogate = null;
|
|
#handle;
|
|
#transform;
|
|
|
|
constructor() {
|
|
this.#handle = new TextEncoder();
|
|
this.#transform = new TransformStream({
|
|
transform: (chunk, controller) => {
|
|
// https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk
|
|
// The only cross-chunk state is a trailing high surrogate;
|
|
// encode() replaces interior lone surrogates with U+FFFD exactly
|
|
// like the spec's per-code-unit walk.
|
|
chunk = String(chunk);
|
|
if (chunk.length === 0)
|
|
return;
|
|
if (this.#pendingHighSurrogate !== null) {
|
|
chunk = this.#pendingHighSurrogate + chunk;
|
|
this.#pendingHighSurrogate = null;
|
|
}
|
|
const lastCodeUnit =
|
|
StringPrototypeCharCodeAt(chunk, chunk.length - 1);
|
|
if (0xD800 <= lastCodeUnit && lastCodeUnit <= 0xDBFF) {
|
|
this.#pendingHighSurrogate =
|
|
StringPrototypeSlice(chunk, -1);
|
|
chunk = StringPrototypeSlice(chunk, 0, -1);
|
|
if (chunk.length === 0)
|
|
return;
|
|
}
|
|
controller.enqueue(this.#handle.encode(chunk));
|
|
},
|
|
flush: (controller) => {
|
|
// https://encoding.spec.whatwg.org/#encode-and-flush
|
|
if (this.#pendingHighSurrogate !== null) {
|
|
controller.enqueue(new Uint8Array([0xEF, 0xBF, 0xBD]));
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {string}
|
|
*/
|
|
get encoding() {
|
|
return this.#handle.encoding;
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {ReadableStream}
|
|
*/
|
|
get readable() {
|
|
return this.#transform.readable;
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {WritableStream}
|
|
*/
|
|
get writable() {
|
|
return this.#transform.writable;
|
|
}
|
|
|
|
[kInspect](depth, options) {
|
|
if (this == null)
|
|
throw new ERR_INVALID_THIS('TextEncoderStream');
|
|
return customInspect(depth, options, 'TextEncoderStream', {
|
|
encoding: this.#handle.encoding,
|
|
readable: this.#transform.readable,
|
|
writable: this.#transform.writable,
|
|
});
|
|
}
|
|
}
|
|
|
|
class TextDecoderStream {
|
|
#handle;
|
|
#transform;
|
|
|
|
/**
|
|
* @param {string} [encoding]
|
|
* @param {{
|
|
* fatal? : boolean,
|
|
* ignoreBOM? : boolean,
|
|
* }} [options]
|
|
*/
|
|
constructor(encoding = 'utf-8', options = kEmptyObject) {
|
|
this.#handle = new TextDecoder(encoding, options);
|
|
this.#transform = new TransformStream({
|
|
transform: (chunk, controller) => {
|
|
if (chunk === undefined) {
|
|
throw new ERR_INVALID_ARG_TYPE('chunk', 'string', chunk);
|
|
}
|
|
const value = this.#handle.decode(chunk, kDecodeStreamingOptions);
|
|
if (value)
|
|
controller.enqueue(value);
|
|
},
|
|
flush: (controller) => {
|
|
const value = this.#handle.decode();
|
|
if (value)
|
|
controller.enqueue(value);
|
|
controller.terminate();
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {string}
|
|
*/
|
|
get encoding() {
|
|
return this.#handle.encoding;
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {boolean}
|
|
*/
|
|
get fatal() {
|
|
return this.#handle.fatal;
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {boolean}
|
|
*/
|
|
get ignoreBOM() {
|
|
return this.#handle.ignoreBOM;
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {ReadableStream}
|
|
*/
|
|
get readable() {
|
|
return this.#transform.readable;
|
|
}
|
|
|
|
/**
|
|
* @readonly
|
|
* @type {WritableStream}
|
|
*/
|
|
get writable() {
|
|
return this.#transform.writable;
|
|
}
|
|
|
|
[kInspect](depth, options) {
|
|
if (this == null)
|
|
throw new ERR_INVALID_THIS('TextDecoderStream');
|
|
return customInspect(depth, options, 'TextDecoderStream', {
|
|
encoding: this.#handle.encoding,
|
|
fatal: this.#handle.fatal,
|
|
ignoreBOM: this.#handle.ignoreBOM,
|
|
readable: this.#transform.readable,
|
|
writable: this.#transform.writable,
|
|
});
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|