Skip to content

Commit

Permalink
Merge pull request #284 from kireevmp/feat/once-operator
Browse files Browse the repository at this point in the history
  • Loading branch information
sergeysova committed May 8, 2023
2 parents 5c7a5ce + 5855fd0 commit b69038a
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));
}

1 comment on commit b69038a

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚛 size-compare report

Comparing 5c7a5cea...b69038a0

File +/- Base Current +/- gzip Base gzip Current gzip
dist/and/index.cjs = 397 B 397 B = 251 B 251 B
dist/and/index.js = 413 B 413 B = 245 B 245 B
dist/babel-preset.cjs = 434 B 434 B = 249 B 249 B
dist/combine-events/index.cjs = 2.46 kB 2.46 kB = 841 B 841 B
dist/combine-events/index.js = 3.95 kB 3.95 kB = 1.35 kB 1.35 kB
dist/condition/index.cjs = 1.37 kB 1.37 kB = 505 B 505 B
dist/condition/index.js = 1.29 kB 1.29 kB = 461 B 461 B
dist/debounce/index.cjs = 3.99 kB 3.99 kB = 1.31 kB 1.31 kB
dist/debounce/index.js = 3.84 kB 3.84 kB = 1.29 kB 1.29 kB
dist/debug/index.cjs = 11.3 kB 11.3 kB = 3.16 kB 3.16 kB
dist/debug/index.js = 13.3 kB 13.3 kB = 3.79 kB 3.79 kB
dist/delay/index.cjs = 1.79 kB 1.79 kB = 727 B 727 B
dist/delay/index.js = 1.84 kB 1.84 kB = 719 B 719 B
dist/either/index.cjs = 600 B 600 B = 334 B 334 B
dist/either/index.js = 477 B 477 B = 270 B 270 B
dist/empty/index.cjs = 175 B 175 B = 151 B 151 B
dist/empty/index.js = 79 B 79 B = 89 B 89 B
dist/equals/index.cjs = 336 B 336 B = 249 B 249 B
dist/equals/index.js = 221 B 221 B = 179 B 179 B
dist/every/index.cjs = 1.22 kB 1.22 kB = 513 B 513 B
dist/every/index.js = 1.08 kB 1.08 kB = 442 B 442 B
dist/format/index.cjs = 642 B 642 B = 366 B 366 B
dist/format/index.js = 687 B 687 B = 368 B 368 B
dist/in-flight/index.cjs = 641 B 641 B = 357 B 357 B
dist/in-flight/index.js = 546 B 546 B = 305 B 305 B
dist/index.cjs +3.57% 1.57 kB 1.62 kB +2.79% 359 B 369 B
dist/index.js +3.58% 1.09 kB 1.13 kB +2.15% 279 B 285 B
dist/interval/index.cjs = 3.88 kB 3.88 kB = 1.14 kB 1.14 kB
dist/interval/index.js = 3.74 kB 3.74 kB = 1.12 kB 1.12 kB
dist/macro.cjs = 1.91 kB 1.91 kB = 808 B 808 B
dist/not/index.cjs = 161 B 161 B = 148 B 148 B
dist/not/index.js = 69 B 69 B = 81 B 81 B
dist/once/index.cjs 597 B 308 B
dist/once/index.js 465 B 242 B
dist/or/index.cjs = 393 B 393 B = 249 B 249 B
dist/or/index.js = 411 B 411 B = 245 B 245 B
dist/patronum.cjs +1.82% 18.8 kB 19.2 kB +1.32% 5.97 kB 6.05 kB
dist/patronum.js +1.13% 17.8 kB 18.1 kB +0.96% 6.04 kB 6.1 kB
dist/patronum.umd.js +1.82% 19.9 kB 20.2 kB +1.33% 6.07 kB 6.16 kB
dist/pending/index.cjs = 909 B 909 B = 495 B 495 B
dist/pending/index.js = 828 B 828 B = 444 B 444 B
dist/reset/index.cjs = 526 B 526 B = 312 B 312 B
dist/reset/index.js = 439 B 439 B = 256 B 256 B
dist/reshape/index.cjs = 419 B 419 B = 242 B 242 B
dist/reshape/index.js = 379 B 379 B = 201 B 201 B
dist/snapshot/index.cjs = 763 B 763 B = 350 B 350 B
dist/snapshot/index.js = 648 B 648 B = 292 B 292 B
dist/some/index.cjs = 1.16 kB 1.16 kB = 474 B 474 B
dist/some/index.js = 1.02 kB 1.02 kB = 407 B 407 B
dist/split-map/index.cjs = 628 B 628 B = 359 B 359 B
dist/split-map/index.js = 575 B 575 B = 318 B 318 B
dist/spread/index.cjs = 1.26 kB 1.26 kB = 534 B 534 B
dist/spread/index.js = 1.28 kB 1.28 kB = 516 B 516 B
dist/status/index.cjs = 426 B 426 B = 265 B 265 B
dist/status/index.js = 339 B 339 B = 208 B 208 B
dist/throttle/index.cjs = 2.1 kB 2.1 kB = 814 B 814 B
dist/throttle/index.js = 1.99 kB 1.99 kB = 775 B 775 B
dist/time/index.cjs = 719 B 719 B = 376 B 376 B
dist/time/index.js = 621 B 621 B = 323 B 323 B

Please sign in to comment.