@zukhruf/async 0.0.0-stage → 0.3.15
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/LICENSE +21 -0
- package/README.md +54 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/latch.d.ts +15 -0
- package/dist/latch.d.ts.map +1 -0
- package/dist/latch.js +21 -0
- package/dist/latch.js.map +1 -0
- package/dist/until-aborted.d.ts +7 -0
- package/dist/until-aborted.d.ts.map +1 -0
- package/dist/until-aborted.js +22 -0
- package/dist/until-aborted.js.map +1 -0
- package/package.json +41 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ezzabuzaid
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zukhruf/async
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Helpers for code that waits for promises: `untilAborted` cancels a wait, and `Latch` holds callers until something happened. `@zukhruf/mutex` and `@zukhruf/single-flight` use them.
|
|
4
|
+
|
|
5
|
+
The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
|
|
6
|
+
|
|
7
|
+
## Cancel a wait
|
|
8
|
+
|
|
9
|
+
A caller waits for a promise. The caller wants to stop the wait when a signal aborts, for example after a time limit, or when the user stops the program. `untilAborted` gives a promise that settles in the same way as the promise of the caller. When the signal aborts first, it rejects with the reason of the signal.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { untilAborted } from '@zukhruf/async';
|
|
13
|
+
|
|
14
|
+
const report = buildReport(); // The work. It does not take a signal.
|
|
15
|
+
const value = await untilAborted(report, AbortSignal.timeout(5_000));
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- When `report` settles first, `value` is its value, or the call rejects with its error.
|
|
19
|
+
- When the 5 seconds end first, the call rejects with the reason of the signal: a `TimeoutError`.
|
|
20
|
+
- When the signal aborted before the call, the call rejects at once.
|
|
21
|
+
- When the signal is `undefined`, the call settles in the same way as `report`.
|
|
22
|
+
|
|
23
|
+
## A cancel stops the wait, not the work
|
|
24
|
+
|
|
25
|
+
`untilAborted` cannot stop the work. In the example, `buildReport` continues after the cancel. Thus a caller that cancels must deal with the work that continues. For example, a caller of `MemoryStore` that cancels keeps its place in the line, and its place passes the key on when its turn comes.
|
|
26
|
+
|
|
27
|
+
To stop the work too, give the signal to the work, for example `fetch(url, { signal })`.
|
|
28
|
+
|
|
29
|
+
## The wait ends also when another listener stops the event
|
|
30
|
+
|
|
31
|
+
An `AbortSignal` is an `EventTarget`. A listener of the `abort` event can call `event.stopImmediatePropagation()`. Then the listeners that were added after it do not run. A caller often shares its signal with other code. Thus a wait that listens with `addEventListener` can stay pending after the abort.
|
|
32
|
+
|
|
33
|
+
`untilAborted` listens through `events.addAbortListener` of Node.js. That listener runs also on a stopped event. See [ADR 0001](./docs/adr/0001-a-wait-listens-for-the-abort-through-addabortlistener.md).
|
|
34
|
+
|
|
35
|
+
## No listener stays on the signal
|
|
36
|
+
|
|
37
|
+
When the promise settles, `untilAborted` removes its listener. Thus many waits on one signal that lives long, for example a signal that stops a server, do not add listeners to it.
|
|
38
|
+
|
|
39
|
+
## Wait until something happened, with a latch
|
|
40
|
+
|
|
41
|
+
A latch starts closed. It opens once, with a value. A wait on a closed latch ends when the latch opens, and gets the value. A wait on an open latch ends at once, with the same value. A latch never closes again, and a second `open` changes nothing.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { Latch } from '@zukhruf/async';
|
|
45
|
+
|
|
46
|
+
const released = new Latch(); // Carries no value.
|
|
47
|
+
const done = new Latch<string>(); // Carries a string.
|
|
48
|
+
|
|
49
|
+
done.open('report');
|
|
50
|
+
const value = await done.wait(); // 'report'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A latch never rejects. It keeps its value in a box, so it never adopts a promise, whatever its type. Thus a class can keep a latch in a field: the latch cannot fail before somebody awaits it. A latch whose type names a promise refuses one at compile time, because a wait would adopt the promise and could then reject. To share the outcome of work that can fail, give the latch a value that tells how the work ended, for example the `PromiseSettledResult` of `Promise.allSettled`. Each caller then throws the error for itself. See [ADR 0002](./docs/adr/0002-a-latch-carries-a-value-never-a-failure.md).
|
|
54
|
+
|
|
55
|
+
To stop a wait on a latch, cancel it: `untilAborted(latch.wait(), signal)`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/latch.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A gate that starts closed and opens once, with a value. It never closes
|
|
3
|
+
* again and never rejects, so a wait that starts after it opened ends at once
|
|
4
|
+
* with the same value.
|
|
5
|
+
*/
|
|
6
|
+
export declare class Latch<T = void> {
|
|
7
|
+
#private;
|
|
8
|
+
/**
|
|
9
|
+
* A later open changes nothing. A type that names a promise is refused: a
|
|
10
|
+
* wait would adopt the promise, and could then reject.
|
|
11
|
+
*/
|
|
12
|
+
open(value: T extends PromiseLike<unknown> ? never : T): void;
|
|
13
|
+
wait(): Promise<T>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=latch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"latch.d.ts","sourceRoot":"","sources":["../src/latch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,qBAAa,KAAK,CAAC,CAAC,GAAG,IAAI;;IAKzB;;;OAGG;IACH,IAAI,CAAC,KAAK,EAAE,CAAC,SAAS,WAAW,CAAC,OAAO,CAAC,GAAG,KAAK,GAAG,CAAC,GAAG,IAAI;IAI7D,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC;CAGnB"}
|
package/dist/latch.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A gate that starts closed and opens once, with a value. It never closes
|
|
3
|
+
* again and never rejects, so a wait that starts after it opened ends at once
|
|
4
|
+
* with the same value.
|
|
5
|
+
*/
|
|
6
|
+
export class Latch {
|
|
7
|
+
/** Holds the value in a box, so it never adopts a promise and never rejects, whatever `T` is. */
|
|
8
|
+
// eslint-disable-next-line zukhruf/no-promise-field -- A latch never rejects and is never replaced, so no hazard of the rule applies; it is the primitive the rule recommends.
|
|
9
|
+
#opened = Promise.withResolvers();
|
|
10
|
+
/**
|
|
11
|
+
* A later open changes nothing. A type that names a promise is refused: a
|
|
12
|
+
* wait would adopt the promise, and could then reject.
|
|
13
|
+
*/
|
|
14
|
+
open(value) {
|
|
15
|
+
this.#opened.resolve({ value });
|
|
16
|
+
}
|
|
17
|
+
wait() {
|
|
18
|
+
return this.#opened.promise.then(({ value }) => value);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=latch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"latch.js","sourceRoot":"","sources":["../src/latch.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,OAAO,KAAK;IAChB,iGAAiG;IACjG,+KAA+K;IACtK,OAAO,GAAG,OAAO,CAAC,aAAa,EAAgB,CAAC;IAEzD;;;OAGG;IACH,IAAI,CAAC,KAAiD;QACpD,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IAClC,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC;CACF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settles like `promise`, or rejects with `signal.reason` first if `signal` aborts.
|
|
3
|
+
* Listens through `addAbortListener`, so a listener that stops the abort event
|
|
4
|
+
* cannot leave the wait pending.
|
|
5
|
+
*/
|
|
6
|
+
export declare function untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T>;
|
|
7
|
+
//# sourceMappingURL=until-aborted.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"until-aborted.d.ts","sourceRoot":"","sources":["../src/until-aborted.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAC5B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,CAAC,CAAC,CAgBZ"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { addAbortListener } from 'node:events';
|
|
2
|
+
/**
|
|
3
|
+
* Settles like `promise`, or rejects with `signal.reason` first if `signal` aborts.
|
|
4
|
+
* Listens through `addAbortListener`, so a listener that stops the abort event
|
|
5
|
+
* cannot leave the wait pending.
|
|
6
|
+
*/
|
|
7
|
+
export function untilAborted(promise, signal) {
|
|
8
|
+
if (!signal)
|
|
9
|
+
return promise;
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
// Also calls the listener when the signal aborted before the call.
|
|
12
|
+
const listening = addAbortListener(signal, () => reject(signal.reason));
|
|
13
|
+
promise.then((value) => {
|
|
14
|
+
listening[Symbol.dispose]();
|
|
15
|
+
resolve(value);
|
|
16
|
+
}, (error) => {
|
|
17
|
+
listening[Symbol.dispose]();
|
|
18
|
+
reject(error);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=until-aborted.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"until-aborted.js","sourceRoot":"","sources":["../src/until-aborted.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,OAAmB,EACnB,MAA+B;IAE/B,IAAI,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IAC5B,OAAO,IAAI,OAAO,CAAI,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACxC,mEAAmE;QACnE,MAAM,SAAS,GAAG,gBAAgB,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QACxE,OAAO,CAAC,IAAI,CACV,CAAC,KAAK,EAAE,EAAE;YACR,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;YACjB,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,KAAK,CAAC,CAAC;QAChB,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zukhruf/async",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.15",
|
|
4
|
+
"description": "Helpers for code that waits for promises: cancel a wait when an AbortSignal aborts, and hold callers on a one-shot latch that never rejects.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/JanuaryLabs/zukhruf",
|
|
9
|
+
"directory": "packages/async"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/JanuaryLabs/zukhruf/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/JanuaryLabs/zukhruf/tree/main/packages/async#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"abort",
|
|
17
|
+
"abortsignal",
|
|
18
|
+
"abortable",
|
|
19
|
+
"promise",
|
|
20
|
+
"async",
|
|
21
|
+
"latch"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
"./package.json": "./package.json",
|
|
27
|
+
".": {
|
|
28
|
+
"@zukhruf/source": "./src/index.ts",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"import": "./dist/index.js",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"!dist/spec/**",
|
|
37
|
+
"!**/*.tsbuildinfo",
|
|
38
|
+
"!**/*.test.*"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|