@yume-chan/stream-extra 2.5.3 → 3.0.0-beta.0

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.
Files changed (167) hide show
  1. package/LICENSE +1 -1
  2. package/esm/buffered-transform.d.ts +2 -2
  3. package/esm/buffered-transform.d.ts.map +1 -1
  4. package/esm/buffered-transform.js +1 -1
  5. package/esm/buffered-transform.js.map +1 -1
  6. package/esm/buffered.d.ts +1 -1
  7. package/esm/buffered.d.ts.map +1 -1
  8. package/esm/buffered.js.map +1 -1
  9. package/esm/concat.d.ts +25 -16
  10. package/esm/concat.d.ts.map +1 -1
  11. package/esm/concat.js +124 -93
  12. package/esm/concat.js.map +1 -1
  13. package/esm/consumable/readable.d.ts +2 -2
  14. package/esm/consumable/readable.d.ts.map +1 -1
  15. package/esm/consumable/readable.js +1 -1
  16. package/esm/consumable/readable.js.map +1 -1
  17. package/esm/consumable/wrap-byte-readable.d.ts +5 -2
  18. package/esm/consumable/wrap-byte-readable.d.ts.map +1 -1
  19. package/esm/consumable/wrap-byte-readable.js +12 -6
  20. package/esm/consumable/wrap-byte-readable.js.map +1 -1
  21. package/esm/consumable/wrap-writable.d.ts +1 -1
  22. package/esm/consumable/wrap-writable.d.ts.map +1 -1
  23. package/esm/consumable/wrap-writable.js +1 -1
  24. package/esm/consumable/wrap-writable.js.map +1 -1
  25. package/esm/consumable/writable.d.ts +2 -2
  26. package/esm/consumable/writable.d.ts.map +1 -1
  27. package/esm/consumable/writable.js +1 -1
  28. package/esm/consumable/writable.js.map +1 -1
  29. package/esm/consumable.d.ts +4 -0
  30. package/esm/consumable.d.ts.map +1 -1
  31. package/esm/consumable.js +5 -0
  32. package/esm/consumable.js.map +1 -1
  33. package/esm/distribution.d.ts +1 -1
  34. package/esm/distribution.d.ts.map +1 -1
  35. package/esm/distribution.js +1 -1
  36. package/esm/distribution.js.map +1 -1
  37. package/esm/duplex.d.ts +2 -2
  38. package/esm/duplex.d.ts.map +1 -1
  39. package/esm/duplex.js +1 -1
  40. package/esm/duplex.js.map +1 -1
  41. package/esm/global/abort-signal.d.ts +87 -0
  42. package/esm/global/abort-signal.d.ts.map +1 -0
  43. package/esm/global/abort-signal.js +4 -0
  44. package/esm/global/abort-signal.js.map +1 -0
  45. package/esm/global/compression.d.ts +15 -0
  46. package/esm/global/compression.d.ts.map +1 -0
  47. package/esm/global/compression.js +4 -0
  48. package/esm/global/compression.js.map +1 -0
  49. package/esm/global/encoding.d.ts +25 -0
  50. package/esm/global/encoding.d.ts.map +1 -0
  51. package/esm/global/encoding.js +4 -0
  52. package/esm/global/encoding.js.map +1 -0
  53. package/esm/global/event.d.ts +191 -0
  54. package/esm/global/event.d.ts.map +1 -0
  55. package/esm/global/event.js +4 -0
  56. package/esm/global/event.js.map +1 -0
  57. package/esm/global/index.d.ts +7 -0
  58. package/esm/global/index.d.ts.map +1 -0
  59. package/esm/global/index.js +7 -0
  60. package/esm/global/index.js.map +1 -0
  61. package/esm/{types.d.ts → global/streams.d.ts} +136 -222
  62. package/esm/global/streams.d.ts.map +1 -0
  63. package/esm/{stream.js → global/streams.js} +16 -8
  64. package/esm/global/streams.js.map +1 -0
  65. package/esm/global/timer.d.ts +13 -0
  66. package/esm/global/timer.d.ts.map +1 -0
  67. package/esm/global/timer.js +4 -0
  68. package/esm/global/timer.js.map +1 -0
  69. package/esm/global/utils.d.ts +9 -0
  70. package/esm/global/utils.d.ts.map +1 -0
  71. package/esm/global/utils.js +5 -0
  72. package/esm/global/utils.js.map +1 -0
  73. package/esm/index.d.ts +2 -2
  74. package/esm/index.d.ts.map +1 -1
  75. package/esm/index.js +2 -2
  76. package/esm/index.js.map +1 -1
  77. package/esm/inspect.d.ts +6 -2
  78. package/esm/inspect.d.ts.map +1 -1
  79. package/esm/inspect.js +10 -4
  80. package/esm/inspect.js.map +1 -1
  81. package/esm/maybe-consumable/wrap-writable.d.ts +1 -1
  82. package/esm/maybe-consumable/wrap-writable.d.ts.map +1 -1
  83. package/esm/maybe-consumable/wrap-writable.js +1 -1
  84. package/esm/maybe-consumable/wrap-writable.js.map +1 -1
  85. package/esm/maybe-consumable/writable.d.ts +2 -2
  86. package/esm/maybe-consumable/writable.d.ts.map +1 -1
  87. package/esm/maybe-consumable/writable.js +1 -1
  88. package/esm/maybe-consumable/writable.js.map +1 -1
  89. package/esm/pipe-from.d.ts +3 -3
  90. package/esm/pipe-from.d.ts.map +1 -1
  91. package/esm/pipe-from.js +1 -1
  92. package/esm/pipe-from.js.map +1 -1
  93. package/esm/push-readable.d.ts +20 -3
  94. package/esm/push-readable.d.ts.map +1 -1
  95. package/esm/push-readable.js +180 -180
  96. package/esm/push-readable.js.map +1 -1
  97. package/esm/split-string.d.ts +6 -2
  98. package/esm/split-string.d.ts.map +1 -1
  99. package/esm/split-string.js +28 -7
  100. package/esm/split-string.js.map +1 -1
  101. package/esm/struct-deserialize.js.map +1 -1
  102. package/esm/struct-serialize.d.ts +1 -1
  103. package/esm/struct-serialize.d.ts.map +1 -1
  104. package/esm/struct-serialize.js +1 -1
  105. package/esm/struct-serialize.js.map +1 -1
  106. package/esm/task-queue.d.ts +8 -0
  107. package/esm/task-queue.d.ts.map +1 -0
  108. package/esm/task-queue.js +50 -0
  109. package/esm/task-queue.js.map +1 -0
  110. package/esm/try-close.d.ts +3 -5
  111. package/esm/try-close.d.ts.map +1 -1
  112. package/esm/try-close.js.map +1 -1
  113. package/esm/wrap-readable.d.ts +2 -2
  114. package/esm/wrap-readable.d.ts.map +1 -1
  115. package/esm/wrap-readable.js +1 -1
  116. package/esm/wrap-readable.js.map +1 -1
  117. package/esm/wrap-writable.d.ts +2 -2
  118. package/esm/wrap-writable.d.ts.map +1 -1
  119. package/esm/wrap-writable.js +1 -1
  120. package/esm/wrap-writable.js.map +1 -1
  121. package/package.json +8 -8
  122. package/src/buffered-transform.ts +8 -7
  123. package/src/buffered.ts +4 -1
  124. package/src/concat.ts +161 -118
  125. package/src/consumable/readable.ts +2 -2
  126. package/src/consumable/wrap-byte-readable.ts +20 -7
  127. package/src/consumable/wrap-writable.ts +1 -1
  128. package/src/consumable/writable.ts +2 -2
  129. package/src/consumable.ts +8 -0
  130. package/src/distribution.ts +1 -1
  131. package/src/duplex.ts +2 -2
  132. package/src/global/abort-signal.ts +129 -0
  133. package/src/global/compression.ts +28 -0
  134. package/src/global/encoding.ts +36 -0
  135. package/src/global/event.ts +221 -0
  136. package/src/global/index.ts +6 -0
  137. package/src/{types.ts → global/streams.ts} +266 -244
  138. package/src/global/timer.ts +18 -0
  139. package/src/global/utils.ts +14 -0
  140. package/src/index.ts +2 -2
  141. package/src/inspect.ts +18 -4
  142. package/src/maybe-consumable/wrap-writable.ts +1 -1
  143. package/src/maybe-consumable/writable.ts +3 -3
  144. package/src/pipe-from.ts +2 -2
  145. package/src/push-readable.ts +226 -198
  146. package/src/split-string.ts +41 -7
  147. package/src/struct-deserialize.ts +1 -1
  148. package/src/struct-serialize.ts +1 -1
  149. package/src/task-queue.ts +69 -0
  150. package/src/try-close.ts +0 -12
  151. package/src/wrap-readable.ts +2 -2
  152. package/src/wrap-writable.ts +5 -2
  153. package/tsconfig.build.json +1 -6
  154. package/CHANGELOG.md +0 -102
  155. package/esm/encoding.d.ts +0 -21
  156. package/esm/encoding.d.ts.map +0 -1
  157. package/esm/encoding.js +0 -4
  158. package/esm/encoding.js.map +0 -1
  159. package/esm/stream.d.ts +0 -25
  160. package/esm/stream.d.ts.map +0 -1
  161. package/esm/stream.js.map +0 -1
  162. package/esm/types.d.ts.map +0 -1
  163. package/esm/types.js +0 -3
  164. package/esm/types.js.map +0 -1
  165. package/src/encoding.ts +0 -39
  166. package/src/stream.ts +0 -106
  167. package/tsconfig.build.tsbuildinfo +0 -1
