@statewalker/webrun-rpc 0.5.0 → 0.5.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/dist/async-dispose.d.ts +10 -0
- package/dist/async-dispose.d.ts.map +1 -0
- package/dist/connect-serve.d.ts.map +1 -1
- package/dist/duplex-over-port.d.ts.map +1 -1
- package/dist/index.js +87 -5
- package/dist/return-at-once.d.ts +17 -0
- package/dist/return-at-once.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/async-dispose.ts +22 -0
- package/src/close-signal.ts +1 -1
- package/src/connect-serve.ts +19 -3
- package/src/duplex-over-port.ts +9 -1
- package/src/return-at-once.ts +51 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
5
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
6
|
+
* declared types, which would require consumers to compile with the
|
|
7
|
+
* `esnext.disposable` lib.
|
|
8
|
+
*/
|
|
9
|
+
export declare function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T;
|
|
10
|
+
//# sourceMappingURL=async-dispose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-dispose.d.ts","sourceRoot":"","sources":["../src/async-dispose.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAa/F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAM1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"connect-serve.d.ts","sourceRoot":"","sources":["../src/connect-serve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAU,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAM1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAW1D,uEAAuE;AACvE,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,KAAK,OAAO,GAAG,SAAS,CAAC;AAElF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAE7E,MAAM,WAAW,UAAU;IACzB;;;;;;;;;;OAUG;IACH,GAAG,EAAE,cAAc,CAAC;IACpB;;;yEAGqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,UAAU,CAyCvC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,UAAU,CAqDnC,CAAC;AAEF,MAAM,WAAW,eAAe;IAC9B,mFAAmF;IACnF,KAAK,EAAE,SAAS,CAAC;IACjB;;;;OAIG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC;IACjC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,eAAe,GAAG,cAAc,CAEtF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAEjE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"duplex-over-port.d.ts","sourceRoot":"","sources":["../src/duplex-over-port.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EAOZ,MAAM,6BAA6B,CAAC;AAIrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"duplex-over-port.d.ts","sourceRoot":"","sources":["../src/duplex-over-port.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,MAAM,EAOZ,MAAM,6BAA6B,CAAC;AAIrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,4BAA4B,CAAC;AActD,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAE/F;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,qBAA0B,GAClC,MAAM,IAAI,CA8BZ"}
|
package/dist/index.js
CHANGED
|
@@ -468,6 +468,73 @@ async function* callBidi(port, input, { options = {}, ...params } = {}) {
|
|
|
468
468
|
if (outerError !== void 0) throw outerError;
|
|
469
469
|
}
|
|
470
470
|
//#endregion
|
|
471
|
+
//#region src/async-dispose.ts
|
|
472
|
+
/**
|
|
473
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
474
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
475
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
476
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
477
|
+
* declared types, which would require consumers to compile with the
|
|
478
|
+
* `esnext.disposable` lib.
|
|
479
|
+
*/
|
|
480
|
+
function asyncDisposable(target, dispose) {
|
|
481
|
+
const asyncDispose = Symbol.asyncDispose;
|
|
482
|
+
if (asyncDispose && !(asyncDispose in target)) Object.defineProperty(target, asyncDispose, {
|
|
483
|
+
value: async () => {
|
|
484
|
+
await dispose();
|
|
485
|
+
},
|
|
486
|
+
configurable: true,
|
|
487
|
+
writable: true
|
|
488
|
+
});
|
|
489
|
+
return target;
|
|
490
|
+
}
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/return-at-once.ts
|
|
493
|
+
/**
|
|
494
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
495
|
+
*
|
|
496
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
497
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
498
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
499
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
500
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
501
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
502
|
+
*
|
|
503
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
504
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
505
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
506
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
507
|
+
*/
|
|
508
|
+
function returningAtOnce(gen, abandon) {
|
|
509
|
+
let abandoned = false;
|
|
510
|
+
const giveUp = (reason) => {
|
|
511
|
+
if (abandoned) return;
|
|
512
|
+
abandoned = true;
|
|
513
|
+
abandon(reason);
|
|
514
|
+
};
|
|
515
|
+
const wrapped = {
|
|
516
|
+
next: (...args) => gen.next(...args).catch((error) => {
|
|
517
|
+
if (abandoned) return {
|
|
518
|
+
done: true,
|
|
519
|
+
value: void 0
|
|
520
|
+
};
|
|
521
|
+
throw error;
|
|
522
|
+
}),
|
|
523
|
+
return(value) {
|
|
524
|
+
giveUp();
|
|
525
|
+
return gen.return(value);
|
|
526
|
+
},
|
|
527
|
+
throw(error) {
|
|
528
|
+
giveUp(error);
|
|
529
|
+
return gen.throw(error);
|
|
530
|
+
},
|
|
531
|
+
[Symbol.asyncIterator]() {
|
|
532
|
+
return wrapped;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
return asyncDisposable(wrapped, () => wrapped.return(void 0));
|
|
536
|
+
}
|
|
537
|
+
//#endregion
|
|
471
538
|
//#region src/duplex-over-port.ts
|
|
472
539
|
/**
|
|
473
540
|
* The `type` of the out-of-band notice a side posts when it abandons a stream.
|
|
@@ -565,7 +632,7 @@ function runCallerSide(port, input, options) {
|
|
|
565
632
|
const inbound = receiveChunks(port, CHANNEL_OUT, controller, clock.touch);
|
|
566
633
|
const pump = sendChunks(port, CHANNEL_IN, input, options, controller.signal, clock.touch);
|
|
567
634
|
pump.catch(() => {});
|
|
568
|
-
return (async function* () {
|
|
635
|
+
return returningAtOnce((async function* () {
|
|
569
636
|
try {
|
|
570
637
|
yield* inbound.stream;
|
|
571
638
|
} finally {
|
|
@@ -579,7 +646,9 @@ function runCallerSide(port, input, options) {
|
|
|
579
646
|
await port.close?.();
|
|
580
647
|
} catch {}
|
|
581
648
|
}
|
|
582
|
-
})()
|
|
649
|
+
})(), () => {
|
|
650
|
+
if (!controller.signal.aborted) controller.abort(/* @__PURE__ */ new Error("webrun-rpc: the caller abandoned the stream"));
|
|
651
|
+
});
|
|
583
652
|
}
|
|
584
653
|
/**
|
|
585
654
|
* Listens for the peer's abort notice, and can post our own. The notice is a
|
|
@@ -981,9 +1050,22 @@ const connect = async ({ mux: factory, timeout }) => {
|
|
|
981
1050
|
maxMessageSize: mux.maxMessageSize,
|
|
982
1051
|
timeout
|
|
983
1052
|
};
|
|
984
|
-
const call = (input) =>
|
|
985
|
-
|
|
986
|
-
|
|
1053
|
+
const call = (input) => {
|
|
1054
|
+
let stream;
|
|
1055
|
+
let abandoned = false;
|
|
1056
|
+
return returningAtOnce((async function* () {
|
|
1057
|
+
const port = await mux.openPort(STREAM_META);
|
|
1058
|
+
if (abandoned) {
|
|
1059
|
+
await port.close?.();
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
1063
|
+
yield* stream;
|
|
1064
|
+
})(), () => {
|
|
1065
|
+
abandoned = true;
|
|
1066
|
+
stream?.return(void 0).catch(() => {});
|
|
1067
|
+
});
|
|
1068
|
+
};
|
|
987
1069
|
return {
|
|
988
1070
|
call,
|
|
989
1071
|
async close() {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
3
|
+
*
|
|
4
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
5
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
6
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
7
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
8
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
9
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
10
|
+
*
|
|
11
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
12
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
13
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
14
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
15
|
+
*/
|
|
16
|
+
export declare function returningAtOnce<T>(gen: AsyncGenerator<T>, abandon: (reason?: unknown) => void): AsyncGenerator<T>;
|
|
17
|
+
//# sourceMappingURL=return-at-once.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"return-at-once.d.ts","sourceRoot":"","sources":["../src/return-at-once.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,GAAG,EAAE,cAAc,CAAC,CAAC,CAAC,EACtB,OAAO,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,GAClC,cAAc,CAAC,CAAC,CAAC,CA8BnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-rpc",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Ports and RPC over them: a port multiplexer, and typed request/response and streaming primitives over any MessageTarget.",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"src"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@statewalker/webrun-streams": "^0.2.
|
|
33
|
+
"@statewalker/webrun-streams": "^0.2.3"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@biomejs/biome": "^2.5.15",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"rolldown": "^1.2.12",
|
|
40
40
|
"typescript": "^7.0.2",
|
|
41
41
|
"vitest": "^5.0.3",
|
|
42
|
-
"@statewalker/webrun-streams-conformance": "^0.3.
|
|
42
|
+
"@statewalker/webrun-streams-conformance": "^0.3.3"
|
|
43
43
|
},
|
|
44
44
|
"sideEffects": false,
|
|
45
45
|
"publishConfig": {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Makes `target` disposable with `await using`: installs `dispose` as its
|
|
3
|
+
* `[Symbol.asyncDispose]`, where the runtime defines the symbol. Hand-written
|
|
4
|
+
* iterators lose what a native generator gets from `AsyncIteratorPrototype`;
|
|
5
|
+
* this gives it back. A no-op where the symbol is missing. Kept off the
|
|
6
|
+
* declared types, which would require consumers to compile with the
|
|
7
|
+
* `esnext.disposable` lib.
|
|
8
|
+
*/
|
|
9
|
+
export function asyncDisposable<T extends object>(target: T, dispose: () => Promise<unknown>): T {
|
|
10
|
+
// Read here, not at module level: a top-level read is code a bundler keeps.
|
|
11
|
+
const asyncDispose = (Symbol as { asyncDispose?: symbol }).asyncDispose;
|
|
12
|
+
if (asyncDispose && !(asyncDispose in target)) {
|
|
13
|
+
Object.defineProperty(target, asyncDispose, {
|
|
14
|
+
value: async () => {
|
|
15
|
+
await dispose();
|
|
16
|
+
},
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return target;
|
|
22
|
+
}
|
package/src/close-signal.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import type { MessageTarget } from "./message-target.js";
|
|
24
24
|
|
|
25
|
-
const closeSignals = new WeakMap<MessageTarget, AbortSignal>();
|
|
25
|
+
const closeSignals = /* @__PURE__ */ new WeakMap<MessageTarget, AbortSignal>();
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Internal: register a port's close signal. Called once by `bindBytesToPort`.
|
package/src/connect-serve.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import type { MessageTarget } from "./message-target.js";
|
|
8
8
|
import { multiplexPort } from "./multiplex-port.js";
|
|
9
9
|
import type { PortCodec, PortMux } from "./port-types.js";
|
|
10
|
+
import { returningAtOnce } from "./return-at-once.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Announced with every port this pair opens. Layer 1 never reads `meta` — it is
|
|
@@ -74,19 +75,34 @@ export const connect: Connect<PortParams> = async ({ mux: factory, timeout }) =>
|
|
|
74
75
|
const mux = await factory();
|
|
75
76
|
const streamOptions: DuplexOverPortOptions = { maxMessageSize: mux.maxMessageSize, timeout };
|
|
76
77
|
|
|
77
|
-
const call: Duplex = (input) =>
|
|
78
|
+
const call: Duplex = (input) => {
|
|
79
|
+
let stream: AsyncGenerator<Uint8Array> | undefined;
|
|
80
|
+
let abandoned = false;
|
|
78
81
|
// The port is opened on the consumer's first pull, not when `call` is
|
|
79
82
|
// invoked. A caller that builds a stream and then drops it without
|
|
80
83
|
// iterating therefore costs the peer nothing — under eager opening it
|
|
81
84
|
// would burn a port (and over libp2p, a whole stream) for a call that
|
|
82
85
|
// never arrives.
|
|
83
|
-
(async function* () {
|
|
86
|
+
const outer = (async function* () {
|
|
84
87
|
const port = await mux.openPort(STREAM_META);
|
|
88
|
+
if (abandoned) {
|
|
89
|
+
// Given up while the port was being opened.
|
|
90
|
+
await port.close?.();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
stream = duplexOverPort(port, streamOptions)(input);
|
|
85
94
|
// `yield*` and not a hand-rolled pump: delegation already forwards
|
|
86
95
|
// `return()` and `throw()` into `duplexOverPort`'s generator, which is
|
|
87
96
|
// exactly the cancellation path `Duplex` specifies.
|
|
88
|
-
yield*
|
|
97
|
+
yield* stream;
|
|
89
98
|
})();
|
|
99
|
+
// Delegation forwards them only once the pending `next()` settles, though:
|
|
100
|
+
// giving up while the peer has not answered must reach the stream now.
|
|
101
|
+
return returningAtOnce(outer, () => {
|
|
102
|
+
abandoned = true;
|
|
103
|
+
void stream?.return(undefined).catch(() => {});
|
|
104
|
+
});
|
|
105
|
+
};
|
|
90
106
|
|
|
91
107
|
return {
|
|
92
108
|
call,
|
package/src/duplex-over-port.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { callPort, NO_TIMEOUT } from "./call-port.js";
|
|
|
12
12
|
import { getPortCloseSignal } from "./close-signal.js";
|
|
13
13
|
import { listenPort } from "./listen-port.js";
|
|
14
14
|
import type { MessageTarget } from "./message-target.js";
|
|
15
|
+
import { returningAtOnce } from "./return-at-once.js";
|
|
15
16
|
import { throughAbort } from "./through-abort.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -177,7 +178,7 @@ function runCallerSide(
|
|
|
177
178
|
// the consumer's contract is the inbound half.
|
|
178
179
|
});
|
|
179
180
|
|
|
180
|
-
|
|
181
|
+
const stream = (async function* () {
|
|
181
182
|
try {
|
|
182
183
|
yield* inbound.stream;
|
|
183
184
|
} finally {
|
|
@@ -200,6 +201,13 @@ function runCallerSide(
|
|
|
200
201
|
}
|
|
201
202
|
}
|
|
202
203
|
})();
|
|
204
|
+
// A caller that gives up while it waits for the peer's next chunk ends the
|
|
205
|
+
// stream now: the abort settles that wait, and the `finally` above runs.
|
|
206
|
+
return returningAtOnce(stream, () => {
|
|
207
|
+
if (!controller.signal.aborted) {
|
|
208
|
+
controller.abort(new Error("webrun-rpc: the caller abandoned the stream"));
|
|
209
|
+
}
|
|
210
|
+
});
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
/**
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { asyncDisposable } from "./async-dispose.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `gen`, except that `return()` and `throw()` take effect at once.
|
|
5
|
+
*
|
|
6
|
+
* On an async generator both are queued behind a `next()` that is still
|
|
7
|
+
* pending: a consumer that gives up on a stream while it waits for the peer's
|
|
8
|
+
* next chunk — an aborted fetch whose response head has not come, say — cannot
|
|
9
|
+
* end it until that chunk arrives, which for a peer that never answers is
|
|
10
|
+
* never. The stream's `finally` (which closes its port and tells the peer)
|
|
11
|
+
* then never runs, and on a multiplexer the port is kept for good.
|
|
12
|
+
*
|
|
13
|
+
* So `abandon` runs first, synchronously: it must make the pending `next()`
|
|
14
|
+
* settle (abort the stream), which then resolves done, after which the
|
|
15
|
+
* generator's own `return()` / `throw()` proceeds as usual. On a generator
|
|
16
|
+
* suspended at a `yield` it is the same ending, a step earlier. `abandon` runs once, whichever comes first.
|
|
17
|
+
*/
|
|
18
|
+
export function returningAtOnce<T>(
|
|
19
|
+
gen: AsyncGenerator<T>,
|
|
20
|
+
abandon: (reason?: unknown) => void,
|
|
21
|
+
): AsyncGenerator<T> {
|
|
22
|
+
let abandoned = false;
|
|
23
|
+
const giveUp = (reason?: unknown) => {
|
|
24
|
+
if (abandoned) return;
|
|
25
|
+
abandoned = true;
|
|
26
|
+
abandon(reason);
|
|
27
|
+
};
|
|
28
|
+
const wrapped: AsyncGenerator<T> = {
|
|
29
|
+
// A `next()` that was pending when the consumer gave up ends as the
|
|
30
|
+
// stream does — done — rather than rejecting with the abort it caused: a
|
|
31
|
+
// consumer that raced it against a timer may hold no handler for it.
|
|
32
|
+
next: (...args: [] | [unknown]) =>
|
|
33
|
+
gen.next(...args).catch((error: unknown) => {
|
|
34
|
+
if (abandoned) return { done: true, value: undefined } as IteratorResult<T>;
|
|
35
|
+
throw error;
|
|
36
|
+
}),
|
|
37
|
+
return(value) {
|
|
38
|
+
giveUp();
|
|
39
|
+
return gen.return(value);
|
|
40
|
+
},
|
|
41
|
+
throw(error) {
|
|
42
|
+
giveUp(error);
|
|
43
|
+
return gen.throw(error);
|
|
44
|
+
},
|
|
45
|
+
[Symbol.asyncIterator]() {
|
|
46
|
+
return wrapped;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
// `await using` ends the stream as leaving a `for await` early does.
|
|
50
|
+
return asyncDisposable(wrapped, () => wrapped.return(undefined));
|
|
51
|
+
}
|