From 223e3688631528a327c79d39e2f497c6e1506165 Mon Sep 17 00:00:00 2001 From: Jesper Ek Date: Sat, 29 Oct 2022 13:44:59 +0200 Subject: [PATCH] feature: abort when() using AbortSignal (#3551) --- .changeset/popular-comics-tap.md | 5 +++++ docs/reactions.md | 4 ++++ package.json | 2 +- .../mobx/__tests__/v5/base/typescript-tests.ts | 14 ++++++++++++++ packages/mobx/src/api/when.ts | 12 +++++++++++- yarn.lock | 8 ++++---- 6 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 .changeset/popular-comics-tap.md diff --git a/.changeset/popular-comics-tap.md b/.changeset/popular-comics-tap.md new file mode 100644 index 000000000..747d707ac --- /dev/null +++ b/.changeset/popular-comics-tap.md @@ -0,0 +1,5 @@ +--- +"mobx": minor +--- + +Added new option `signal` to `when()`, to support abortion using an AbortSignal / AbortController. diff --git a/docs/reactions.md b/docs/reactions.md index 39d05dfef..c5ef1849f 100644 --- a/docs/reactions.md +++ b/docs/reactions.md @@ -364,6 +364,10 @@ Number of milliseconds that can be used to throttle the effect function. If zero Set a limited amount of time that `when` will wait for. If the deadline passes, `when` will reject / throw. +### `signal` _(when)_ + +An AbortSignal object instance; allows you to abort waiting for the reaction via an AbortController. This will also cause the returned promise to reject with an error "WHEN_ABORTED". This option is ignored when using an effect function, and only applies with the promised based version. + ### `onError` By default, any exception thrown inside an reaction will be logged, but not further thrown. This is to make sure that an exception in one reaction does not prevent the scheduled execution of other, possibly unrelated reactions. This also allows reactions to recover from exceptions. Throwing an exception does not break the tracking done by MobX, so subsequent runs of the reaction might complete normally again if the cause for the exception is removed. This option allows overriding that behavior. It is possible to set a global error handler or to disable catching errors completely using [configure](configuration.md#disableerrorboundaries-boolean). diff --git a/package.json b/package.json index 4d727c3b6..99632ccfa 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@testing-library/react": "^13.0.0", "@testing-library/react-hooks": "7.0.2", "@types/jest": "^26.0.15", - "@types/node": "14", + "@types/node": "18", "@types/prop-types": "^15.5.2", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", diff --git a/packages/mobx/__tests__/v5/base/typescript-tests.ts b/packages/mobx/__tests__/v5/base/typescript-tests.ts index 19ab29b52..1ec5cf6c3 100644 --- a/packages/mobx/__tests__/v5/base/typescript-tests.ts +++ b/packages/mobx/__tests__/v5/base/typescript-tests.ts @@ -1806,6 +1806,20 @@ test("promised when can be cancelled", async () => { } }) +test("promised when can be aborted", async () => { + const x = mobx.observable.box(1) + + try { + const ac = new AbortController() + const p = mobx.when(() => x.get() === 3, { signal: ac.signal }) + setTimeout(() => ac.abort(), 100) + await p + fail("should abort") + } catch (e) { + expect("" + e).toMatch(/WHEN_ABORTED/) + } +}) + test("it should support asyncAction as decorator (ts)", async () => { mobx.configure({ enforceActions: "observed" }) diff --git a/packages/mobx/src/api/when.ts b/packages/mobx/src/api/when.ts index 4c0439635..84c17f6ca 100644 --- a/packages/mobx/src/api/when.ts +++ b/packages/mobx/src/api/when.ts @@ -13,6 +13,7 @@ export interface IWhenOptions { name?: string timeout?: number onError?: (error: any) => void + signal?: AbortSignal } export function when( @@ -74,14 +75,23 @@ function whenPromise( if (__DEV__ && opts && opts.onError) { return die(`the options 'onError' and 'promise' cannot be combined`) } + if (opts?.signal?.aborted) { + return Object.assign(Promise.reject(new Error("WHEN_ABORTED")), { cancel: () => null }) + } let cancel + let abort const res = new Promise((resolve, reject) => { let disposer = _when(predicate, resolve as Lambda, { ...opts, onError: reject }) cancel = () => { disposer() reject(new Error("WHEN_CANCELLED")) } - }) + abort = () => { + disposer() + reject(new Error("WHEN_ABORTED")) + } + opts?.signal?.addEventListener("abort", abort) + }).finally(() => opts?.signal?.removeEventListener("abort", abort)) ;(res as any).cancel = cancel return res as any } diff --git a/yarn.lock b/yarn.lock index 22ceab2ef..e4f319f9b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2901,10 +2901,10 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/node@*", "@types/node@14", "@types/node@>= 8": - version "14.18.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.0.tgz#98df2397f6936bfbff4f089e40e06fa5dd88d32a" - integrity sha512-0GeIl2kmVMXEnx8tg1SlG6Gg8vkqirrW752KqolYo1PHevhhZN3bhJ67qHj+bQaINhX0Ra3TlWwRvMCd9iEfNQ== +"@types/node@*", "@types/node@18", "@types/node@>= 8": + version "18.11.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.4.tgz#7017a52e18dfaad32f55eebd539993014441949c" + integrity sha512-BxcJpBu8D3kv/GZkx/gSMz6VnTJREBj/4lbzYOQueUOELkt8WrO6zAcSPmp9uRPEW/d+lUO8QK0W2xnS1hEU0A== "@types/node@^12.7.1": version "12.20.37"