@@ -0,0 +1,18 @@
1
+ import { getGlobalValue } from "./utils.js";
2
+
3
+ interface TimerHandler {
4
+ (...args: unknown[]): void;
5
+ }
6
+
7
+ interface SetTimeoutFunction {
8
+ (handler: TimerHandler, timeout?: number, ...args: unknown[]): number;
9
+ }
10
+
11
+ interface ClearTimeoutFunction {
12
+ (timeoutId: number): void;
13
+ }
14
+
15
+ export const setTimeout =
16
+ getGlobalValue<SetTimeoutFunction>("setTimeout") ?? (() => {});
17
+ export const clearTimeout =
18
+ getGlobalValue<ClearTimeoutFunction>("clearTimeout") ?? (() => {});
@@ -0,0 +1,14 @@
1
+ export type GlobalPrototypeOr<K extends string, Or> =
2
+ typeof globalThis extends Record<K, { prototype: infer T }> ? T : Or;
3
+
4
+ export type GlobalValueOr<
5
+ K extends string,
6
+ Or,
7
+ > = (typeof globalThis extends Record<K, infer T> ? T : Or) & {
8
+ __keepAlias?: never;
9
+ };
10
+
11
+ /** #__NO_SIDE_EFFECTS__ */
12
+ export function getGlobalValue<T>(key: string) {
13
+ return (globalThis as unknown as Record<string, T>)[key]!;
14
+ }
package/src/index.ts CHANGED
@@ -4,15 +4,15 @@ export * from "./concat.js";
4
4
  export * from "./consumable.js";
