Skip to content

Commit

Permalink
feat(once): add support for reset argument (#299)
Browse files Browse the repository at this point in the history
Co-authored-by: Alexander Khoroshikh <32790736+AlexandrHoroshih@users.noreply.github.com>
  • Loading branch information
kireevmp and AlexandrHoroshih committed Oct 11, 2023
1 parent 0478526 commit 547117f
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 5 deletions.
4 changes: 3 additions & 1 deletion src/babel-plugin-factories.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"patronum/in-flight",
"patronum/interval",
"patronum/not",
"patronum/once",
"patronum/or",
"patronum/pending",
"patronum/reset",
Expand All @@ -40,6 +41,7 @@
"inFlight": "in-flight",
"interval": "interval",
"not": "not",
"once": "once",
"or": "or",
"pending": "pending",
"reset": "reset",
Expand All @@ -52,4 +54,4 @@
"throttle": "throttle",
"time": "time"
}
}
}
29 changes: 26 additions & 3 deletions src/once/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,45 @@ import {
Store,
Event,
Effect,
EventAsReturnType,
is,
sample,
createStore,
EventAsReturnType,
} from 'effector';

type SourceType<T> = Event<T> | Effect<T, any, any> | Store<T>;

export function once<T>(config: {
source: SourceType<T>;
reset?: SourceType<any>;
}): EventAsReturnType<T>;

export function once<T>(unit: SourceType<T>): EventAsReturnType<T>;

export function once<T>(
unit: Event<T> | Effect<T, any, any> | Store<T>,
unitOrConfig: { source: SourceType<T>; reset?: SourceType<any> } | SourceType<T>,
): EventAsReturnType<T> {
let source: Unit<T>;
let reset: Unit<any> | undefined;

if (is.unit(unitOrConfig)) {
source = unitOrConfig;
} else {
({ source, reset } = unitOrConfig);
}

const $canTrigger = createStore<boolean>(true);

const trigger: Event<T> = sample({
clock: unit as Unit<T>,
source,
filter: $canTrigger,
});

$canTrigger.on(trigger, () => false);

if (reset) {
$canTrigger.reset(reset);
}

return sample({ clock: trigger });
}
20 changes: 20 additions & 0 deletions src/once/once.fork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();
const reset = createEvent<void>();

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);
});
31 changes: 31 additions & 0 deletions src/once/once.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();
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<void>();
const reset = createEvent<void>();

const derived = once({ source, reset });

derived.watch(fn);

source();
reset();
source();

expect(fn).toHaveBeenCalledTimes(2);
});
43 changes: 43 additions & 0 deletions src/once/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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<T>` | `Effect<T>` | `Store<T>)` — 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<T>` — 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,
});
```
42 changes: 41 additions & 1 deletion test-typings/once.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,56 @@ import { once } from '../src/once';
expectType<Event<string>>(once(createStore<string>('')));
}

// Does not allow scope or domain as a first argument
// Supports Event, Effect and Store as source in config
{
expectType<Event<string>>(once({ source: createEvent<string>() }));
expectType<Event<string>>(once({ source: createEffect<string, void>() }));
expectType<Event<string>>(once({ source: createStore<string>('') }));
}

// 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
{
const source = createEvent<'string' | false>();
expectType<Event<'string' | false>>(once(source));
}

// Requires source in config form
{
// @ts-expect-error
once({
/* No source */
});

// @ts-expect-error
once({
/* No source */
reset: createEvent<void>(),
});
}

// Accepts reset in config form
{
const source = createEvent<string>();

expectType<Event<string>>(once({ source, reset: createEvent<'string'>() }));
expectType<Event<string>>(once({ source, reset: createStore('string') }));
expectType<Event<string>>(once({ source, reset: createEffect<number, void>() }));
}

0 comments on commit 547117f

Please sign in to comment.