From 2e585a216fc4ca2062a1b628fd13b69a922b89cc Mon Sep 17 00:00:00 2001 From: Dmitry Olyenyov Date: Thu, 27 Apr 2023 10:52:47 +0300 Subject: [PATCH] feat(throttle): add handling of leading/trailing edges similar to lodash --- src/throttle/index.ts | 53 ++++++++++++++++++++++++++++++++++++++---- src/throttle/readme.md | 10 ++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/throttle/index.ts b/src/throttle/index.ts index daaf2e46..7f0600eb 100644 --- a/src/throttle/index.ts +++ b/src/throttle/index.ts @@ -15,21 +15,29 @@ type EventAsReturnType = any extends Payload ? Event : never; export function throttle(_: { source: Unit; timeout: number | Store; + leading?: boolean | Store; + trailing?: boolean | Store; name?: string; }): EventAsReturnType; export function throttle>(_: { source: Unit; timeout: number | Store; target: Target; + leading?: boolean | Store; + trailing?: boolean | Store; name?: string; }): Target; export function throttle({ source, timeout, + leading, + trailing, target = createEvent(), }: { source: Unit; timeout: number | Store; + leading?: boolean | Store; + trailing?: boolean | Store; name?: string; target?: Unit; }): EventAsReturnType { @@ -42,13 +50,18 @@ export function throttle({ ); // It's ok - nothing will ever start unless source is triggered - const $payload = createStore(null as unknown as T, { serialize: 'ignore' }).on( - source, - (_, payload) => payload, - ); + const $payload = createStore(null as unknown as T, { + serialize: 'ignore', + }).on(source, (_, payload) => payload); const triggerTick = createEvent(); + const $leading = toStoreBoolean(leading, '$leading', false); + const $trailing = toStoreBoolean(trailing, '$trailing', true); + + const $neverCalled = createStore(true).on(target, () => false); + const $lastCalled = createStore(0).on(target, () => Date.now()); + const $canTick = createStore(true, { serialize: 'ignore' }) .on(triggerTick, () => false) .on(target, () => true); @@ -66,8 +79,25 @@ export function throttle({ }); sample({ - source: $payload, + clock: $payload, + source: [$leading, $neverCalled], + filter: ([leading, neverCalled]) => leading && neverCalled, + target, + }); + + sample({ + source: [$trailing, $payload] as const, clock: timerFx.done, + filter: ([trailing]) => trailing, + fn: ([_, payload]) => payload, + target, + }); + sample({ + clock: $payload, + source: { trailing: $trailing, lastCalled: $lastCalled, timeout: $timeout }, + filter: ({ trailing, lastCalled, timeout }) => + !trailing && lastCalled + timeout < Date.now(), + fn: (_src, clk) => clk, target, }); @@ -88,3 +118,16 @@ function toStoreNumber(value: number | Store | unknown): Store { `timeout parameter should be number or Store. "${typeof value}" was passed`, ); } + +function toStoreBoolean( + value: boolean | Store | undefined, + name: string, + defaultValue: boolean, +): Store { + if (is.store(value)) return value; + if (typeof value === 'boolean') { + return createStore(value, { name }); + } else { + return createStore(defaultValue, { name }); + } +} diff --git a/src/throttle/readme.md b/src/throttle/readme.md index 0d51d1fa..ea3d7508 100644 --- a/src/throttle/readme.md +++ b/src/throttle/readme.md @@ -25,6 +25,11 @@ target = throttle({ source, timeout }); 1. `source` ([_`Event`_] | [_`Store`_] | [_`Effect`_]) — Source unit, data from this unit used by the `target` 1. `timeout` ([_`number`_] | `Store`) — time to wait before trigger `target` after last trigger or `source` trigger +1. `leading` ([_`boolean`_] | `Store`) — trigger `target` on the leading edge of the `timeout`. If `true` first trigger of `source` causes + immediate trigger of `target`, default is `false`, `target` will be first triggered only after `timeout` +1. `trailing` ([_`boolean`_] | `Store`) — trigger `target` on the trailing edge of the `timeout`. If `true` last trigger of `source` + withing `timeout` causes trigger of `target` after `timeout` expires. If `false` any trigger of `source` + will be ignored completely within the `timeout` not causing trigger of `target` after `timeout` expires. ### Returns @@ -105,6 +110,11 @@ throttle({ source, timeout, target }); 1. `source` ([_`Event`_] | [_`Store`_] | [_`Effect`_]) — Source unit, data from this unit used by the `target` 1. `timeout` ([_`number`_] | `Store`) — time to wait before trigger `target` after last trigger or `source` trigger 1. `target` ([_`Event`_] | [_`Store`_] | [_`Effect`_]) — Target unit, that triggered each time after triggering `source` with argument from `source` +1. `leading` ([_`boolean`_] | `Store`) — trigger `target` on the leading edge of the `timeout`. If `true` first trigger of `source` causes + immediate trigger of `target`, default is `false`, `target` will be first triggered only after `timeout` +1. `trailing` ([_`boolean`_] | `Store`) — trigger `target` on the trailing edge of the `timeout`. If `true` last trigger of `source` + withing `timeout` causes trigger of `target` after `timeout` expires. If `false` any trigger of `source` + will be ignored completely within the `timeout` not causing trigger of `target` after `timeout` expires. ### Returns