@yume-chan/stream-extra 0.0.18 → 0.0.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.json +27 -0
- package/CHANGELOG.md +16 -1
- package/README.md +1 -1
- package/esm/buffered-transform.d.ts +10 -10
- package/esm/buffered-transform.d.ts.map +1 -1
- package/esm/buffered-transform.js +56 -55
- package/esm/buffered-transform.js.map +1 -1
- package/esm/buffered.d.ts +24 -27
- package/esm/buffered.d.ts.map +1 -1
- package/esm/buffered.js +132 -123
- package/esm/buffered.js.map +1 -1
- package/esm/consumable.d.ts +52 -0
- package/esm/consumable.d.ts.map +1 -0
- package/esm/consumable.js +178 -0
- package/esm/consumable.js.map +1 -0
- package/esm/decode-utf8.d.ts +4 -4
- package/esm/decode-utf8.js +11 -11
- package/esm/distribution.d.ts +21 -0
- package/esm/distribution.d.ts.map +1 -0
- package/esm/distribution.js +99 -0
- package/esm/distribution.js.map +1 -0
- package/esm/duplex.d.ts +44 -47
- package/esm/duplex.d.ts.map +1 -1
- package/esm/duplex.js +92 -92
- package/esm/duplex.js.map +1 -1
- package/esm/gather-string.d.ts +6 -6
- package/esm/gather-string.d.ts.map +1 -1
- package/esm/gather-string.js +15 -13
- package/esm/gather-string.js.map +1 -1
- package/esm/index.d.ts +16 -15
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +16 -15
- package/esm/index.js.map +1 -1
- package/esm/inspect.d.ts +4 -4
- package/esm/inspect.js +11 -11
- package/esm/pipe-from.d.ts +12 -11
- package/esm/pipe-from.d.ts.map +1 -1
- package/esm/pipe-from.js +23 -25
- package/esm/pipe-from.js.map +1 -1
- package/esm/push-readable.d.ts +19 -18
- package/esm/push-readable.d.ts.map +1 -1
- package/esm/push-readable.js +66 -61
- package/esm/push-readable.js.map +1 -1
- package/esm/split-string.d.ts +4 -4
- package/esm/split-string.d.ts.map +1 -1
- package/esm/split-string.js +24 -24
- package/esm/split-string.js.map +1 -1
- package/esm/stream.d.ts +25 -24
- package/esm/stream.d.ts.map +1 -1
- package/esm/stream.js +26 -26
- package/esm/stream.js.map +1 -1
- package/esm/struct-deserialize.d.ts +6 -6
- package/esm/struct-deserialize.d.ts.map +1 -1
- package/esm/struct-deserialize.js +8 -8
- package/esm/struct-serialize.d.ts +5 -5
- package/esm/struct-serialize.js +10 -10
- package/esm/wrap-readable.d.ts +21 -20
- package/esm/wrap-readable.d.ts.map +1 -1
- package/esm/wrap-readable.js +56 -56
- package/esm/wrap-readable.js.map +1 -1
- package/esm/wrap-writable.d.ts +14 -12
- package/esm/wrap-writable.d.ts.map +1 -1
- package/esm/wrap-writable.js +62 -52
- package/esm/wrap-writable.js.map +1 -1
- package/package.json +9 -8
- package/src/buffered-transform.ts +15 -22
- package/src/buffered.ts +41 -33
- package/src/consumable.ts +281 -0
- package/src/distribution.ts +113 -0
- package/src/duplex.ts +35 -34
- package/src/gather-string.ts +6 -4
- package/src/index.ts +16 -15
- package/src/inspect.ts +1 -1
- package/src/pipe-from.ts +8 -6
- package/src/push-readable.ts +12 -12
- package/src/split-string.ts +5 -2
- package/src/stream.ts +1 -1
- package/src/struct-deserialize.ts +1 -1
- package/src/wrap-readable.ts +9 -9
- package/src/wrap-writable.ts +21 -9
- package/tsconfig.build.json +6 -1
- package/tsconfig.build.tsbuildinfo +1 -1
- package/esm/chunk.d.ts +0 -5
- package/esm/chunk.d.ts.map +0 -1
- package/esm/chunk.js +0 -15
- package/esm/chunk.js.map +0 -1
- package/esm/trace.d.ts +0 -16
- package/esm/trace.d.ts.map +0 -1
- package/esm/trace.js +0 -131
- package/esm/trace.js.map +0 -1
- package/src/chunk.ts +0 -15
package/src/buffered.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
|
+
import type { AsyncExactReadable } from "@yume-chan/struct";
|
|
2
|
+
import { ExactReadableEndedError } from "@yume-chan/struct";
|
|
3
|
+
|
|
1
4
|
import { PushReadableStream } from "./push-readable.js";
|
|
2
|
-
import {
|
|
3
|
-
type ReadableStream,
|
|
4
|
-
type ReadableStreamDefaultReader,
|
|
5
|
-
} from "./stream.js";
|
|
5
|
+
import type { ReadableStream, ReadableStreamDefaultReader } from "./stream.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const NOOP = () => {
|
|
8
|
+
// no-op
|
|
9
|
+
};
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export class BufferedReadableStream implements AsyncExactReadable {
|
|
12
|
+
#buffered: Uint8Array | undefined;
|
|
13
|
+
#bufferedOffset = 0;
|
|
14
|
+
#bufferedLength = 0;
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
#position = 0;
|
|
17
|
+
public get position() {
|
|
18
|
+
return this.#position;
|
|
19
|
+
}
|
|
20
20
|
|
|
21
21
|
protected readonly stream: ReadableStream<Uint8Array>;
|
|
22
|
-
|
|
23
22
|
protected readonly reader: ReadableStreamDefaultReader<Uint8Array>;
|
|
24
23
|
|
|
25
24
|
public constructor(stream: ReadableStream<Uint8Array>) {
|
|
@@ -30,8 +29,9 @@ export class BufferedReadableStream {
|
|
|
30
29
|
private async readSource() {
|
|
31
30
|
const { done, value } = await this.reader.read();
|
|
32
31
|
if (done) {
|
|
33
|
-
throw new
|
|
32
|
+
throw new ExactReadableEndedError();
|
|
34
33
|
}
|
|
34
|
+
this.#position += value.byteLength;
|
|
35
35
|
return value;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -51,9 +51,9 @@ export class BufferedReadableStream {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
if (array.byteLength > length) {
|
|
54
|
-
this
|
|
55
|
-
this
|
|
56
|
-
this
|
|
54
|
+
this.#buffered = array;
|
|
55
|
+
this.#bufferedOffset = length;
|
|
56
|
+
this.#bufferedLength = array.byteLength - length;
|
|
57
57
|
return array.subarray(0, length);
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -71,9 +71,9 @@ export class BufferedReadableStream {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
if (array.byteLength > length) {
|
|
74
|
-
this
|
|
75
|
-
this
|
|
76
|
-
this
|
|
74
|
+
this.#buffered = array;
|
|
75
|
+
this.#bufferedOffset = length;
|
|
76
|
+
this.#bufferedLength = array.byteLength - length;
|
|
77
77
|
result.set(array.subarray(0, length), index);
|
|
78
78
|
return result;
|
|
79
79
|
}
|
|
@@ -91,20 +91,22 @@ export class BufferedReadableStream {
|
|
|
91
91
|
* @param length
|
|
92
92
|
* @returns
|
|
93
93
|
*/
|
|
94
|
-
public
|
|
94
|
+
public readExactly(length: number): Uint8Array | Promise<Uint8Array> {
|
|
95
95
|
// PERF: Add a synchronous path for reading from internal buffer
|
|
96
|
-
if (this
|
|
97
|
-
const array = this
|
|
98
|
-
const offset = this
|
|
99
|
-
if (this
|
|
96
|
+
if (this.#buffered) {
|
|
97
|
+
const array = this.#buffered;
|
|
98
|
+
const offset = this.#bufferedOffset;
|
|
99
|
+
if (this.#bufferedLength > length) {
|
|
100
100
|
// PERF: `subarray` is slow
|
|
101
101
|
// don't use it until absolutely necessary
|
|
102
|
-
this
|
|
103
|
-
this
|
|
102
|
+
this.#bufferedOffset += length;
|
|
103
|
+
this.#bufferedLength -= length;
|
|
104
104
|
return array.subarray(offset, offset + length);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
this
|
|
107
|
+
this.#buffered = undefined;
|
|
108
|
+
this.#bufferedLength = 0;
|
|
109
|
+
this.#bufferedOffset = 0;
|
|
108
110
|
return this.readAsync(length, array.subarray(offset));
|
|
109
111
|
}
|
|
110
112
|
|
|
@@ -117,10 +119,16 @@ export class BufferedReadableStream {
|
|
|
117
119
|
* @returns A `ReadableStream`
|
|
118
120
|
*/
|
|
119
121
|
public release(): ReadableStream<Uint8Array> {
|
|
120
|
-
if (this
|
|
122
|
+
if (this.#bufferedLength > 0) {
|
|
121
123
|
return new PushReadableStream<Uint8Array>(async (controller) => {
|
|
122
124
|
// Put the remaining data back to the stream
|
|
123
|
-
|
|
125
|
+
const buffered = this.#buffered!.subarray(this.#bufferedOffset);
|
|
126
|
+
await controller.enqueue(buffered);
|
|
127
|
+
|
|
128
|
+
controller.abortSignal.addEventListener("abort", () => {
|
|
129
|
+
// NOOP: the reader might already be released
|
|
130
|
+
this.reader.cancel().catch(NOOP);
|
|
131
|
+
});
|
|
124
132
|
|
|
125
133
|
// Manually pipe the stream
|
|
126
134
|
while (true) {
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { PromiseResolver } from "@yume-chan/async";
|
|
2
|
+
|
|
3
|
+
import type { QueuingStrategy, WritableStreamDefaultWriter } from "./stream.js";
|
|
4
|
+
import { ReadableStream, TransformStream, WritableStream } from "./stream.js";
|
|
5
|
+
|
|
6
|
+
interface Task {
|
|
7
|
+
run<T>(callback: () => T): T;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Console {
|
|
11
|
+
createTask(name: string): Task;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface GlobalExtension {
|
|
15
|
+
console: Console;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// `createTask` allows browser DevTools to track the call stack across async boundaries.
|
|
19
|
+
const { console } = globalThis as unknown as GlobalExtension;
|
|
20
|
+
const createTask: Console["createTask"] =
|
|
21
|
+
console.createTask?.bind(console) ??
|
|
22
|
+
(() => ({
|
|
23
|
+
run(callback) {
|
|
24
|
+
return callback();
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
export class Consumable<T> {
|
|
29
|
+
readonly #task: Task;
|
|
30
|
+
readonly #resolver: PromiseResolver<void>;
|
|
31
|
+
|
|
32
|
+
public readonly value: T;
|
|
33
|
+
public readonly consumed: Promise<void>;
|
|
34
|
+
|
|
35
|
+
public constructor(value: T) {
|
|
36
|
+
this.#task = createTask("Consumable");
|
|
37
|
+
this.value = value;
|
|
38
|
+
this.#resolver = new PromiseResolver<void>();
|
|
39
|
+
this.consumed = this.#resolver.promise;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public consume() {
|
|
43
|
+
this.#resolver.resolve();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public error(error: any) {
|
|
47
|
+
this.#resolver.reject(error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async tryConsume<U>(callback: (value: T) => U) {
|
|
51
|
+
try {
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/await-thenable
|
|
53
|
+
const result = await this.#task.run(() => callback(this.value));
|
|
54
|
+
this.consume();
|
|
55
|
+
return result;
|
|
56
|
+
} catch (e) {
|
|
57
|
+
this.#resolver.reject(e);
|
|
58
|
+
throw e;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function enqueue<T>(
|
|
64
|
+
controller: { enqueue: (chunk: Consumable<T>) => void },
|
|
65
|
+
chunk: T
|
|
66
|
+
) {
|
|
67
|
+
const output = new Consumable(chunk);
|
|
68
|
+
controller.enqueue(output);
|
|
69
|
+
await output.consumed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class WrapConsumableStream<T> extends TransformStream<T, Consumable<T>> {
|
|
73
|
+
public constructor() {
|
|
74
|
+
super({
|
|
75
|
+
async transform(chunk, controller) {
|
|
76
|
+
await enqueue(controller, chunk);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class UnwrapConsumableStream<T> extends TransformStream<
|
|
83
|
+
Consumable<T>,
|
|
84
|
+
T
|
|
85
|
+
> {
|
|
86
|
+
public constructor() {
|
|
87
|
+
super({
|
|
88
|
+
transform(chunk, controller) {
|
|
89
|
+
controller.enqueue(chunk.value);
|
|
90
|
+
chunk.consume();
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ConsumableReadableStreamController<T> {
|
|
97
|
+
enqueue(chunk: T): Promise<void>;
|
|
98
|
+
close(): void;
|
|
99
|
+
error(reason: any): void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ConsumableReadableStreamSource<T> {
|
|
103
|
+
start?(
|
|
104
|
+
controller: ConsumableReadableStreamController<T>
|
|
105
|
+
): void | PromiseLike<void>;
|
|
106
|
+
pull?(
|
|
107
|
+
controller: ConsumableReadableStreamController<T>
|
|
108
|
+
): void | PromiseLike<void>;
|
|
109
|
+
cancel?(reason: any): void | PromiseLike<void>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export class ConsumableReadableStream<T> extends ReadableStream<Consumable<T>> {
|
|
113
|
+
public constructor(
|
|
114
|
+
source: ConsumableReadableStreamSource<T>,
|
|
115
|
+
strategy?: QueuingStrategy<T>
|
|
116
|
+
) {
|
|
117
|
+
let wrappedController:
|
|
118
|
+
| ConsumableReadableStreamController<T>
|
|
119
|
+
| undefined;
|
|
120
|
+
|
|
121
|
+
let wrappedStrategy: QueuingStrategy<Consumable<T>> | undefined;
|
|
122
|
+
if (strategy) {
|
|
123
|
+
wrappedStrategy = {};
|
|
124
|
+
if ("highWaterMark" in strategy) {
|
|
125
|
+
wrappedStrategy.highWaterMark = strategy.highWaterMark;
|
|
126
|
+
}
|
|
127
|
+
if ("size" in strategy) {
|
|
128
|
+
wrappedStrategy.size = (chunk) => {
|
|
129
|
+
return strategy.size!(chunk.value);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
super(
|
|
135
|
+
{
|
|
136
|
+
async start(controller) {
|
|
137
|
+
wrappedController = {
|
|
138
|
+
async enqueue(chunk) {
|
|
139
|
+
await enqueue(controller, chunk);
|
|
140
|
+
},
|
|
141
|
+
close() {
|
|
142
|
+
controller.close();
|
|
143
|
+
},
|
|
144
|
+
error(reason) {
|
|
145
|
+
controller.error(reason);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
await source.start?.(wrappedController);
|
|
150
|
+
},
|
|
151
|
+
async pull() {
|
|
152
|
+
await source.pull?.(wrappedController!);
|
|
153
|
+
},
|
|
154
|
+
async cancel(reason) {
|
|
155
|
+
await source.cancel?.(reason);
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
wrappedStrategy
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ConsumableWritableStreamSink<T> {
|
|
164
|
+
start?(): void | PromiseLike<void>;
|
|
165
|
+
write?(chunk: T): void | PromiseLike<void>;
|
|
166
|
+
abort?(reason: any): void | PromiseLike<void>;
|
|
167
|
+
close?(): void | PromiseLike<void>;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class ConsumableWritableStream<T> extends WritableStream<Consumable<T>> {
|
|
171
|
+
public static async write<T>(
|
|
172
|
+
writer: WritableStreamDefaultWriter<Consumable<T>>,
|
|
173
|
+
value: T
|
|
174
|
+
) {
|
|
175
|
+
const consumable = new Consumable(value);
|
|
176
|
+
await writer.write(consumable);
|
|
177
|
+
await consumable.consumed;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public constructor(
|
|
181
|
+
sink: ConsumableWritableStreamSink<T>,
|
|
182
|
+
strategy?: QueuingStrategy<T>
|
|
183
|
+
) {
|
|
184
|
+
let wrappedStrategy: QueuingStrategy<Consumable<T>> | undefined;
|
|
185
|
+
if (strategy) {
|
|
186
|
+
wrappedStrategy = {};
|
|
187
|
+
if ("highWaterMark" in strategy) {
|
|
188
|
+
wrappedStrategy.highWaterMark = strategy.highWaterMark;
|
|
189
|
+
}
|
|
190
|
+
if ("size" in strategy) {
|
|
191
|
+
wrappedStrategy.size = (chunk) => {
|
|
192
|
+
return strategy.size!(chunk.value);
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
super(
|
|
198
|
+
{
|
|
199
|
+
start() {
|
|
200
|
+
return sink.start?.();
|
|
201
|
+
},
|
|
202
|
+
async write(chunk) {
|
|
203
|
+
await chunk.tryConsume((value) => sink.write?.(value));
|
|
204
|
+
chunk.consume();
|
|
205
|
+
},
|
|
206
|
+
abort(reason) {
|
|
207
|
+
return sink.abort?.(reason);
|
|
208
|
+
},
|
|
209
|
+
close() {
|
|
210
|
+
return sink.close?.();
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
wrappedStrategy
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface ConsumableTransformer<I, O> {
|
|
219
|
+
start?(
|
|
220
|
+
controller: ConsumableReadableStreamController<O>
|
|
221
|
+
): void | PromiseLike<void>;
|
|
222
|
+
transform?(
|
|
223
|
+
chunk: I,
|
|
224
|
+
controller: ConsumableReadableStreamController<O>
|
|
225
|
+
): void | PromiseLike<void>;
|
|
226
|
+
flush?(
|
|
227
|
+
controller: ConsumableReadableStreamController<O>
|
|
228
|
+
): void | PromiseLike<void>;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export class ConsumableTransformStream<I, O> extends TransformStream<
|
|
232
|
+
Consumable<I>,
|
|
233
|
+
Consumable<O>
|
|
234
|
+
> {
|
|
235
|
+
public constructor(transformer: ConsumableTransformer<I, O>) {
|
|
236
|
+
let wrappedController:
|
|
237
|
+
| ConsumableReadableStreamController<O>
|
|
238
|
+
| undefined;
|
|
239
|
+
|
|
240
|
+
super({
|
|
241
|
+
async start(controller) {
|
|
242
|
+
wrappedController = {
|
|
243
|
+
async enqueue(chunk) {
|
|
244
|
+
await enqueue(controller, chunk);
|
|
245
|
+
},
|
|
246
|
+
close() {
|
|
247
|
+
controller.terminate();
|
|
248
|
+
},
|
|
249
|
+
error(reason) {
|
|
250
|
+
controller.error(reason);
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
await transformer.start?.(wrappedController);
|
|
255
|
+
},
|
|
256
|
+
async transform(chunk) {
|
|
257
|
+
await chunk.tryConsume((value) =>
|
|
258
|
+
transformer.transform?.(value, wrappedController!)
|
|
259
|
+
);
|
|
260
|
+
chunk.consume();
|
|
261
|
+
},
|
|
262
|
+
async flush() {
|
|
263
|
+
await transformer.flush?.(wrappedController!);
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export class ConsumableInspectStream<T> extends TransformStream<
|
|
270
|
+
Consumable<T>,
|
|
271
|
+
Consumable<T>
|
|
272
|
+
> {
|
|
273
|
+
public constructor(callback: (value: T) => void) {
|
|
274
|
+
super({
|
|
275
|
+
transform(chunk, controller) {
|
|
276
|
+
callback(chunk.value);
|
|
277
|
+
controller.enqueue(chunk);
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ConsumableTransformStream } from "./consumable.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Splits or combines buffers to specified size.
|
|
5
|
+
*/
|
|
6
|
+
export class BufferCombiner {
|
|
7
|
+
#capacity: number;
|
|
8
|
+
readonly #buffer: Uint8Array;
|
|
9
|
+
#offset: number;
|
|
10
|
+
#available: number;
|
|
11
|
+
|
|
12
|
+
public constructor(size: number) {
|
|
13
|
+
this.#capacity = size;
|
|
14
|
+
this.#buffer = new Uint8Array(size);
|
|
15
|
+
this.#offset = 0;
|
|
16
|
+
this.#available = size;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pushes data to the combiner.
|
|
21
|
+
* @param data The input data to be split or combined.
|
|
22
|
+
* @returns
|
|
23
|
+
* A generator that yields buffers of specified size.
|
|
24
|
+
* It may yield the same buffer multiple times, consume the data before calling `next`.
|
|
25
|
+
*/
|
|
26
|
+
public *push(data: Uint8Array): Generator<Uint8Array, void, void> {
|
|
27
|
+
let offset = 0;
|
|
28
|
+
let available = data.byteLength;
|
|
29
|
+
|
|
30
|
+
if (this.#offset !== 0) {
|
|
31
|
+
if (available >= this.#available) {
|
|
32
|
+
this.#buffer.set(
|
|
33
|
+
data.subarray(0, this.#available),
|
|
34
|
+
this.#offset
|
|
35
|
+
);
|
|
36
|
+
offset += this.#available;
|
|
37
|
+
available -= this.#available;
|
|
38
|
+
|
|
39
|
+
yield this.#buffer;
|
|
40
|
+
this.#offset = 0;
|
|
41
|
+
this.#available = this.#capacity;
|
|
42
|
+
|
|
43
|
+
if (available === 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
this.#buffer.set(data, this.#offset);
|
|
48
|
+
this.#offset += available;
|
|
49
|
+
this.#available -= available;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
while (available >= this.#capacity) {
|
|
55
|
+
const end = offset + this.#capacity;
|
|
56
|
+
yield data.subarray(offset, end);
|
|
57
|
+
offset = end;
|
|
58
|
+
available -= this.#capacity;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (available > 0) {
|
|
62
|
+
this.#buffer.set(data.subarray(offset), this.#offset);
|
|
63
|
+
this.#offset += available;
|
|
64
|
+
this.#available -= available;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public flush(): Uint8Array | undefined {
|
|
69
|
+
if (this.#offset === 0) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const output = this.#buffer.subarray(0, this.#offset);
|
|
74
|
+
this.#offset = 0;
|
|
75
|
+
this.#available = this.#capacity;
|
|
76
|
+
return output;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class DistributionStream extends ConsumableTransformStream<
|
|
81
|
+
Uint8Array,
|
|
82
|
+
Uint8Array
|
|
83
|
+
> {
|
|
84
|
+
public constructor(size: number, combine = false) {
|
|
85
|
+
const combiner = combine ? new BufferCombiner(size) : undefined;
|
|
86
|
+
super({
|
|
87
|
+
async transform(chunk, controller) {
|
|
88
|
+
if (combiner) {
|
|
89
|
+
for (const buffer of combiner.push(chunk)) {
|
|
90
|
+
await controller.enqueue(buffer);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
let offset = 0;
|
|
94
|
+
let available = chunk.byteLength;
|
|
95
|
+
while (available > 0) {
|
|
96
|
+
const end = offset + size;
|
|
97
|
+
await controller.enqueue(chunk.subarray(offset, end));
|
|
98
|
+
offset = end;
|
|
99
|
+
available -= size;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
async flush(controller) {
|
|
104
|
+
if (combiner) {
|
|
105
|
+
const data = combiner.flush();
|
|
106
|
+
if (data) {
|
|
107
|
+
await controller.enqueue(data);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
package/src/duplex.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { PromiseResolver } from "@yume-chan/async";
|
|
2
|
-
import {
|
|
2
|
+
import type { ValueOrPromise } from "@yume-chan/struct";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type WritableStreamDefaultWriter,
|
|
4
|
+
import type {
|
|
5
|
+
ReadableStream,
|
|
6
|
+
ReadableStreamDefaultController,
|
|
7
|
+
WritableStreamDefaultWriter,
|
|
9
8
|
} from "./stream.js";
|
|
9
|
+
import { WritableStream } from "./stream.js";
|
|
10
10
|
import { WrapReadableStream } from "./wrap-readable.js";
|
|
11
11
|
|
|
12
|
+
const NOOP = () => {
|
|
13
|
+
// no-op
|
|
14
|
+
};
|
|
15
|
+
|
|
12
16
|
export interface DuplexStreamFactoryOptions {
|
|
13
17
|
/**
|
|
14
18
|
* Callback when any `ReadableStream` is cancelled (the user doesn't need any more data),
|
|
@@ -42,33 +46,33 @@ export interface DuplexStreamFactoryOptions {
|
|
|
42
46
|
* when any of them is closed, all other streams will be closed as well.
|
|
43
47
|
*/
|
|
44
48
|
export class DuplexStreamFactory<R, W> {
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
#readableControllers: ReadableStreamDefaultController<R>[] = [];
|
|
50
|
+
#writers: WritableStreamDefaultWriter<W>[] = [];
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
#writableClosed = false;
|
|
49
53
|
public get writableClosed() {
|
|
50
|
-
return this
|
|
54
|
+
return this.#writableClosed;
|
|
51
55
|
}
|
|
52
56
|
|
|
53
|
-
|
|
57
|
+
#closed = new PromiseResolver<void>();
|
|
54
58
|
public get closed() {
|
|
55
|
-
return this.
|
|
59
|
+
return this.#closed.promise;
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
62
|
+
readonly #options: DuplexStreamFactoryOptions;
|
|
59
63
|
|
|
60
64
|
public constructor(options?: DuplexStreamFactoryOptions) {
|
|
61
|
-
this
|
|
65
|
+
this.#options = options ?? {};
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
public wrapReadable(readable: ReadableStream<R>): WrapReadableStream<R> {
|
|
65
69
|
return new WrapReadableStream<R>({
|
|
66
70
|
start: (controller) => {
|
|
67
|
-
this
|
|
71
|
+
this.#readableControllers.push(controller);
|
|
68
72
|
return readable;
|
|
69
73
|
},
|
|
70
74
|
cancel: async () => {
|
|
71
|
-
// cancel means the local peer
|
|
75
|
+
// cancel means the local peer wants to close the connection.
|
|
72
76
|
await this.close();
|
|
73
77
|
},
|
|
74
78
|
close: async () => {
|
|
@@ -80,13 +84,12 @@ export class DuplexStreamFactory<R, W> {
|
|
|
80
84
|
|
|
81
85
|
public createWritable(stream: WritableStream<W>): WritableStream<W> {
|
|
82
86
|
const writer = stream.getWriter();
|
|
83
|
-
this
|
|
87
|
+
this.#writers.push(writer);
|
|
84
88
|
|
|
85
89
|
// `WritableStream` has no way to tell if the remote peer has closed the connection.
|
|
86
90
|
// So it only triggers `close`.
|
|
87
91
|
return new WritableStream<W>({
|
|
88
92
|
write: async (chunk) => {
|
|
89
|
-
await writer.ready;
|
|
90
93
|
await writer.write(chunk);
|
|
91
94
|
},
|
|
92
95
|
abort: async (reason) => {
|
|
@@ -94,45 +97,43 @@ export class DuplexStreamFactory<R, W> {
|
|
|
94
97
|
await this.close();
|
|
95
98
|
},
|
|
96
99
|
close: async () => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
});
|
|
100
|
+
// NOOP: the writer is already closed
|
|
101
|
+
await writer.close().catch(NOOP);
|
|
100
102
|
await this.close();
|
|
101
103
|
},
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
public async close() {
|
|
106
|
-
if (this
|
|
108
|
+
if (this.#writableClosed) {
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
|
-
this
|
|
111
|
+
this.#writableClosed = true;
|
|
110
112
|
|
|
111
113
|
// Call `close` first, so it can still write data to `WritableStream`s.
|
|
112
|
-
if ((await this
|
|
114
|
+
if ((await this.#options.close?.()) !== false) {
|
|
113
115
|
// `close` can return `false` to disable automatic `dispose`.
|
|
114
116
|
await this.dispose();
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
for (const writer of this
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
119
|
+
for (const writer of this.#writers) {
|
|
120
|
+
// NOOP: the writer is already closed
|
|
121
|
+
writer.close().catch(NOOP);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
public async dispose() {
|
|
125
|
-
this
|
|
126
|
-
this.
|
|
126
|
+
this.#writableClosed = true;
|
|
127
|
+
this.#closed.resolve();
|
|
127
128
|
|
|
128
|
-
for (const controller of this
|
|
129
|
+
for (const controller of this.#readableControllers) {
|
|
129
130
|
try {
|
|
130
131
|
controller.close();
|
|
131
|
-
} catch
|
|
132
|
-
|
|
132
|
+
} catch {
|
|
133
|
+
// ignore
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
await this
|
|
137
|
+
await this.#options.dispose?.();
|
|
137
138
|
}
|
|
138
139
|
}
|
package/src/gather-string.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { WritableStream } from "./stream.js";
|
|
2
2
|
|
|
3
|
-
export class GatherStringStream extends WritableStream<string>{
|
|
3
|
+
export class GatherStringStream extends WritableStream<string> {
|
|
4
4
|
// PERF: rope (concat strings) is faster than `[].join('')`
|
|
5
|
-
|
|
6
|
-
public get result() {
|
|
5
|
+
#result = "";
|
|
6
|
+
public get result() {
|
|
7
|
+
return this.#result;
|
|
8
|
+
}
|
|
7
9
|
|
|
8
10
|
public constructor() {
|
|
9
11
|
super({
|
|
10
12
|
write: (chunk) => {
|
|
11
|
-
this
|
|
13
|
+
this.#result += chunk;
|
|
12
14
|
},
|
|
13
15
|
});
|
|
14
16
|
}
|