Skip to content

Commit

Permalink
Created debounce & throttle effects (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
rmc authored and okwolf committed Mar 4, 2018
1 parent db06834 commit 7436416
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 3 deletions.
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

0 comments on commit 7436416

Please sign in to comment.