Skip to content

Commit

Permalink
debounce, throttle, latest combined use changes
Browse files Browse the repository at this point in the history
Allow debounce, throttle, and latest to be used in combination. Original code would throw an error if debounce or throttle was used in conjunction with latest, but now this is allowed.

Also I have reversed the order in which debounce and throttle are applied, first debouncing is applied, then throttling which I believe aligns best with most use cases.
  • Loading branch information
jeffbski committed Aug 23, 2016
1 parent 3abb50b commit 03e0307
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 26 deletions.
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ const fooLogic = createLogic({
// type and cancelType also support redux-actions fns for which
// the fn.toString() returns the associated action type

// limiting - optionally define one of these
latest: true, // only take latest, default false
// limiting - optionally define any of these
debounce: 0, // debounce for N ms, default 0
throttle: 0, // throttle for N ms, default 0
latest: true, // only take latest, default false

// Put your business logic into one or more of these
// execution phase hooks: validate, transform, process
Expand Down
4 changes: 2 additions & 2 deletions docs/where-business-logic.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,10 +484,10 @@ const fooLogic = createLogic({
type, // required str, regex, array of str/regex, use '*' for all
cancelType, // string, regex, array of strings or regexes

// limiting - optionally define one of these
latest, // only take latest, default false
// limiting - optionally define any of these
debounce, // debounce for N ms, default 0
throttle, // throttle for N ms, default 0
latest, // only take latest, default false

// Put your business logic into one or more of these
// execution phase hooks.
Expand Down
4 changes: 0 additions & 4 deletions src/createLogic.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,6 @@ export default function createLogic(logicOptions = {}) {
throw new Error('type is required, use \'*\' to match all actions');
}

if (latest && (debounce || throttle)) {
throw new Error('logic cannot use both latest and debounce/throttle');
}

if (validate && transform) {
throw new Error('logic cannot define both the validate and transform hooks they are aliases');
}
Expand Down
2 changes: 1 addition & 1 deletion src/logicWrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export default function logicWrapper(logic, store, deps) {
act$ => act$;

const limiting = act =>
debouncing(throttling(act));
throttling(debouncing(act));

return function wrappedLogic(actionIn$) {
// we want to share the same copy amongst all here
Expand Down
324 changes: 307 additions & 17 deletions test/createLogic.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import expect from 'expect';
import { createLogic } from '../src/index';
import Rx from 'rxjs';
import { createLogic, createLogicMiddleware } from '../src/index';

describe('createLogic', () => {
describe('createLogic()', () => {
Expand All @@ -18,27 +19,316 @@ describe('createLogic', () => {
});
});

describe('latest and debounce)', () => {
it('throws cannot use both error', () => {
expect(() => {
createLogic({
type: 'FOO',
latest: true,
debounce: 10
describe('debounce)', () => {
let dispatch;
beforeEach((done) => {
const next = expect.createSpy();
dispatch = expect.createSpy().andCall(check);
function check(action) {
// last dispatch should be slow: 3
if (action.slow === 3) { done(); }
}
const logicA = createLogic({
type: 'FOO',
debounce: 40,
process({ action }, dispatch) {
setTimeout(() => {
dispatch({
...action,
type: 'BAR'
});
}, 100); // delay so we can use latest
}
});
const mw = createLogicMiddleware([logicA]);
const storeFn = mw({ dispatch })(next);
Rx.Observable.merge(
// fast 0, 1, 2
Rx.Observable.interval(10)
.take(3)
.map(x => ({ fast: x })),
// slow 0, 1, 2, 3
Rx.Observable.interval(60)
.take(4)
.delay(40)
.map(x => ({ slow: x }))
).subscribe(x => {
storeFn({
...x,
type: 'FOO'
});
}).toThrow('cannot use both');
});
});

it('should debounce the fast calls', () => {
expect(dispatch.calls.length).toBe(5);
expect(dispatch.calls[0].arguments[0]).toEqual({
type: 'BAR',
fast: 2
});
});
});

describe('latest and throttle)', () => {
it('throws cannot use both error', () => {
expect(() => {
createLogic({
type: 'FOO',
latest: true,
throttle: 10
describe('debounce and latest)', () => {
let dispatch;
beforeEach((done) => {
const next = expect.createSpy();
dispatch = expect.createSpy().andCall(check);
function check(action) {
// last dispatch should be slow: 3
if (action.slow === 3) { done(); }
}
const logicA = createLogic({
type: 'FOO',
latest: true,
debounce: 40,
process({ action }, dispatch) {
setTimeout(() => {
dispatch({
...action,
type: 'BAR'
});
}, 100); // delay so we can use latest
}
});
const mw = createLogicMiddleware([logicA]);
const storeFn = mw({ dispatch })(next);
Rx.Observable.merge(
// fast 0, 1, 2
Rx.Observable.interval(10)
.take(3)
.map(x => ({ fast: x })),
// slow 0, 1, 2, 3
Rx.Observable.interval(60)
.take(4)
.delay(40)
.map(x => ({ slow: x }))
).subscribe(x => {
storeFn({
...x,
type: 'FOO'
});
}).toThrow('cannot use both');
});
});

it('should debounce and only use latest', () => {
expect(dispatch.calls.length).toBe(1);
expect(dispatch.calls[0].arguments[0]).toEqual({
type: 'BAR',
slow: 3
});
});
});

describe('throttle)', () => {
let dispatch;
beforeEach((done) => {
const asyncProcessDelay = 100; // simulate slow service
const next = expect.createSpy();
dispatch = expect.createSpy();
const logicA = createLogic({
type: 'FOO',
throttle: 40,
process({ action }, dispatch) {
setTimeout(() => {
dispatch({
...action,
type: 'BAR'
});
}, asyncProcessDelay); // delay so we can use latest
}
});
const mw = createLogicMiddleware([logicA]);
const storeFn = mw({ dispatch })(next);
Rx.Observable.merge(
// fast 0, 1, 2
Rx.Observable.interval(10)
.take(3)
.map(x => ({ fast: x })),
// slow 0, 1, 2, 3
Rx.Observable.interval(60)
.take(4)
.delay(40)
.map(x => ({ slow: x }))
).subscribe({
next: x => {
storeFn({
...x,
type: 'FOO'
});
},
complete: () => {
setTimeout(() => {
done();
}, asyncProcessDelay + 20); // add margin
}
});
});

it('should throttle', () => {
expect(dispatch.calls.length)
.toBeLessThan(7)
.toBeGreaterThan(3); // margin for CI test env
});
});

describe('throttle and latest', () => {
let dispatch;
beforeEach((done) => {
const asyncProcessDelay = 100; // simulate slow service
const next = expect.createSpy();
dispatch = expect.createSpy();
const logicA = createLogic({
type: 'FOO',
latest: true,
throttle: 40,
process({ action }, dispatch) {
setTimeout(() => {
dispatch({
...action,
type: 'BAR'
});
}, asyncProcessDelay); // delay so we can use latest
}
});
const mw = createLogicMiddleware([logicA]);
const storeFn = mw({ dispatch })(next);
Rx.Observable.merge(
// fast 0, 1, 2
Rx.Observable.interval(10)
.take(3)
.map(x => ({ fast: x })),
// slow 0, 1, 2, 3
Rx.Observable.interval(60)
.take(4)
.delay(40)
.map(x => ({ slow: x }))
).subscribe({
next: x => {
storeFn({
...x,
type: 'FOO'
});
},
complete: () => {
setTimeout(() => {
done();
}, asyncProcessDelay + 20); // add margin
}
});
});

it('should throttle and use latest', () => {
expect(dispatch.calls.length).toBe(1);
expect(dispatch.calls[0].arguments[0]).toEqual({
type: 'BAR',
slow: 3
});
});
});

describe('debounce and throttle', () => {
let dispatch;
beforeEach((done) => {
const asyncProcessDelay = 100; // simulate slow service
const next = expect.createSpy();
dispatch = expect.createSpy();
const logicA = createLogic({
type: 'FOO',
debounce: 30,
throttle: 80,
process({ action }, dispatch) {
setTimeout(() => {
dispatch({
...action,
type: 'BAR'
});
}, asyncProcessDelay); // delay so we can use latest
}
});
const mw = createLogicMiddleware([logicA]);
const storeFn = mw({ dispatch })(next);
Rx.Observable.merge(
// fast 0, 1, 2
Rx.Observable.interval(10)
.take(3)
.map(x => ({ fast: x })),
// slow 0, 1, 2, 3
Rx.Observable.interval(60)
.take(4)
.delay(40)
.map(x => ({ slow: x }))
).subscribe({
next: x => {
storeFn({
...x,
type: 'FOO'
});
},
complete: () => {
setTimeout(() => {
done();
}, asyncProcessDelay + 20); // add margin
}
});
});

it('should debounce and throttle', () => {
expect(dispatch.calls.length)
.toBeGreaterThan(1)
.toBeLessThan(5); // allow margin for CI env
});
});

describe('debounce, throttle, and latest', () => {
let dispatch;
beforeEach((done) => {
const asyncProcessDelay = 100; // simulate slow service
const next = expect.createSpy();
dispatch = expect.createSpy();
const logicA = createLogic({
type: 'FOO',
debounce: 30,
throttle: 80,
latest: true,
process({ action }, dispatch) {
setTimeout(() => {
dispatch({
...action,
type: 'BAR'
});
}, asyncProcessDelay); // delay so we can use latest
}
});
const mw = createLogicMiddleware([logicA]);
const storeFn = mw({ dispatch })(next);
Rx.Observable.merge(
// fast 0, 1, 2
Rx.Observable.interval(10)
.take(3)
.map(x => ({ fast: x })),
// slow 0, 1, 2, 3
Rx.Observable.interval(60)
.take(4)
.delay(40)
.map(x => ({ slow: x }))
).subscribe({
next: x => {
storeFn({
...x,
type: 'FOO'
});
},
complete: () => {
setTimeout(() => {
done();
}, asyncProcessDelay + 100); // add margin
}
});
});

it('should debounce, throttle, and use latest', () => {
expect(dispatch.calls.length).toBe(1);
});
});

Expand Down

0 comments on commit 03e0307

Please sign in to comment.