Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mobx): add support for AbortSignal for reaction, autorun and sync when #3727

Merged
merged 7 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thirty-tools-confess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"mobx": minor
---

Added support for `signal` (AbortSignal) in `autorun` and `reaction` options to dispose those
6 changes: 4 additions & 2 deletions docs/reactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,11 @@ 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)_
### `signal`

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.
An AbortSignal object instance
for `when` 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.
for `autorun` and `reaction` allow you to dispose the reaction via the AbortController.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace with

An AbortSignal object instance; can be used as an alternative method for disposal.<br>
`when`: This option is available only for the promise based version. When aborted, the promise rejects with the "WHEN_ABORTED" error.

Feel free to fix grammar or typos if necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading:

when: This option is available only

it seems like it's only for when

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean like the whole thing? Btw when delegates to autorun, can't we now support the non-promise based version as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are right, adding support

Copy link
Contributor Author

@rluvaton rluvaton Jul 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, the non-promised version won't throw an error as it won't get caught...


### `onError`

Expand Down
31 changes: 31 additions & 0 deletions packages/mobx/__tests__/v5/base/autorun.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,37 @@ test("autorun can be disposed on first run", function () {
expect(values).toEqual([1])
})

test("autorun can be disposed using AbortSignal", function () {
const a = mobx.observable.box(1)
const ac = new AbortController()
const values = []

mobx.autorun(() => {
values.push(a.get())
}, { signal: ac.signal })

a.set(2)
a.set(3)
ac.abort()
a.set(4)

expect(values).toEqual([1, 2, 3])
})

test("autorun should not run first time when passing already aborted AbortSignal", function () {
const a = mobx.observable.box(1)
const ac = new AbortController()
const values = []

ac.abort()

mobx.autorun(() => {
values.push(a.get())
}, { signal: ac.signal })

expect(values).toEqual([])
})

test("autorun warns when passed an action", function () {
const action = mobx.action(() => {})
expect.assertions(1)
Expand Down
42 changes: 42 additions & 0 deletions packages/mobx/__tests__/v5/base/reaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,48 @@ test("can dispose reaction on first run", () => {
expect(valuesEffect).toEqual([])
})

test("can dispose reaction with AbortSignal", () => {
const a = mobx.observable.box(1)
const ac = new AbortController()
const values = []

reaction(
() => a.get(),
(newValue, oldValue) => {
values.push([newValue, oldValue])
},
{ signal: ac.signal }
)

a.set(2)
a.set(3)
ac.abort()
a.set(4)

expect(values).toEqual([
[2, 1],
[3, 2]
])
})

test("fireImmediately should not be honored when passed already aborted AbortSignal", () => {
const a = mobx.observable.box(1)
const ac = new AbortController()
const values = []

ac.abort()

reaction(
() => a.get(),
(newValue) => {
values.push(newValue)
},
{ signal: ac.signal, fireImmediately: true }
)

expect(values).toEqual([])
})

test("#278 do not rerun if expr output doesn't change", () => {
const a = mobx.observable.box(1)
const values = []
Expand Down
16 changes: 11 additions & 5 deletions packages/mobx/src/api/autorun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
isFunction,
isPlainObject,
die,
allowStateChanges
allowStateChanges,
GenericAbortSignal
} from "../internal"

export interface IAutorunOptions {
Expand All @@ -25,6 +26,7 @@ export interface IAutorunOptions {
requiresObservable?: boolean
scheduler?: (callback: () => void) => any
onError?: (error: any) => void
signal?: GenericAbortSignal
}

/**
Expand Down Expand Up @@ -88,8 +90,10 @@ export function autorun(
view(reaction)
}

reaction.schedule_()
return reaction.getDisposer_()
if(!opts?.signal?.aborted) {
reaction.schedule_()
}
return reaction.getDisposer_(opts?.signal)
}

export type IReactionOptions<T, FireImmediately extends boolean> = IAutorunOptions & {
Expand Down Expand Up @@ -178,8 +182,10 @@ export function reaction<T, FireImmediately extends boolean = false>(
firstTime = false
}

r.schedule_()
return r.getDisposer_()
if(!opts?.signal?.aborted) {
r.schedule_()
}
return r.getDisposer_(opts?.signal)
}

function wrapErrorHandler(errorHandler, baseFn) {
Expand Down
11 changes: 2 additions & 9 deletions packages/mobx/src/api/when.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,10 @@ import {
createAction,
getNextId,
die,
allowStateChanges
allowStateChanges,
GenericAbortSignal
} from "../internal"

// https://github.com/mobxjs/mobx/issues/3582
interface GenericAbortSignal {
readonly aborted: boolean
onabort?: ((...args: any) => any) | null
addEventListener?: (...args: any) => any
removeEventListener?: (...args: any) => any
}

export interface IWhenOptions {
name?: string
timeout?: number
Expand Down
15 changes: 10 additions & 5 deletions packages/mobx/src/core/reaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
spyReportStart,
startBatch,
trace,
trackDerivedFunction
trackDerivedFunction, GenericAbortSignal
} from "../internal"

/**
Expand Down Expand Up @@ -195,10 +195,15 @@ export class Reaction implements IDerivation, IReactionPublic {
}
}

getDisposer_(): IReactionDisposer {
const r = this.dispose.bind(this) as IReactionDisposer
r[$mobx] = this
return r
getDisposer_(abortSignal?: GenericAbortSignal): IReactionDisposer {
const dispose = (() => {
this.dispose()
abortSignal?.removeEventListener?.("abort", dispose)
}) as IReactionDisposer
abortSignal?.addEventListener?.("abort", dispose)
dispose[$mobx] = this

return dispose
}

toString() {
Expand Down
1 change: 1 addition & 0 deletions packages/mobx/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from "./types/flowannotation"
export * from "./types/computedannotation"
export * from "./types/observableannotation"
export * from "./types/autoannotation"
export * from "./types/generic-abort-signal"
export * from "./api/observable"
export * from "./api/computed"
export * from "./core/action"
Expand Down
7 changes: 7 additions & 0 deletions packages/mobx/src/types/generic-abort-signal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// https://github.com/mobxjs/mobx/issues/3582
export interface GenericAbortSignal {
readonly aborted: boolean
onabort?: ((...args: any) => any) | null
addEventListener?: (...args: any) => any
removeEventListener?: (...args: any) => any
}