Skip to content

Commit 2e585a2

Browse files
committed
feat(throttle): add handling of leading/trailing edges similar to lodash
1 parent 786f745 commit 2e585a2

File tree

2 files changed

+58
-5
lines changed

2 files changed

+58
-5
lines changed

src/throttle/index.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,29 @@ type EventAsReturnType<Payload> = any extends Payload ? Event<Payload> : never;
1515
export function throttle<T>(_: {
1616
source: Unit<T>;
1717
timeout: number | Store<number>;
18+
leading?: boolean | Store<boolean>;
19+
trailing?: boolean | Store<boolean>;
1820
name?: string;
1921
}): EventAsReturnType<T>;
2022
export function throttle<T, Target extends Unit<T>>(_: {
2123
source: Unit<T>;
2224
timeout: number | Store<number>;
2325
target: Target;
26+
leading?: boolean | Store<boolean>;
27+
trailing?: boolean | Store<boolean>;
2428
name?: string;
2529
}): Target;
2630
export function throttle<T>({
2731
source,
2832
timeout,
33+
leading,
34+
trailing,
2935
target = createEvent<T>(),
3036
}: {
3137
source: Unit<T>;
3238
timeout: number | Store<number>;
39+
leading?: boolean | Store<boolean>;
40+
trailing?: boolean | Store<boolean>;
3341
name?: string;
3442
target?: Unit<any>;
3543
}): EventAsReturnType<T> {
@@ -42,13 +50,18 @@ export function throttle<T>({
4250
);
4351

4452
// It's ok - nothing will ever start unless source is triggered
45-
const $payload = createStore<T>(null as unknown as T, { serialize: 'ignore' }).on(
46-
source,
47-
(_, payload) => payload,
48-
);
53+
const $payload = createStore<T>(null as unknown as T, {
54+
serialize: 'ignore',
55+
}).on(source, (_, payload) => payload);
4956

5057
const triggerTick = createEvent<T>();
5158

59+
const $leading = toStoreBoolean(leading, '$leading', false);
60+
const $trailing = toStoreBoolean(trailing, '$trailing', true);
61+
62+
const $neverCalled = createStore(true).on(target, () => false);
63+
const $lastCalled = createStore<number>(0).on(target, () => Date.now());
64+
5265
const $canTick = createStore(true, { serialize: 'ignore' })
5366
.on(triggerTick, () => false)
5467
.on(target, () => true);
@@ -66,8 +79,25 @@ export function throttle<T>({
6679
});
6780

6881
sample({
69-
source: $payload,
82+
clock: $payload,
83+
source: [$leading, $neverCalled],
84+
filter: ([leading, neverCalled]) => leading && neverCalled,
85+
target,
86+
});
87+
88+
sample({
89+
source: [$trailing, $payload] as const,
7090
clock: timerFx.done,
91+
filter: ([trailing]) => trailing,
92+
fn: ([_, payload]) => payload,
93+
target,
94+
});
95+
sample({
96+
clock: $payload,
97+
source: { trailing: $trailing, lastCalled: $lastCalled, timeout: $timeout },
98+
filter: ({ trailing, lastCalled, timeout }) =>
99+
!trailing && lastCalled + timeout < Date.now(),
100+
fn: (_src, clk) => clk,
71101
target,
72102
});
73103

@@ -88,3 +118,16 @@ function toStoreNumber(value: number | Store<number> | unknown): Store<number> {
88118
`timeout parameter should be number or Store. "${typeof value}" was passed`,
89119
);
90120
}
121+
122+
function toStoreBoolean(
123+
value: boolean | Store<boolean> | undefined,
124+
name: string,
125+
defaultValue: boolean,
126+
): Store<boolean> {
127+
if (is.store(value)) return value;
128+
if (typeof value === 'boolean') {
129+
return createStore(value, { name });
130+
} else {
131+
return createStore(defaultValue, { name });
132+
}
133+
}

src/throttle/readme.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ target = throttle({ source, timeout });
2525

2626
1. `source` ([_`Event`_] | [_`Store`_] | [_`Effect`_]) — Source unit, data from this unit used by the `target`
2727
1. `timeout` ([_`number`_] | `Store<number>`) — time to wait before trigger `target` after last trigger or `source` trigger
28+
1. `leading` ([_`boolean`_] | `Store<boolean>`) — trigger `target` on the leading edge of the `timeout`. If `true` first trigger of `source` causes
29+
immediate trigger of `target`, default is `false`, `target` will be first triggered only after `timeout`
30+
1. `trailing` ([_`boolean`_] | `Store<boolean>`) — trigger `target` on the trailing edge of the `timeout`. If `true` last trigger of `source`
31+
withing `timeout` causes trigger of `target` after `timeout` expires. If `false` any trigger of `source`
32+
will be ignored completely within the `timeout` not causing trigger of `target` after `timeout` expires.
2833

2934
### Returns
3035

@@ -105,6 +110,11 @@ throttle({ source, timeout, target });
105110
1. `source` ([_`Event`_] | [_`Store`_] | [_`Effect`_]) — Source unit, data from this unit used by the `target`
106111
1. `timeout` ([_`number`_] | `Store<number>`) — time to wait before trigger `target` after last trigger or `source` trigger
107112
1. `target` ([_`Event`_] | [_`Store`_] | [_`Effect`_]) — Target unit, that triggered each time after triggering `source` with argument from `source`
113+
1. `leading` ([_`boolean`_] | `Store<boolean>`) — trigger `target` on the leading edge of the `timeout`. If `true` first trigger of `source` causes
114+
immediate trigger of `target`, default is `false`, `target` will be first triggered only after `timeout`
115+
1. `trailing` ([_`boolean`_] | `Store<boolean>`) — trigger `target` on the trailing edge of the `timeout`. If `true` last trigger of `source`
116+
withing `timeout` causes trigger of `target` after `timeout` expires. If `false` any trigger of `source`
117+
will be ignored completely within the `timeout` not causing trigger of `target` after `timeout` expires.
108118

109119
### Returns
110120

0 commit comments

Comments
 (0)