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

Created debounce & throttle effects #27

Merged
merged 8 commits into from
Mar 4, 2018
Merged
Show file tree
Hide file tree
Changes from all 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
72 changes: 72 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,78 @@ const actions = {
withFx(app)(state, actions).foo()
```

### `debounce`

```js
debounce = (wait: number, action: string, data?: any) => EffectTuple
```

Describes an effect that will call an action after waiting for a delay to have passed, the delay will be reset each time the action is called.

Example:

```js
import { withFx, debounce } from "@hyperapp/fx"

const state = {
// ...
}

const actions = {
waitForLastInput: (input) => debounce(
500,
"search",
{ query: input }
),
search: data => {
// This action will run after waiting
// for 500ms since the last call.
// This action will only be called once
// data will have { query: "hyperapp" }
}
}

const ha = withFx(app)(state, actions)
ha.waitForLastInput("hyper")
ha.waitForLastInput("hyperapp")
```
### `throttle`

```js
throttle = (rate: number, action: string, data?: any) => EffectTuple
```

Describes an effect that will call an action at a maximum rate. Where `rate` is 1 call per `rate` miliseconds

Example:

```js
import { withFx, throttle } from '@hyperapp/fx'

const state = {
// ...
}

const actions = {
doExpensiveAction: (param) => throttle(
500,
"calculate",
{ foo: param }
),
expensiveAction: data => {
// This action will only run once per rate limit
// This action will only be called twice
// data will receive { foo: "foo" }, { foo: "baz" }
}
}

const ha = withFx(app)(state, actions)
ha.doExpensiveAction("foo")
ha.doExpensiveAction("bar")
setTimeout(function () { ha.doExpensiveAction("baz") })
```


## License

Hyperapp FX is MIT licensed. See [LICENSE](LICENSE.md).
26 changes: 25 additions & 1 deletion src/fxCreators.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
EVENT,
KEY_DOWN,
KEY_UP,
RANDOM
RANDOM,
DEBOUNCE,
THROTTLE
} from "./fxTypes"

export function action(name, data) {
Expand Down Expand Up @@ -107,3 +109,25 @@ export function random(action, min, max) {
}
]
}

export function debounce (wait, action, data) {
return [
DEBOUNCE,
{
wait: wait,
action: action,
data: data
}
]
}

export function throttle (rate, action, data) {
return [
THROTTLE,
{
rate: rate,
action: action,
data: data
}
]
}
2 changes: 2 additions & 0 deletions src/fxTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export var EVENT = "event"
export var KEY_DOWN = "keydown"
export var KEY_UP = "keyup"
export var RANDOM = "random"
export var DEBOUNCE = "debounce"
export var THROTTLE = "throttle"
27 changes: 26 additions & 1 deletion src/makeDefaultFx.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import {
EVENT,
KEY_DOWN,
KEY_UP,
RANDOM
RANDOM,
DEBOUNCE,
THROTTLE
} from "./fxTypes"
import { assign, omit } from "./utils.js"

Expand Down Expand Up @@ -87,5 +89,28 @@ export default function makeDefaultFx() {
getAction(props.action)(randomValue)
}

var debounceTimeouts = {}
fx[DEBOUNCE] = function(props, getAction) {
return (function(props, getAction) {
clearTimeout(debounceTimeouts[props.action])
debounceTimeouts[props.action] = setTimeout(function () {
getAction(props.action)(props.data)
}, props.wait)
})(props, getAction)
}

var throttleLocks = {}
fx[THROTTLE] = function(props, getAction) {
return (function (props, getAction) {
if(!throttleLocks[props.action]) {
getAction(props.action)(props.data)
throttleLocks[props.action] = true
setTimeout(function () {
throttleLocks[props.action] = false
}, props.rate)
}
})(props, getAction)
}

return fx
}
125 changes: 124 additions & 1 deletion test/withFx.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
event,
keydown,
keyup,
random
random,
debounce,
throttle
} from "../src"

describe("withFx", () => {
Expand Down Expand Up @@ -508,6 +510,127 @@ describe("withFx", () => {
Math.random = defaultRandom
})
})
describe("debounce", () => {
it("should fire an action after a delay", () => {
jest.useFakeTimers()
try {
const main = withFx(app)(
{},
{
get: () => state => state,
foo: () => debounce(1000, "bar.baz", { updated: "data" }),
bar: {
baz: data => data
}
},
Function.prototype
)
main.foo()
expect(main.get()).toEqual({ bar: {} })
jest.runAllTimers()
expect(main.get()).toEqual({ bar: { updated: "data" } })
} finally {
jest.useRealTimers()
}
})
it("should not execute an action until the delay has passed", () => {
jest.useFakeTimers()
try {
const main = withFx(app)(
{},
{
get: () => state => state,
foo: (data) => debounce(1000, "bar.baz", data),
bar: {
baz: data => data
}
},
Function.prototype
)
jest.spyOn(main.bar, 'baz')
main.foo({ data: "updated" })
expect(main.bar.baz).toHaveBeenCalledTimes(0)
expect(main.get()).toEqual({ bar: {} })
jest.runAllTimers()
expect(main.bar.baz).toHaveBeenCalledTimes(1)
expect(main.get()).toEqual({ bar: { data: "updated" } })
} finally {
jest.useRealTimers()
}
})
it("should receive the data of the last attempted action call", () => {
jest.useFakeTimers()
try {
const main = withFx(app)(
{},
{
get: () => state => state,
foo: (data) => debounce(1000, "bar.baz", data),
bar: {
baz: data => data
}
},
Function.prototype
)
jest.spyOn(main.bar, 'baz')
main.foo({ data: "first" })
main.foo({ data: "last"})
jest.runAllTimers()
expect(main.get()).toEqual({ bar: { data: "last" } })
} finally {
jest.useRealTimers()
}
})
})
describe("throttle", () => {
it("should execute an action within a limit", () => {
jest.useFakeTimers()
try {
const main = withFx(app)(
{},
{
get: () => state => state,
foo: () => throttle(1000, "bar.baz", { updated: "data" }),
bar: {
baz: data => data
}
},
Function.prototype
)
main.foo()
expect(main.get()).toEqual({ bar: { updated: "data" } })
jest.runAllTimers()
} finally {
jest.useRealTimers()
}
})
it("should only execute an action once within a limit", () => {
jest.useFakeTimers()
try {
const main = withFx(app)(
{},
{
get: () => state => state,
foo: () => throttle(1000, "bar.baz", { updated: "data" }),
bar: {
baz: data => data
}
},
Function.prototype
)
jest.spyOn(main.bar, "baz")
main.foo({ updated: "data" })
main.foo({ updated: "again" })
expect(main.bar.baz).toHaveBeenCalledTimes(1)
expect(main.get()).toEqual({ bar: { updated: "data" } })
jest.runAllTimers()
expect(main.bar.baz).toHaveBeenCalledTimes(1)
expect(main.get()).toEqual({ bar: { updated: "data" } })
} finally {
jest.useRealTimers()
}
})
})
})
it("should allow combining action and event fx in view", done => {
document.body.innerHTML = ""
Expand Down