Skip to content

Commit 48fae8f

Browse files
authored
feat(encoding/unstable): add options argument to hex streaming & performance (#6453)
1 parent 922c2f4 commit 48fae8f

File tree

3 files changed

+138
-55
lines changed

3 files changed

+138
-55
lines changed

_tools/check_docs.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const ENTRY_POINTS = [
4848
"../datetime/mod.ts",
4949
"../dotenv/mod.ts",
5050
"../encoding/mod.ts",
51+
"../encoding/unstable_hex.ts",
5152
"../encoding/unstable_base32.ts",
5253
"../encoding/unstable_base64.ts",
5354
"../encoding/unstable_hex.ts",

encoding/unstable_hex_stream.ts

Lines changed: 95 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,96 +2,151 @@
22
// This module is browser compatible.
33

44
/**
5-
* Utilities for encoding and decoding to and from hex in a streaming manner.
5+
* TransformStream classes to encode and decode to and from hexadecimal data in a streaming manner.
66
*
77
* ```ts
88
* import { assertEquals } from "@std/assert";
9-
* import { HexDecoderStream } from "@std/encoding/unstable-hex-stream";
10-
* import { toText } from "@std/streams/to-text";
9+
* import { encodeHex } from "@std/encoding/unstable-hex";
10+
* import { HexEncoderStream } from "@std/encoding/unstable-hex-stream";
11+
* import { toText } from "@std/streams";
1112
*
12-
* const stream = ReadableStream.from(["48656c6c6f2c", "20776f726c6421"])
13-
* .pipeThrough(new HexDecoderStream())
14-
* .pipeThrough(new TextDecoderStream());
13+
* const readable = (await Deno.open("./deno.lock"))
14+
* .readable
15+
* .pipeThrough(new HexEncoderStream({ output: "string" }));
1516
*
16-
* assertEquals(await toText(stream), "Hello, world!");
17+
* assertEquals(
18+
* await toText(readable),
19+
* encodeHex(await Deno.readFile("./deno.lock")),
20+
* );
1721
* ```
1822
*
1923
* @experimental **UNSTABLE**: New API, yet to be vetted.
2024
*
2125
* @module
2226
*/
2327

24-
import { decodeHex, encodeHex } from "./hex.ts";
2528
import type { Uint8Array_ } from "./_types.ts";
2629
export type { Uint8Array_ };
30+
import {
31+
calcMax,
32+
decodeRawHex as decode,
33+
encodeRawHex as encode,
34+
} from "./unstable_hex.ts";
35+
import { detach } from "./_common_detach.ts";
36+
37+
type Expect<T> = T extends "bytes" ? Uint8Array_ : string;
2738

2839
/**
29-
* Converts a Uint8Array stream into a hex-encoded stream.
40+
* Transforms a {@linkcode Uint8Array<ArrayBuffer>} stream into a hexadecimal stream.
3041
*
3142
* @experimental **UNSTABLE**: New API, yet to be vetted.
3243
*
33-
* @see {@link https://www.rfc-editor.org/rfc/rfc4648.html#section-8}
44+
* @typeParam T The type of the hexadecimal stream.
3445
*
35-
* @example Usage
46+
* @example Basic Usage
3647
* ```ts
3748
* import { assertEquals } from "@std/assert";
38-
* import { encodeHex } from "@std/encoding/hex";
49+
* import { encodeHex } from "@std/encoding/unstable-hex";
3950
* import { HexEncoderStream } from "@std/encoding/unstable-hex-stream";
40-
* import { toText } from "@std/streams/to-text";
51+
* import { toText } from "@std/streams";
4152
*
42-
* const stream = ReadableStream.from(["Hello,", " world!"])
43-
* .pipeThrough(new TextEncoderStream())
44-
* .pipeThrough(new HexEncoderStream());
53+
* const readable = (await Deno.open("./deno.lock"))
54+
* .readable
55+
* .pipeThrough(new HexEncoderStream({ output: "string" }));
4556
*
46-
* assertEquals(await toText(stream), encodeHex(new TextEncoder().encode("Hello, world!")));
57+
* assertEquals(
58+
* await toText(readable),
59+
* encodeHex(await Deno.readFile("./deno.lock")),
60+
* );
4761
* ```
4862
*/
49-
export class HexEncoderStream extends TransformStream<Uint8Array, string> {
50-
constructor() {
63+
export class HexEncoderStream<T extends "string" | "bytes">
64+
extends TransformStream<
65+
Uint8Array_,
66+
T extends "bytes" ? Uint8Array_ : string
67+
> {
68+
/**
69+
* Constructs a new instance.
70+
*
71+
* @param options The options for the hexadecimal stream.
72+
*/
73+
constructor(options: { output?: T } = {}) {
74+
const decode = function (): (input: Uint8Array_) => Expect<T> {
75+
if (options.output === "bytes") return (x) => x as Expect<T>;
76+
const decoder = new TextDecoder();
77+
return (x) => decoder.decode(x) as Expect<T>;
78+
}();
5179
super({
5280
transform(chunk, controller) {
53-
controller.enqueue(encodeHex(chunk));
81+
const [output, i] = detach(chunk, calcMax(chunk.length));
82+
encode(output, i, 0);
83+
controller.enqueue(decode(output));
5484
},
5585
});
5686
}
5787
}
5888

5989
/**
60-
* Decodes a hex-encoded stream into a Uint8Array stream.
90+
* Transforms a hexadecimal stream into a {@link Uint8Array<ArrayBuffer>} stream.
6191
*
6292
* @experimental **UNSTABLE**: New API, yet to be vetted.
6393
*
64-
* @see {@link https://www.rfc-editor.org/rfc/rfc4648.html#section-8}
94+
* @typeParam T The type of the hexadecimal stream.
6595
*
66-
* @example Usage
96+
* @example Basic Usage
6797
* ```ts
6898
* import { assertEquals } from "@std/assert";
69-
* import { HexDecoderStream } from "@std/encoding/unstable-hex-stream";
70-
* import { toText } from "@std/streams/to-text";
99+
* import {
100+
* HexDecoderStream,
101+
* HexEncoderStream,
102+
* } from "@std/encoding/unstable-hex-stream";
103+
* import { toBytes } from "@std/streams/unstable-to-bytes";
71104
*
72-
* const stream = ReadableStream.from(["48656c6c6f2c", "20776f726c6421"])
73-
* .pipeThrough(new HexDecoderStream())
74-
* .pipeThrough(new TextDecoderStream());
105+
* const readable = (await Deno.open("./deno.lock"))
106+
* .readable
107+
* .pipeThrough(new HexEncoderStream({ output: "bytes" }))
108+
* .pipeThrough(new HexDecoderStream({ input: "bytes" }));
75109
*
76-
* assertEquals(await toText(stream), "Hello, world!");
110+
* assertEquals(
111+
* await toBytes(readable),
112+
* await Deno.readFile("./deno.lock"),
113+
* );
77114
* ```
78115
*/
79-
export class HexDecoderStream extends TransformStream<string, Uint8Array_> {
80-
constructor() {
81-
let push = "";
116+
export class HexDecoderStream<T extends "string" | "bytes">
117+
extends TransformStream<
118+
T extends "bytes" ? Uint8Array_ : string,
119+
Uint8Array_
120+
> {
121+
/**
122+
* Constructs a new instance.
123+
*
124+
* @param options The options of the hexadecimal stream.
125+
*/
126+
constructor(options: { input?: T } = {}) {
127+
const encode = function (): (input: Expect<T>) => Uint8Array_ {
128+
if (options.input === "bytes") return (x) => x as Uint8Array_;
129+
const encoder = new TextEncoder();
130+
return (x) => encoder.encode(x as string) as Uint8Array_;
131+
}();
132+
const push = new Uint8Array(1);
133+
let remainder = 0;
82134
super({
83135
transform(chunk, controller) {
84-
push += chunk;
85-
if (push.length < 2) {
86-
return;
136+
let output = encode(chunk);
137+
if (remainder) {
138+
output = detach(output, remainder + output.length)[0];
139+
output.set(push.subarray(0, remainder));
87140
}
88-
const remainder = -push.length % 2;
89-
controller.enqueue(decodeHex(push.slice(0, remainder || undefined)));
90-
push = remainder ? push.slice(remainder) : "";
141+
remainder = output.length % 2;
142+
if (remainder) push.set(output.subarray(-remainder));
143+
const o = decode(output.subarray(0, -remainder || undefined), 0, 0);
144+
controller.enqueue(output.subarray(0, o));
91145
},
92146
flush(controller) {
93-
if (push.length) {
94-
controller.enqueue(decodeHex(push));
147+
if (remainder) {
148+
const o = decode(push.subarray(0, remainder), 0, 0);
149+
controller.enqueue(push.subarray(0, o));
95150
}
96151
},
97152
});
Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,60 @@
11
// Copyright 2018-2025 the Deno authors. MIT license.
22

33
import { assertEquals } from "@std/assert";
4-
import { encodeHex } from "./hex.ts";
4+
import { toText } from "@std/streams";
5+
import { toBytes } from "@std/streams/unstable-to-bytes";
6+
import { FixedChunkStream } from "@std/streams/unstable-fixed-chunk-stream";
7+
import { encodeHex } from "./unstable_hex.ts";
58
import { HexDecoderStream, HexEncoderStream } from "./unstable_hex_stream.ts";
6-
import { toText } from "@std/streams/to-text";
7-
import { concat } from "@std/bytes/concat";
8-
import { RandomSliceStream } from "./_random_slice_stream.ts";
99

10-
Deno.test("HexEncoderStream() encodes stream", async () => {
11-
const stream = (await Deno.open("./deno.lock"))
10+
Deno.test("HexEncoderStream() with normal format", async () => {
11+
const readable = (await Deno.open("./deno.lock"))
1212
.readable
13-
.pipeThrough(new RandomSliceStream())
14-
.pipeThrough(new HexEncoderStream());
13+
.pipeThrough(new FixedChunkStream(1021))
14+
.pipeThrough(new HexEncoderStream({ output: "string" }));
1515

1616
assertEquals(
17-
await toText(stream),
17+
await toText(readable),
1818
encodeHex(await Deno.readFile("./deno.lock")),
1919
);
2020
});
2121

22-
Deno.test("HexDecoderStream() decodes stream", async () => {
23-
const stream = (await Deno.open("./deno.lock"))
22+
Deno.test("HexEncoderStream() with raw format", async () => {
23+
const readable = (await Deno.open("./deno.lock"))
2424
.readable
25-
.pipeThrough(new HexEncoderStream())
26-
.pipeThrough(new RandomSliceStream())
27-
.pipeThrough(new HexDecoderStream());
25+
.pipeThrough(new FixedChunkStream(1021))
26+
.pipeThrough(new HexEncoderStream({ output: "bytes" }));
2827

2928
assertEquals(
30-
concat(await Array.fromAsync(stream)),
29+
await toBytes(readable),
30+
new TextEncoder().encode(encodeHex(await Deno.readFile("./deno.lock"))),
31+
);
32+
});
33+
34+
Deno.test("HexDecoderStream() with normal format", async () => {
35+
const readable = (await Deno.open("./deno.lock"))
36+
.readable
37+
.pipeThrough(new HexEncoderStream({ output: "string" }))
38+
.pipeThrough(new TextEncoderStream())
39+
.pipeThrough(new FixedChunkStream(1021))
40+
.pipeThrough(new TextDecoderStream())
41+
.pipeThrough(new HexDecoderStream({ input: "string" }));
42+
43+
assertEquals(
44+
await toBytes(readable),
45+
await Deno.readFile("./deno.lock"),
46+
);
47+
});
48+
49+
Deno.test("HexDecoderStream() with raw format", async () => {
50+
const readable = (await Deno.open("./deno.lock"))
51+
.readable
52+
.pipeThrough(new HexEncoderStream({ output: "bytes" }))
53+
.pipeThrough(new FixedChunkStream(1021))
54+
.pipeThrough(new HexDecoderStream({ input: "bytes" }));
55+
56+
assertEquals(
57+
await toBytes(readable),
3158
await Deno.readFile("./deno.lock"),
3259
);
3360
});

0 commit comments

Comments
 (0)