diff --git a/src/snapshot/index.ts b/src/snapshot/index.ts index 74011388..10f79a5b 100644 --- a/src/snapshot/index.ts +++ b/src/snapshot/index.ts @@ -1,4 +1,6 @@ -import { createStore, Event, sample, Store } from 'effector'; +import { createStore, Effect, sample, Store, Unit, Event } from 'effector'; + +type NoInfer = [T][T extends any ? 0 : never]; export function snapshot({ source, @@ -6,11 +8,13 @@ export function snapshot({ fn = (value: SourceType) => value as unknown as TargetType, }: { source: Store; - clock?: Event; + clock?: Event | Effect | Store; fn?(value: SourceType): TargetType; -}): Store { +}): Store> { const defaultValue = fn(source.defaultState); - const onSnapshot = clock ? sample({ source, clock, fn }) : sample({ source, fn }); + const onSnapshot = clock + ? sample({ source, clock: clock as Unit, fn }) + : sample({ source, fn }); const $snapshot = createStore(defaultValue); $snapshot.on(onSnapshot, (_, value) => value); diff --git a/src/snapshot/readme.md b/src/snapshot/readme.md index ecf4a739..9ae42d49 100644 --- a/src/snapshot/readme.md +++ b/src/snapshot/readme.md @@ -14,7 +14,7 @@ import { snapshot } from 'patronum/snapshot'; ### Motivation -This method allows to copy any store on optional trigger event. +This method allows to copy any store on optional trigger unit. It useful when you want to save previous state of store before some actions. ### Formulae @@ -24,12 +24,12 @@ result = snapshot({ source, clock, fn }); ``` - Call `fn` with data from `source` while `clock` triggered, and create store with the value -- If function in `shape` returns `undefined`, the update will be skipped. +- If `fn` returns `undefined`, the update will be skipped. ### Arguments 1. `source` ([_`Store`_]) — Source store, data from this unit passed to `fn` -2. `clock` ([_`Event`_]) — Trigger event +2. `clock` ([_`Event`_], [_`Effect`_], [_`Store`_]) — Trigger unit 3. `fn` `((value: T) => U)` — Transformation function ### Returns diff --git a/src/snapshot/snapshot.fork.test.ts b/src/snapshot/snapshot.fork.test.ts index e5039e9e..4c563b26 100644 --- a/src/snapshot/snapshot.fork.test.ts +++ b/src/snapshot/snapshot.fork.test.ts @@ -1,4 +1,4 @@ -import { createDomain, allSettled, fork } from 'effector'; +import { createDomain, allSettled, fork, createEvent, createStore } from 'effector'; import { snapshot } from './index'; test('works in forked scope', async () => { @@ -74,3 +74,36 @@ test('does not affect original store state', async () => { expect(scope.getState($copy)).toBe(2); expect($copy.getState()).toBe(1); }); + +test('store clock from one scope does not affect another', async () => { + const app = createDomain(); + + const updateOriginal = createEvent({ domain: app }); + const updateTrigger = createEvent({ domain: app }); + + const $original = createStore('first', { domain: app }).on( + updateOriginal, + (_, newValue) => newValue, + ); + + const $trigger = createStore(0, { domain: app }).on( + updateTrigger, + (state) => state + 1, + ); + + const $copy = snapshot({ source: $original, clock: $trigger }); + + const scope1 = fork(app); + const scope2 = fork(app); + + expect(scope1.getState($copy)).toBe('first'); + expect(scope2.getState($copy)).toBe('first'); + + await allSettled(updateOriginal, { scope: scope1, params: 'second' }); + await allSettled(updateOriginal, { scope: scope2, params: 'second' }); + + await allSettled(updateTrigger, { scope: scope1 }); + + expect(scope1.getState($copy)).toBe('second'); + expect(scope2.getState($copy)).toBe('first'); +}); diff --git a/src/snapshot/snapshot.test.ts b/src/snapshot/snapshot.test.ts index ced9395a..dbbd60eb 100644 --- a/src/snapshot/snapshot.test.ts +++ b/src/snapshot/snapshot.test.ts @@ -1,4 +1,4 @@ -import { createStore, createEvent } from 'effector'; +import { createStore, createEvent, createEffect } from 'effector'; import { snapshot } from './index'; test('snapshot copy original store', () => { @@ -78,3 +78,40 @@ test('snapshot copy original store with transformation on trigger', () => { expect($copy.getState()).toBe(6); }); + +test('snapshot supports effect as a clock', () => { + const updateOriginal = createEvent(); + + const trigger = createEffect(() => undefined); + + const $original = createStore('first').on( + updateOriginal, + (_, newValue) => newValue, + ); + const $copy = snapshot({ source: $original, clock: trigger }); + + updateOriginal('second'); + expect($copy.getState()).toBe('first'); + + trigger(); + expect($copy.getState()).toBe('second'); +}); + +test('snapshot supports store as a clock', () => { + const updateOriginal = createEvent(); + const updateTrigger = createEvent(); + + const $trigger = createStore(0).on(updateTrigger, (state) => state + 1); + + const $original = createStore('first').on( + updateOriginal, + (_, newValue) => newValue, + ); + const $copy = snapshot({ source: $original, clock: $trigger }); + + updateOriginal('second'); + expect($copy.getState()).toBe('first'); + + updateTrigger(); + expect($copy.getState()).toBe('second'); +}); diff --git a/test-typings/snapshot.ts b/test-typings/snapshot.ts index 65cef0d9..a5c17c45 100644 --- a/test-typings/snapshot.ts +++ b/test-typings/snapshot.ts @@ -23,7 +23,7 @@ import { snapshot } from '../src/snapshot'; expectType>(snapshot({ source: $a })); const $b = createStore<'a' | 'b'>('a'); - expectType>(snapshot({ source: $b })); + expectType>(snapshot({ source: $b })); } // Check invalid type for Clock @@ -54,3 +54,29 @@ import { snapshot } from '../src/snapshot'; // @ts-expect-error snapshot({ source: $a, fn: b }); } + +// Check TargetType is not inferred from return value +{ + const $a = createStore(0); + + // @ts-expect-error + expectType>(snapshot({ source: $a })); +} + +// Check effect is supported as a clock +{ + const $a = createStore(0); + + const clock = createEffect(); + + expectType>(snapshot({ source: $a, clock })); +} + +// Check store is supported as a clock +{ + const $a = createStore(0); + + const $clock = createStore(0); + + expectType>(snapshot({ source: $a, clock: $clock })); +}