diff --git a/src/babel-plugin-factories.json b/src/babel-plugin-factories.json index c17ba7d4..8fc1e510 100644 --- a/src/babel-plugin-factories.json +++ b/src/babel-plugin-factories.json @@ -14,6 +14,7 @@ "patronum/in-flight", "patronum/interval", "patronum/not", + "patronum/once", "patronum/or", "patronum/pending", "patronum/reset", @@ -40,6 +41,7 @@ "inFlight": "in-flight", "interval": "interval", "not": "not", + "once": "once", "or": "or", "pending": "pending", "reset": "reset", @@ -52,4 +54,4 @@ "throttle": "throttle", "time": "time" } -} +} \ No newline at end of file diff --git a/src/once/index.ts b/src/once/index.ts index e529b70c..63028fb5 100644 --- a/src/once/index.ts +++ b/src/once/index.ts @@ -3,22 +3,45 @@ import { Store, Event, Effect, + EventAsReturnType, + is, sample, createStore, - EventAsReturnType, } from 'effector'; +type SourceType = Event | Effect | Store; + +export function once(config: { + source: SourceType; + reset?: SourceType; +}): EventAsReturnType; + +export function once(unit: SourceType): EventAsReturnType; + export function once( - unit: Event | Effect | Store, + unitOrConfig: { source: SourceType; reset?: SourceType } | SourceType, ): EventAsReturnType { + let source: Unit; + let reset: Unit | undefined; + + if (is.unit(unitOrConfig)) { + source = unitOrConfig; + } else { + ({ source, reset } = unitOrConfig); + } + const $canTrigger = createStore(true); const trigger: Event = sample({ - clock: unit as Unit, + source, filter: $canTrigger, }); $canTrigger.on(trigger, () => false); + if (reset) { + $canTrigger.reset(reset); + } + return sample({ clock: trigger }); } diff --git a/src/once/once.fork.test.ts b/src/once/once.fork.test.ts index 4b2e6a39..4ad28d62 100644 --- a/src/once/once.fork.test.ts +++ b/src/once/once.fork.test.ts @@ -17,3 +17,23 @@ it('persists state between scopes', async () => { expect(fn).toHaveBeenCalledTimes(1); }); + +it('resetting does not leak between scopes', async () => { + const fn = jest.fn(); + + const source = createEvent(); + const reset = createEvent(); + + const derived = once({ source, reset }); + + derived.watch(fn); + + const triggeredScope = fork(); + const resetScope = fork(); + + await allSettled(source, { scope: triggeredScope }); + await allSettled(reset, { scope: resetScope }); + await allSettled(source, { scope: triggeredScope }); + + expect(fn).toHaveBeenCalledTimes(1); +}); diff --git a/src/once/once.test.ts b/src/once/once.test.ts index 8fd63d01..59043baf 100644 --- a/src/once/once.test.ts +++ b/src/once/once.test.ts @@ -86,3 +86,34 @@ it('calling derived event does not lock once', () => { expect(fn).toHaveBeenCalledTimes(2); }); + +it('supports config as an argument', () => { + const fn = jest.fn(); + + const source = createEvent(); + const derived = once({ source }); + + derived.watch(fn); + + source(); + source(); + + expect(fn).toHaveBeenCalledTimes(1); +}); + +it('supports resetting via config', () => { + const fn = jest.fn(); + + const source = createEvent(); + const reset = createEvent(); + + const derived = once({ source, reset }); + + derived.watch(fn); + + source(); + reset(); + source(); + + expect(fn).toHaveBeenCalledTimes(2); +}); diff --git a/src/once/readme.md b/src/once/readme.md index 712b7cce..c14bae3f 100644 --- a/src/once/readme.md +++ b/src/once/readme.md @@ -6,6 +6,8 @@ import { once } from 'patronum'; import { once } from 'patronum/once'; ``` +## `target = once(source)` + ### Motivation The method allows to do something only on the first ever trigger of `source`. @@ -53,3 +55,44 @@ sample({ target: fetchDataFx, }); ``` + +## `target = once({ source, reset })` + +### Motivation + +This overload may receive `reset` in addition to `source`. If `reset` is fired, +`target` will be allowed to trigger one more time, when `source` is called. + +### Formulae + +```ts +target = once({ source, reset }); +``` + +- When `source` is triggered, launch `target` with data from `source`, but only once. +- When `reset` is triggered, let `once` be reset to the initial state and allow `target` to be triggered again upon `source` being called. + +### Arguments + +- `source` `(Event` | `Effect` | `Store)` — Source unit, data from this unit is used by `target`. +- `reset` `(Event` | `Effect` | `Store)` — A unit that resets `once` to the initial state, allowing `target` to be triggered again. + +### Returns + +- `target` `Event` — The event that will be triggered once after `source` is triggered, until `once` is reset by calling `reset`. + +### Example + +```ts +import { createGate } from 'effector-react'; + +const PageGate = createGate(); + +sample({ + source: once({ + source: PageGate.open, + reset: fetchDataFx.fail, + }), + target: fetchDataFx, +}); +``` diff --git a/test-typings/once.ts b/test-typings/once.ts index b7780392..d8662f5a 100644 --- a/test-typings/once.ts +++ b/test-typings/once.ts @@ -16,12 +16,29 @@ import { once } from '../src/once'; expectType>(once(createStore(''))); } -// Does not allow scope or domain as a first argument +// Supports Event, Effect and Store as source in config +{ + expectType>(once({ source: createEvent() })); + expectType>(once({ source: createEffect() })); + expectType>(once({ source: createStore('') })); +} + +// Does not allow scope or domain as source or reset { // @ts-expect-error once(createDomain()); // @ts-expect-error once(fork()); + + // @ts-expect-error + once({ source: createDomain() }); + // @ts-expect-error + once({ source: fork() }); + + // @ts-expect-error + once({ source: createEvent(), reset: createDomain() }); + // @ts-expect-error + once({ source: createEvent(), reset: fork() }); } // Correctly passes through complex types @@ -29,3 +46,26 @@ import { once } from '../src/once'; const source = createEvent<'string' | false>(); expectType>(once(source)); } + +// Requires source in config form +{ + // @ts-expect-error + once({ + /* No source */ + }); + + // @ts-expect-error + once({ + /* No source */ + reset: createEvent(), + }); +} + +// Accepts reset in config form +{ + const source = createEvent(); + + expectType>(once({ source, reset: createEvent<'string'>() })); + expectType>(once({ source, reset: createStore('string') })); + expectType>(once({ source, reset: createEffect() })); +}