5
5
  export * from "./distribution.js";
6
6
  export * from "./duplex.js";
7
- export * from "./encoding.js";
7
+ export * from "./global/index.js";
8
8
  export * from "./inspect.js";
9
9
  export * from "./maybe-consumable.js";
10
10
  export * from "./pipe-from.js";
11
11
  export * from "./push-readable.js";
12
12
  export * from "./split-string.js";
13
- export * from "./stream.js";
14
13
  export * from "./struct-deserialize.js";
15
14
  export * from "./struct-serialize.js";
15
+ export * from "./task-queue.js";
16
16
  export * from "./task.js";
17
17
  export * from "./try-close.js";
18
18
  export * from "./wrap-readable.js";
package/src/inspect.ts CHANGED
@@ -1,12 +1,26 @@
1
- import { TransformStream } from "./stream.js";
1
+ import type { MaybePromiseLike } from "@yume-chan/async";
2
+
3
+ import { TransformStream } from "./global/index.js";
2
4
 
3
5
  export class InspectStream<T> extends TransformStream<T, T> {
4
- constructor(callback: (value: T) => void) {
6
+ constructor(
7
+ write: (value: T) => MaybePromiseLike<undefined>,
8
+ extras?: {
9
+ close?: (() => void) | undefined;
10
+ cancel?: (() => void) | undefined;
11
+ },
12
+ ) {
5
13
  super({
6
- transform(chunk, controller) {
7
- callback(chunk);
14
+ async transform(chunk, controller) {
15
+ await write(chunk);
8
16
  controller.enqueue(chunk);
9
17
  },
18
+ flush() {
19
+ extras?.close?.();
20
+ },
21
+ cancel() {
22
+ extras?.cancel?.();
23
+ },
10
24
  });
11
25
  }
12
26
  }
@@ -1,5 +1,5 @@
1
+ import { WritableStream } from "../global/index.js";
1
2
  import type { MaybeConsumable } from "../maybe-consumable.js";
2
- import { WritableStream } from "../stream.js";
3
3
 
4
4
  import { tryConsume } from "./utils.js";
5
5
 
@@ -1,10 +1,10 @@
1
1
  import { Consumable } from "../consumable.js";
2
- import type { MaybeConsumable } from "../maybe-consumable.js";
3
2
  import type {
4
3
  QueuingStrategy,
5
4
  WritableStreamDefaultController,
6
- } from "../stream.js";
7
- import { WritableStream } from "../stream.js";
5
+ } from "../global/index.js";
6
+ import { WritableStream } from "../global/index.js";
7
+ import type { MaybeConsumable } from "../maybe-consumable.js";
8
8
 
9
9
  import { tryConsume } from "./utils.js";
10
10
 
package/src/pipe-from.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { ReadableWritablePair } from "./stream.js";
2
- import { WritableStream } from "./stream.js";
1
+ import type { ReadableWritablePair } from "./global/index.js";
2
+ import { WritableStream } from "./global/index.js";
3
3
 
4
4
  /**
5
5
  * Pipe `pair.readable` to `writable`, then returns `pair.writable`.
@@ -1,12 +1,35 @@
1
- import { PromiseResolver } from "@yume-chan/async";
1
+ import type { MaybePromise } from "@yume-chan/async";
2
+ import { isPromiseLike, PromiseResolver } from "@yume-chan/async";
2
3
 
3
- import type { AbortSignal, QueuingStrategy } from "./stream.js";
4
- import { AbortController, ReadableStream } from "./stream.js";
4
+ import type {
5
+ AbortSignal,
6
+ QueuingStrategy,
7
+ ReadableStreamDefaultController,
8
+ } from "./global/index.js";
9
+ import { AbortController, ReadableStream } from "./global/index.js";
10
+ import { TaskQueue } from "./task-queue.js";
5
11
 
6
12
  export interface PushReadableStreamController<T> {
7
13
  abortSignal: AbortSignal;
8
14
 
9
- enqueue(chunk: T): Promise<void>;
15
+ /**
16
+ * Enqueue `chunk` into the stream.
17
+ *
18
+ * - If the stream is already cancelled by consumer before calling `enqueue`,
19
+ * the call will return `false`.
20
+ * - If the stream is already closed or errored by producer before calling `enqueue`,
21
+ * the call will throw an error.
22
+ * - If the stream has enough buffer space, the call will return `true`.
23
+ *
24
+ * Otherwise it returns a `Promise`:
25
+ *
26
+ * - If the stream is cancelled by consumer, or closed or errored by producer,
27
+ * while the `Promise` is pending, the `Promise` will be resolved to `false`.
28
+ * - When the stream has enough buffer space, the `Promise` will be resolved to `true`.
29
+ *
30
+ * @param chunk The value to be enqueued
31
+ */
32
+ enqueue(chunk: T): Promise<boolean>;
10
33
 
11
34
  close(): void;
12
35
 
@@ -51,216 +74,220 @@ export class PushReadableStream<T> extends ReadableStream<T> {
51
74
  strategy?: QueuingStrategy<T>,
52
75
  logger?: PushReadableLogger<T>,
53
76
  ) {
54
- let waterMarkLow: PromiseResolver<void> | undefined;
77
+ let controller!: ReadableStreamDefaultController<T>;
78
+ const tasks = new TaskQueue();
79
+
55
80
  let zeroHighWaterMarkAllowEnqueue = false;
81
+ // Resolves when consumer calls `reader.read`.
82
+ // Rejects when producer calls `controller.close` or `controller.error`,
83
+ // or consumer calls `reader.cancel`.
84
+ let waterMarkLow: PromiseResolver<undefined> | undefined;
85
+
56
86
  const abortController = new AbortController();
87
+ // Whether either the consumer has called `stream.cancel()`,
88
+ // or the producer has called `controller.close` or `controller.error`.
89
+ let stopped = false;
90
+
91
+ const enqueue = (chunk: T): MaybePromise<boolean> => {
92
+ logger?.({
93
+ source: "producer",
94
+ operation: "enqueue",
95
+ value: chunk,
96
+ phase: "start",
97
+ });
98
+
99
+ if (abortController.signal.aborted) {
100
+ // In original `ReadableStream`, calling `enqueue` or `close`
101
+ // on an cancelled stream will throw an error,
102
+ //
103
+ // But in `PushReadableStream`, `enqueue` is an async function,
104
+ // the producer can't just check `abortSignal.aborted`
105
+ // before calling `enqueue`, as it might change when waiting
106
+ // for the backpressure to be reduced.
107
+ //
108
+ // So IMO it's better to handle this for the producer
109
+ // by simply ignoring the `enqueue` call.
110
+ //
111
+ // Note that we check `abortSignal.aborted` instead of `stopped`,
112
+ // as it's not allowed for the producer to call `enqueue` after
113
+ // they called `close` or `error`.
114
+ //
115
+ // Obviously, the producer should listen to the `abortSignal` and
116
+ // stop producing, but most pushing data sources don't support that.
117
+ logger?.({
118
+ source: "producer",
119
+ operation: "enqueue",
120
+ value: chunk,
121
+ phase: "ignored",
122
+ });
123
+ return false;
124
+ }
125
+
126
+ if (controller.desiredSize === null) {
127
+ // `desiredSize` being `null` means the stream is in error state,
128
+ // `controller.enqueue` will throw an error for us.
129
+ controller.enqueue(chunk);
130
+ // istanbul ignore next
131
+ throw new Error("unreachable");
132
+ }
133
+
134
+ if (zeroHighWaterMarkAllowEnqueue) {
135
+ // When `highWaterMark` is set to `0`,
136
+ // `controller.desiredSize` will always be `0`,
137
+ // even if the consumer has called `reader.read()`.
138
+ // (in this case, each `reader.read()`/`pull`
139
+ // should allow one `enqueue` of any size)
140
+ //
141
+ // If the consumer has already called `reader.read()`,
142
+ // before the producer tries to `enqueue`,
143
+ // `controller.desiredSize` is `0` and normal `waterMarkLow` signal
144
+ // will never trigger,
145
+ // (because `ReadableStream` prevents reentrance of `pull`)
146
+ // The stream will stuck.
147
+ //
148
+ // So we need a special signal for this case.
149
+ zeroHighWaterMarkAllowEnqueue = false;
150
+ controller.enqueue(chunk);
151
+ logger?.({
152
+ source: "producer",
153
+ operation: "enqueue",
154
+ value: chunk,
155
+ phase: "complete",
156
+ });
157
+ return true;
158
+ }
159
+
160
+ if (controller.desiredSize <= 0) {
161
+ logger?.({
162
+ source: "producer",
163
+ operation: "enqueue",
164
+ value: chunk,
165
+ phase: "waiting",
166
+ });
167
+
168
+ waterMarkLow = new PromiseResolver<undefined>();
169
+ return waterMarkLow.promise.then(
170
+ (): boolean => {
171
+ controller.enqueue(chunk);
172
+ logger?.({
173
+ source: "producer",
174
+ operation: "enqueue",
175
+ value: chunk,
176
+ phase: "complete",
177
+ });
178
+ return true;
179
+ },
180
+ (): boolean => {
181
+ // Only ignore this in-flight `enqueue` call
182
+ // future calls will trigger `desiredSize === null` and throw
183
+ logger?.({
184
+ source: "producer",
185
+ operation: "enqueue",
186
+ value: chunk,
187
+ phase: "ignored",
188
+ });
189
+ return false;
190
+ },
191
+ );
192
+ }
193
+
194
+ controller.enqueue(chunk);
195
+ logger?.({
196
+ source: "producer",
197
+ operation: "enqueue",
198
+ value: chunk,
199
+ phase: "complete",
200
+ });
201
+ return true;
202
+ };
203
+
204
+ const close = (explicit: boolean) => {
205
+ logger?.({
206
+ source: "producer",
207
+ operation: "close",
208
+ explicit,
209
+ phase: "start",
210
+ });
211
+
212
+ // Allow calling `controller.close` on cancelled stream as `enqueue` does
213
+ // Ignore implicit close on any stopped state
214
+ // But don't allow calling `controller.close` multiple times
215
+ if (abortController.signal.aborted || (stopped && !explicit)) {
216
+ logger?.({
217
+ source: "producer",
218
+ operation: "close",
219
+ explicit,
220
+ phase: "ignored",
221
+ });
222
+ return;
223
+ }
224
+
225
+ // This will throw for us if the stream is not in `readable` state
226
+ controller.close();
227
+
228
+ stopped = true;
229
+ // Wake up pending `enqueue`
230
+ waterMarkLow?.reject();
231
+
232
+ logger?.({
233
+ source: "producer",
234
+ operation: "close",
235
+ explicit,
236
+ phase: "complete",
237
+ });
238
+ };
239
+
240
+ const error = (error: unknown, explicit: boolean) => {
241
+ logger?.({
242
+ source: "producer",
243
+ operation: "error",
244
+ explicit,
245
+ phase: "start",
246
+ });
247
+
248
+ stopped = true;
249
+ // `controller.error` won't throw on closed/errored/cancelled stream
250
+ // so don't need any checks
251
+ controller.error(error);
252
+ // Wake up pending `enqueue`
253
+ waterMarkLow?.reject();
254
+
255
+ logger?.({
256
+ source: "producer",
257
+ operation: "error",
258
+ explicit,
259
+ phase: "complete",
260
+ });
261
+ };
57
262
 
58
263
  super(
59
264
  {
60
- start: (controller) => {
265
+ start: (controller_) => {
266
+ controller = controller_;
267
+
61
268
  const result = source({
62
269
  abortSignal: abortController.signal,
63
- enqueue: async (chunk) => {
64
- logger?.({
65
- source: "producer",
66
- operation: "enqueue",
67
- value: chunk,
68
- phase: "start",
69
- });
70
-
71
- if (abortController.signal.aborted) {
72
- // In original `ReadableStream`, calling `enqueue` or `close`
73
- // on an cancelled stream will throw an error,
74
- //
75
- // But in `PushReadableStream`, `enqueue` is an async function,
76
- // the producer can't just check `abortSignal.aborted`
77
- // before calling `enqueue`, as it might change when waiting
78
- // for the backpressure to be reduced.
79
- //
80
- // So IMO it's better to handle this for the producer
81
- // by simply ignoring the `enqueue` call.
82
- //
83
- // Note that we check `abortSignal.aborted` instead of `stopped`,
84
- // as it's not allowed for the producer to call `enqueue` after
85
- // they called `close` or `error`.
86
- //
87
- // Obviously, the producer should listen to the `abortSignal` and
88
- // stop producing, but most pushing data sources don't support that.
89
- logger?.({
90
- source: "producer",
91
- operation: "enqueue",
92
- value: chunk,
93
- phase: "ignored",
94
- });
95
- return;
96
- }
97
-
98
- if (controller.desiredSize === null) {
99
- // `desiredSize` being `null` means the stream is in error state,
100
- // `controller.enqueue` will throw an error for us.
101
- controller.enqueue(chunk);
102
- // istanbul ignore next
103
- return;
104
- }
105
-
106
- if (zeroHighWaterMarkAllowEnqueue) {
107
- // When `highWaterMark` is set to `0`,
108
- // `controller.desiredSize` will always be `0`,
109
- // even if the consumer has called `reader.read()`.
110
- // (in this case, each `reader.read()`/`pull`
111
- // should allow one `enqueue` of any size)
112
- //
113
- // If the consumer has already called `reader.read()`,
114
- // before the producer tries to `enqueue`,
115
- // `controller.desiredSize` is `0` and normal `waterMarkLow` signal
116
- // will never trigger,
117
- // (because `ReadableStream` prevents reentrance of `pull`)
118
- // The stream will stuck.
119
- //
120
- // So we need a special signal for this case.
121
- zeroHighWaterMarkAllowEnqueue = false;
122
- controller.enqueue(chunk);
123
- logger?.({
124
- source: "producer",
125
- operation: "enqueue",
126
- value: chunk,
127
- phase: "complete",
128
- });
129
- return;
130
- }
131
-
132
- if (controller.desiredSize <= 0) {
133
- logger?.({
134
- source: "producer",
135
- operation: "enqueue",
136
- value: chunk,
137
- phase: "waiting",
138
- });
139
-
140
- waterMarkLow = new PromiseResolver<void>();
141
- await waterMarkLow.promise;
142
-
143
- // Recheck consumer cancellation after async operations.
144
- if (abortController.signal.aborted) {
145
- logger?.({
146
- source: "producer",
147
- operation: "enqueue",
148
- value: chunk,
149
- phase: "ignored",
150
- });
151
- return;
152
- }
153
- }
154
-
155
- controller.enqueue(chunk);
156
- logger?.({
157
- source: "producer",
158
- operation: "enqueue",
159
- value: chunk,
160
- phase: "complete",
161
- });
162
- },
270
+ enqueue: async (chunk) =>
271
+ // Run `enqueue`s in serial
272
+ // Use `async/await` to always return a `Promise`
273
+ await tasks.enqueue(() => enqueue(chunk)),
163
274
  close() {
164
- logger?.({
165
- source: "producer",
166
- operation: "close",
167
- explicit: true,
168
- phase: "start",
169
- });
170
-
171
- // Since `enqueue` on an cancelled stream won't throw an error,
172
- // so does `close`.
173
- if (abortController.signal.aborted) {
174
- logger?.({
175
- source: "producer",
176
- operation: "close",
177
- explicit: true,
178
- phase: "ignored",
179
- });
180
- return;
181
- }
182
-
183
- controller.close();
184
- logger?.({
185
- source: "producer",
186
- operation: "close",
187
- explicit: true,
188
- phase: "complete",
189
- });
275
+ close(true);
190
276
  },
191
277
  error(e) {
192
- logger?.({
193
- source: "producer",
194
- operation: "error",
195
- explicit: true,
196
- phase: "start",
197
- });
198
-
199
- // Calling `error` on an already closed or errored stream is a no-op.
200
- controller.error(e);
201
-
202
- logger?.({
203
- source: "producer",
204
- operation: "error",
205
- explicit: true,
206
- phase: "complete",
207
- });
278
+ error(e, true);
208
279
  },
209
280
  });
210
281
 
211
- if (result && "then" in result) {
282
+ if (!stopped && isPromiseLike(result)) {
212
283
  // If `source` returns a `Promise`,
213
284
  // close the stream when the `Promise` is resolved,
214
285
  // and error the stream when the `Promise` is rejected.
215
286
  // The producer can return a never-settling `Promise`
216
287
  // to disable this behavior.
217
288
  result.then(
218
- () => {
219
- logger?.({
220
- source: "producer",
221
- operation: "close",
222
- explicit: false,
223
- phase: "start",
224
- });
225
-
226
- try {
227
- controller.close();
228
-
229
- logger?.({
230
- source: "producer",
231
- operation: "close",
232
- explicit: false,
233
- phase: "complete",
234
- });
235
- } catch {
236
- logger?.({
237
- source: "producer",
238
- operation: "close",
239
- explicit: false,
240
- phase: "ignored",
241
- });
242
-
243
- // The stream is already closed by the producer,
244
- // Or cancelled by the consumer.
245
- }
246
- },
247
- (e) => {
248
- logger?.({
249
- source: "producer",
250
- operation: "error",
251
- explicit: false,
252
- phase: "start",
253
- });
254
-
255
- controller.error(e);
256
-
257
- logger?.({
258
- source: "producer",
259
- operation: "error",
260
- explicit: false,
261
- phase: "complete",
262
- });
263
- },
289
+ () => close(false),
290
+ (e) => error(e, false),
264
291
  );
265
292
  }
266
293
  },
@@ -272,7 +299,8 @@ export class PushReadableStream<T> extends ReadableStream<T> {
272
299
  });
273
300
 
274
301
  if (waterMarkLow) {
275
- waterMarkLow.resolve();
302
+ waterMarkLow.resolve(undefined);
303
+ waterMarkLow = undefined;
276
304
  } else if (strategy?.highWaterMark === 0) {
277
305
  zeroHighWaterMarkAllowEnqueue = true;
278
306
  }
@@ -290,9 +318,9 @@ export class PushReadableStream<T> extends ReadableStream<T> {
290
318
  phase: "start",
291
319
  });
292
320
 
321
+ stopped = true;
293
322
  abortController.abort(reason);
294
- // Resolve it on cancellation. `pull` will check `abortSignal.aborted` again.
295
- waterMarkLow?.resolve();
323
+ waterMarkLow?.reject();
296
324
 
297
325
  logger?.({
298
326
  source: "consumer",
@@ -1,12 +1,42 @@
1
- import { TransformStream } from "./stream.js";
1
+ import type { TransformStreamDefaultController } from "./global/index.js";
2
+ import { TransformStream } from "./global/index.js";
2
3
 
3
4
  export class SplitStringStream extends TransformStream<string, string> {
4
- constructor(separator: string) {
5
+ constructor(
6
+ separator: string,
7
+ options?: {
8
+ trim?: boolean | undefined;
9
+ trimEnd?: boolean | undefined;
10
+ skipEmpty?: boolean | undefined;
11
+ },
12
+ ) {
5
13
  let remaining: string | undefined = undefined;
14
+ const separatorLength = separator.length;
15
+ if (separatorLength === 0) {
16
+ throw new Error("separator must not be empty");
17
+ }
18
+
19
+ const trim = !!options?.trim;
20
+ const trimEnd = !!options?.trimEnd;
21
+ const skipEmpty = !!options?.skipEmpty;
22
+ const enqueue = (
23
+ controller: TransformStreamDefaultController<string>,
24
+ value: string,
25
+ ) => {
26
+ if (trim) {
27
+ value = value.trim();
28
+ } else if (trimEnd) {
29
+ value = value.trimEnd();
30
+ }
31
+
32
+ if (value || !skipEmpty) {
33
+ controller.enqueue(value);
34
+ }
35
+ };
6
36
 
7
37
  super({
8
38
  transform(chunk, controller) {
9
- if (remaining) {
39
+ if (remaining !== undefined) {
10
40
  chunk = remaining + chunk;
11
41
  remaining = undefined;
12
42
  }
@@ -15,17 +45,21 @@ export class SplitStringStream extends TransformStream<string, string> {
15
45
  while (start < chunk.length) {
16
46
  const index = chunk.indexOf(separator, start);
17
47
  if (index === -1) {
48
+ // `remaining` can't be an empty string
49
+ // because `start` is less than `chunk.length`
18
50
  remaining = chunk.substring(start);
19
51
  break;
20
52
  }
21
53
 
22
- controller.enqueue(chunk.substring(start, index));
23
- start = index + 1;
54
+ const value = chunk.substring(start, index);
55
+ enqueue(controller, value);
56
+
57
+ start = index + separatorLength;
24
58
  }
25
59
  },
26
60
  flush(controller) {
27
- if (remaining) {
28
- controller.enqueue(remaining);
61
+ if (remaining !== undefined) {
62
+ enqueue(controller, remaining);
29
63
  }
30
64
  },
31
65
  });
@@ -5,7 +5,7 @@ import { BufferedTransformStream } from "./buffered-transform.js";
5
5
  export class StructDeserializeStream<T> extends BufferedTransformStream<T> {
6
6
  constructor(struct: StructDeserializer<T>) {
7
7
  super((stream) => {
8
- return struct.deserialize(stream) as never;
8
+ return struct.deserialize(stream);
9
9
  });
10
10
  }
11
11
  }