@yume-chan/stream-extra 2.1.0 → 2.6.1
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/esm/push-readable.d.ts +18 -1
- package/esm/push-readable.d.ts.map +1 -1
- package/esm/push-readable.js +179 -179
- package/esm/push-readable.js.map +1 -1
- package/esm/task-queue.d.ts +8 -0
- package/esm/task-queue.d.ts.map +1 -0
- package/esm/task-queue.js +50 -0
- package/esm/task-queue.js.map +1 -0
- package/esm/try-close.d.ts +7 -5
- package/esm/try-close.d.ts.map +1 -1
- package/esm/try-close.js +6 -2
- package/esm/try-close.js.map +1 -1
- package/package.json +7 -7
- package/src/push-readable.ts +225 -197
- package/src/task-queue.ts +69 -0
- package/src/try-close.ts +16 -16
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/push-readable.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MaybePromise } from "@yume-chan/async";
|
|
2
|
+
import { isPromiseLike, PromiseResolver } from "@yume-chan/async";
|
|
2
3
|
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AbortSignal,
|
|
6
|
+
QueuingStrategy,
|
|
7
|
+
ReadableStreamDefaultController,
|
|
8
|
+
} from "./stream.js";
|
|
4
9
|
import { AbortController, ReadableStream } from "./stream.js";
|
|
10
|
+
import { TaskQueue } from "./task-queue.js";
|
|
5
11
|
|
|
6
12
|
export interface PushReadableStreamController<T> {
|
|
7
13
|
abortSignal: AbortSignal;
|
|
8
14
|
|
|
9
|
-
|
|
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
|
|
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: (
|
|
265
|
+
start: (controller_) => {
|
|
266
|
+
controller = controller_;
|
|
267
|
+
|
|
61
268
|
const result = source({
|
|
62
269
|
abortSignal: abortController.signal,
|
|
63
|
-
enqueue: async (chunk) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
295
|
-
waterMarkLow?.resolve();
|
|
323
|
+
waterMarkLow?.reject();
|
|
296
324
|
|
|
297
325
|
logger?.({
|
|
298
326
|
source: "consumer",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { MaybePromise, MaybePromiseLike } from "@yume-chan/async";
|
|
2
|
+
import { isPromiseLike } from "@yume-chan/async";
|
|
3
|
+
|
|
4
|
+
export class TaskQueue {
|
|
5
|
+
#ready: PromiseLike<unknown> | undefined;
|
|
6
|
+
#disposed = false;
|
|
7
|
+
|
|
8
|
+
enqueue<T extends MaybePromiseLike<unknown>>(
|
|
9
|
+
task: () => T,
|
|
10
|
+
bail?: boolean,
|
|
11
|
+
): T;
|
|
12
|
+
enqueue<T>(task: () => T, bail?: boolean): MaybePromise<T>;
|
|
13
|
+
enqueue<T>(
|
|
14
|
+
task: () => MaybePromiseLike<T>,
|
|
15
|
+
bail = false,
|
|
16
|
+
): MaybePromiseLike<T> {
|
|
17
|
+
if (this.#disposed) {
|
|
18
|
+
throw new Error("TaskQueue is disposed");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!this.#ready) {
|
|
22
|
+
// Init state or all previous tasks are synchronous
|
|
23
|
+
try {
|
|
24
|
+
const result = task();
|
|
25
|
+
if (isPromiseLike(result)) {
|
|
26
|
+
this.#ready = result.then(
|
|
27
|
+
() => {},
|
|
28
|
+
(e) => {
|
|
29
|
+
if (bail) {
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (bail) {
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
|
|
39
|
+
const promise = Promise.reject(e);
|
|
40
|
+
// Suppress unhandled-rejection without resolving `#ready`
|
|
41
|
+
void promise.catch(() => {});
|
|
42
|
+
this.#ready = promise;
|
|
43
|
+
}
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = this.#ready.then(() => {
|
|
49
|
+
if (this.#disposed) {
|
|
50
|
+
throw new Error("TaskQueue is disposed");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return task();
|
|
54
|
+
});
|
|
55
|
+
this.#ready = result.then(
|
|
56
|
+
() => {},
|
|
57
|
+
(e) => {
|
|
58
|
+
if (bail || this.#disposed) {
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
dispose() {
|
|
67
|
+
this.#disposed = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/try-close.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
+
import type { MaybePromiseLike } from "@yume-chan/async";
|
|
2
|
+
import { isPromiseLike } from "@yume-chan/async";
|
|
3
|
+
|
|
1
4
|
import type { BufferedReadableStream } from "./buffered.js";
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
4
|
-
ReadableStream,
|
|
5
|
-
ReadableStreamDefaultController,
|
|
6
|
-
ReadableStreamDefaultReader,
|
|
7
|
-
WritableStreamDefaultWriter,
|
|
8
|
-
} from "./stream.js";
|
|
5
|
+
import type { ReadableStream, ReadableStreamDefaultReader } from "./stream.js";
|
|
9
6
|
|
|
10
|
-
export function tryClose(
|
|
11
|
-
|
|
12
|
-
): boolean
|
|
13
|
-
export function tryClose(
|
|
14
|
-
|
|
15
|
-
): boolean;
|
|
16
|
-
export function tryClose(writer: WritableStreamDefaultWriter<never>): boolean;
|
|
17
|
-
export function tryClose(controller: { close(): void }) {
|
|
7
|
+
export function tryClose(value: {
|
|
8
|
+
close(): PromiseLike<void>;
|
|
9
|
+
}): Promise<boolean>;
|
|
10
|
+
export function tryClose(value: { close(): void }): boolean;
|
|
11
|
+
export function tryClose(value: { close(): MaybePromiseLike<void> }) {
|
|
18
12
|
try {
|
|
19
|
-
|
|
13
|
+
const result = value.close();
|
|
14
|
+
if (isPromiseLike(result)) {
|
|
15
|
+
return result.then(
|
|
16
|
+
() => true,
|
|
17
|
+
() => false,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
20
|
return true;
|
|
21
21
|
} catch {
|
|
22
22
|
return false;
|