Skip to content

Commit

Permalink
feat(once): implement once operator
Browse files Browse the repository at this point in the history
  • Loading branch information
kireevmp committed May 3, 2023
1 parent 5c7a5ce commit 5855fd0
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 0 deletions.
24 changes: 24 additions & 0 deletions src/once/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
Unit,
Store,
Event,
Effect,
sample,
createStore,
EventAsReturnType,
} from 'effector';

export function once<T>(
unit: Event<T> | Effect<T, any, any> | Store<T>,
): EventAsReturnType<T> {
const $canTrigger = createStore<boolean>(true);

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

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

return sample({ clock: trigger });
}
19 changes: 19 additions & 0 deletions src/once/once.fork.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { allSettled, createEvent, fork, serialize } from 'effector';
import { once } from './index';

it('persists state between scopes', async () => {
const fn = jest.fn();

const trigger = createEvent<void>();
const derived = once(trigger);

derived.watch(fn);

const scope1 = fork();
await allSettled(trigger, { scope: scope1 });

const scope2 = fork({ values: serialize(scope1) });
await allSettled(trigger, { scope: scope2 });

expect(fn).toHaveBeenCalledTimes(1);
});
88 changes: 88 additions & 0 deletions src/once/once.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { createEffect, createEvent, createStore, is, launch } from 'effector';
import { once } from './index';

it('should source only once', () => {
const fn = jest.fn();

const source = createEvent<void>();
const derived = once(source);

derived.watch(fn);
expect(fn).toHaveBeenCalledTimes(0);

source();
source();

expect(fn).toHaveBeenCalledTimes(1);
});

it('supports effect as an argument', () => {
const fn = jest.fn();

const triggerFx = createEffect<void, void>(jest.fn());
const derived = once(triggerFx);

derived.watch(fn);
expect(fn).toHaveBeenCalledTimes(0);

triggerFx();

expect(fn).toHaveBeenCalledTimes(1);
});

it('supports store as an argument', () => {
const fn = jest.fn();

const increment = createEvent<void>();
const $source = createStore<number>(0).on(increment, (n) => n + 1);
const derived = once($source);

derived.watch(fn);
expect(fn).toHaveBeenCalledTimes(0);

increment();

expect(fn).toHaveBeenCalledTimes(1);
});

it('always returns event', () => {
const event = createEvent<string>();
const effect = createEffect<string, void>();
const $store = createStore<string>('');

expect(is.event(once(event))).toBe(true);
expect(is.event(once(effect))).toBe(true);
expect(is.event(once($store))).toBe(true);
});

it('only triggers once in race conditions', () => {
const fn = jest.fn();

const source = createEvent<string>();
const derived = once(source);

derived.watch(fn);
expect(fn).toHaveBeenCalledTimes(0);

launch({
target: [source, source],
params: ['a', 'b'],
});

expect(fn).toHaveBeenCalledTimes(1);
});

it('calling derived event does not lock once', () => {
const fn = jest.fn();

const source = createEvent<void>();
const derived = once(source);

derived.watch(fn);
expect(fn).toHaveBeenCalledTimes(0);

derived();
source();

expect(fn).toHaveBeenCalledTimes(2);
});
55 changes: 55 additions & 0 deletions src/once/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# once

```ts
import { once } from 'patronum';
// or
import { once } from 'patronum/once';
```

### Motivation

The method allows to do something only on the first ever trigger of `source`.
It is useful to trigger effects or other logic only once per application's lifetime.

### Formulae

```ts
target = once(source);
```

- When `source` is triggered, launch `target` with data from `source`, but only once.

### Arguments

- `source` `(Event<T>` | `Effect<T>` | `Store<T>)` — Source unit, data from this unit is used by `target`.

### Returns

- `target` `Event<T>` — The event that will be triggered exactly once after `source` is triggered.

### Example

```ts
const messageReceived = createEvent<string>();
const firstMessageReceived = once(messageReceived);

firstMessageReceived.watch((message) =>
console.log('First message received:', message),
);

messageReceived('Hello'); // First message received: Hello
messageReceived('World');
```

#### Alternative

```ts
import { createGate } from 'effector-react';

const PageGate = createGate();

sample({
source: once(PageGate.open),
target: fetchDataFx,
});
```
31 changes: 31 additions & 0 deletions test-typings/once.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { expectType } from 'tsd';
import {
Event,
createStore,
createEvent,
createEffect,
createDomain,
fork,
} from 'effector';
import { once } from '../src/once';

// Supports Event, Effect and Store as an argument
{
expectType<Event<string>>(once(createEvent<string>()));
expectType<Event<string>>(once(createEffect<string, void>()));
expectType<Event<string>>(once(createStore<string>('')));
}

// Does not allow scope or domain as a first argument
{
// @ts-expect-error
once(createDomain());
// @ts-expect-error
once(fork());
}

// Correctly passes through complex types
{
const source = createEvent<'string' | false>();
expectType<Event<'string' | false>>(once(source));
}

0 comments on commit 5855fd0

Please sign in to comment.