Skip to content

Commit

Permalink
Trigger API (#274)
Browse files Browse the repository at this point in the history
* Add new _Event_ for the _Query_ — `.refresh`

* Add operator `stale`

* Add new operaor — `keepFresh`

* Fix typo

* Rework keepFresh API

* Extend keepFresh for TriggerProtocol

* Finalize docs

* Ajust @@trigger protocol to real use-cases

* Ajust @@trigger protocol to real use-cases

* Finalize docs

* Finalize docs

* Use all

* Fix format

* Add teardown to trigger protocol

* Correct teardowns for external triggers

* Size limit

* Rename config fields in keepFresh

* Fix bug with extra refreshes

* Add sync batching

* Use more convinient names in keppFresh

* Code clean up

* Fix incorrect refactoring

* Code clean up
  • Loading branch information
igorkamyshev committed Mar 13, 2023
1 parent ff649e3 commit 24cb348
Show file tree
Hide file tree
Showing 40 changed files with 1,159 additions and 85 deletions.
5 changes: 5 additions & 0 deletions .changeset/honest-lies-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@farfetched/core': minor
---

Add new _Event_ for the _Query_`.refresh`
5 changes: 5 additions & 0 deletions .changeset/strange-planes-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@farfetched/core': minor
---

Add new operator — `keepFresh`
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,4 @@ Some of external libraries were inlined to Farfetched due to bundle size and cus
- https://github.com/effector/patronum/pull/168
- https://github.com/emn178/js-sha1/blob/master/tests/test.js
- http://www.movable-type.co.uk/scripts/sha1.html
- https://github.com/smelukov/nano-equal
5 changes: 5 additions & 0 deletions apps/website/docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ export default withMermaid(
},
],
},
{
text: 'Trigger API',
link: '/tutorial/trigger_api',
},
],
},
{
Expand Down Expand Up @@ -185,6 +189,7 @@ export default withMermaid(
{ text: 'update', link: '/api/operators/update' },
{ text: 'retry', link: '/api/operators/retry' },
{ text: 'cache', link: '/api/operators/cache' },
{ text: 'keepFresh', link: '/api/operators/keep_fresh' },
{
text: 'attachOperation',
link: '/api/operators/attach_operation',
Expand Down
1 change: 1 addition & 0 deletions apps/website/docs/api/contracts/zod.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ yarn add zod @farfetched/zod
```sh [npm]
npm install zod @farfetched/zod
```

:::

## `zodContract`
Expand Down
26 changes: 26 additions & 0 deletions apps/website/docs/api/operators/keep_fresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# `keepFresh` <Badge type="tip" text="since v0.8.0" />

Refreshes the data in a [_Query_](/api/primitives/query) automatically or on demand.

::: tip Note
`keepFresh` operator refreshes only [_Queries_](/api/primitives/query) that were started at least once. So, consider calling `query.refresh` on the app start.
:::

## Formulae

### `keepFresh(query, config)`

Config fields:

- `automatically?`: _true_ to refresh the data in a [_Query_](/api/primitives/query) automatically if any [_Store_](https://effector.dev/docs/api/effector/store) that is used in the [_Query_](/api/primitives/query) creation is changed.
- `triggers?`: _Array_ of [_Events_](https://effector.dev/docs/api/effector/event) after which operator starts refreshing the data in the [_Query_](/api/primitives/query).

```ts
import { keepFresh } from '@farfetched/core';

keepFresh(query, { automatically: true, triggers: [someExternalEvent] });
```

:::tip
You can use any object that follows the [`@@trigger`-protocol](https://withease.pages.dev/protocols/trigger.html) as a trigger in the `keepFresh` operator's field `triggers`.
:::
1 change: 1 addition & 0 deletions apps/website/docs/api/primitives/query.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ query.$stale; // Store<boolean>
// Commands
query.start; // Event<Params>
query.reset; // Event<void>, since v0.2.0
query.refresh; // Event<Params>. since v0.8.0

// Events
query.finished.success; // Event<{ result: Data, params: Params }>
Expand Down
4 changes: 2 additions & 2 deletions apps/website/docs/recipes/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ test('let us mock function', async () => {
[locationQuery.__.executeFx, () => 'Mocked'],
]})

await allSettled(locationQuery.start, { scope })
await allSettled(locationQuery.refresh, { scope })

// all computations are settled
})
Expand All @@ -72,7 +72,7 @@ test('let us mock function', async () => {
[locationQuery.__.executeFx, () => 'Mocked'],
]})

await allSettled(locationQuery.start, { scope })
await allSettled(locationQuery.refresh, { scope })

expect(scope.getState(locationQuery.$data)).toBe('Mocked')
})
Expand Down
2 changes: 1 addition & 1 deletion apps/website/docs/releases/0-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,4 +335,4 @@ const mapper = {
};
```

<!--@include: ./0-4.changelog.md-->
<!--@include: ./0-4.changelog.md-->
2 changes: 1 addition & 1 deletion apps/website/docs/releases/0-8.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Saphan Hin is a seaside park in the Phuket Town where almost the whole 0.8 relea

It is a quite big release with a lot of new features and improvements. The most important changes are:

- Triggers API to keep your data fresh
- [Triggers API](/tutorial/trigger_api) to keep your data fresh
- New integration for [`typed-contracts`](/api/contracts/typed-contracts)
- Significant improvements in type inference

Expand Down
2 changes: 1 addition & 1 deletion apps/website/docs/tutorial/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ itemsQuery.finished.error.watch((error) => {
console.error('OH NO', error);
});

itemsQuery.start();
itemsQuery.refresh();
```

:::
Expand Down
128 changes: 128 additions & 0 deletions apps/website/docs/tutorial/trigger_api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Trigger API

:::tip You will learn

- What means that the data in a [_Query_](/api/primitives/query) is fresh (and stale)
- How to refresh the data in a [_Query_](/api/primitives/query) manually
- How to refresh the data in a [_Query_](/api/primitives/query) automatically by different triggers

:::

In the previous chapters, we learned how to fetch data from the server and how to display it. Now let us talk about how to keep the data fresh.

## `.$stale` property of the [_Query_](/api/primitives/query)

Every [_Query_](/api/primitives/query) has a `.$stale` property. It is a [_Store_](https://effector.dev/docs/api/effector/store) that contains a boolean value. It is `true` when the data in the [_Query_](/api/primitives/query) is stale, and `false` when it is fresh.

By default, it is `true` because the [_Query_](/api/primitives/query) is not fetched yet.

Data becomes stale when the [_Query_](/api/primitives/query) in many cases:

1. if it is [updated after _Mutation_ execution](/tutorial/update_query)
2. if it is [dependent on another _Query_](/tutorial/dependent_queries) and the parent _Query_ data is updated
3. if its value is extracted from [cache](/tutorial/caching) and the cache is not marked as fresh

In all these cases, the `.$stale` property of the [_Query_](/api/primitives/query) becomes `true` and the [_Query_](/api/primitives/query) immediately starts the process of refreshing the data.

## Refresh the data manually

Sometimes you want to start a [_Query_] only if the data is stale and skip it if the data is fresh. For example, you got the data from the server and do not want to fetch it again until on client. For this case, you can use the `.refresh` [_Event_](https://effector.dev/docs/api/effector/event) of the [_Query_](/api/primitives/query).

```ts
import { sample } from 'effector';
import { createJsonQuery } from '@farfetched/core';

const someQuery = createJsonQuery({
/* ... */
});

sample({ clock: appStarted, target: someQuery.refresh });
```

In this example, the `someQuery` will be started only every time when the `appStarted` event is triggered and the data in the `someQuery` is stale. You can safely call the `appStarted` on the server, transfer the data to the client, and call the `appStarted` on the client. The `someQuery` will be started only on the server and will be skipped on the client.

## Refresh the data automatically

In the most cases, you want to refresh the data automatically during the lifetime of the app. For this case, you can use the [`keepFresh`](/api/operators/keep_fresh)-operator.

The following example shows how to refresh the `someQuery` every time when `$language` store is changed, but only after the `appStarted` event is triggered.

```ts
import { keepFresh, createJsonQuery } from '@farfetched/core';

const $language = createStore('en');

const someQuery = createJsonQuery({
request: {
url: { source: $language, fn: (_, language) => `/api/${language}` },
},
});

keepFresh(someQuery, {
automatically: true,
});

sample({ clock: appStarted, target: someQuery.refresh });
```

If you do want to refresh the data immediately after some external trigger, you can use `triggers` field of the `keepFresh`-operator's config to specify the triggers.

```ts
import { keepFresh, createJsonQuery } from '@farfetched/core';

const $language = createStore('en');

const someQuery = createJsonQuery(/* ... */);

keepFresh(someQuery, { triggers: [userLoggedIn] });

sample({ clock: appStarted, target: someQuery.refresh });
```

### External triggers

It could be really useful to refresh the data on some application wide triggers like tab visibility or network reconnection. This kind of triggers is out of scope of Farfetched, so they are distributed as [separated package — `@withease/web-api`](https://withease.pages.dev/web-api.html).

::: code-group

```sh [pnpm]
pnpm install @withease/web-api
```

```sh [yarn]
yarn add @withease/web-api
```

```sh [npm]
npm install @withease/web-api
```

:::

It is compatible with Farfetched and can be used without any additional configuration.

```ts
import { trackPageVisibility, trackNetworkStatus } from '@withease/web-api';
import { keepFresh } from '@farfetched/core';

keeypFresh(someQuery, {
triggers: [trackPageVisibility, trackNetworkStatus],
});
```

Check [documentation of `@withease/web-api`](https://withease.pages.dev/web-api.html) for the complete list of available triggers.

### Mix automatic and manual refresh

You can mix automatic and manual refresh:

```ts
keepFresh(someQuery, {
automatically: true,
triggers: [userLoggedIn],
});
```

## API reference

You can find the full API reference for the `keepFresh` operator in the [API reference](/api/operators/keep_fresh).
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"test:types": "nx run-many --target=typetest --all",
"test:tools": "vitest run tools",
"lint": "nx run-many --target=lint --all",
"lint:format": "nx format:check",
"lint:format": "nx format:check --all",
"lint:workspace": "nx workspace-lint",
"lint:changes": "changeset status",
"build": "nx run-many --target=build --all",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,6 @@ export {
isInvalidDataError,
isHttpErrorCode,
} from './src/errors/guards';

// Trigger API
export { keepFresh } from './src/trigger_api/keep_fresh';
2 changes: 1 addition & 1 deletion packages/core/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"size": {
"executor": "./tools/executors/size-limit:size-limit",
"options": {
"limit": "16.9 kB",
"limit": "18.2 kB",
"outputPath": "dist/packages/core"
},
"dependsOn": [
Expand Down
10 changes: 3 additions & 7 deletions packages/core/src/cache/adapters/browser_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,16 @@ import { parseTime } from '../../libs/date-nfs';
import { createAdapter } from './instance';
import { attachObservability } from './observability';
import { CacheAdapter, CacheAdapterOptions } from './type';
import { get } from '../../libs/lohyphen';

export function browserStorageCache(
config: {
storage: () => Storage;
} & CacheAdapterOptions
): CacheAdapter {
const { storage, observability } = config;
const { storage, observability, maxAge, maxEntries } = config;
// -- adapter
function storageCache(): CacheAdapter {
const { maxEntries, maxAge } = config;

const getSavedItemFx = createEffect(async (key: string) => {
const item = await getItemFx(key);

Expand Down Expand Up @@ -69,10 +68,7 @@ export function browserStorageCache(
}),
timeout: parseTime(maxAge),
}),
fn: (params) => ({
key: params.params.key,
value: params.params.value,
}),
fn: get('params'),
target: [itemExpired, removeSavedItemFx],
});
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/src/cache/adapters/observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ export function attachObservability({
if (options?.evicted && events?.itemEvicted) {
sample({
clock: events.itemEvicted,
fn: ({ key }) => ({ key }),
target: options.evicted,
});
}
Expand Down
10 changes: 5 additions & 5 deletions packages/core/src/cache/cache.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createEffect, Event, sample, split } from 'effector';

import { time } from '../libs/patronus';
import { parseTime, Time } from '../libs/date-nfs';
import { parseTime, type Time } from '../libs/date-nfs';
import {
RemoteOperationParams,
RemoteOperationResult,
type RemoteOperationParams,
type RemoteOperationResult,
} from '../remote_operation/type';
import { Query } from '../query/type';
import { type Query } from '../query/type';
import { inMemoryCache } from './adapters/in_memory';
import { CacheAdapter, CacheAdapterInstance } from './adapters/type';
import { type CacheAdapter, type CacheAdapterInstance } from './adapters/type';
import {
enrichFinishedSuccessWithKey,
enrichForcedWithKey,
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/cache/key/key.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Event, sample } from 'effector';

import { get } from '../../libs/lohyphen';
import { Query } from '../../query/type';
import {
RemoteOperationResult,
Expand Down Expand Up @@ -39,9 +40,13 @@ function enrichWithKey<
>(event: Event<T>, query: Q): Event<T & { key: string | null }> {
const queryDataSid = queryUniqId(query);

const source = query.__.lowLevelAPI.sourced.map((sourced) =>
sourced(event.map(get('params')))
);

return sample({
clock: event,
source: query.__.lowLevelAPI.sources,
source,
fn: (sources, payload) => ({
...payload,
key: createKey({
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/contract/apply_contract.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createEffect, Effect } from 'effector';

import { invalidDataError } from '../errors/create_error';
import { InvalidDataError } from '../errors/type';
import { ExecutionMeta } from '../remote_operation/type';
import { Contract } from './type';
import { type InvalidDataError } from '../errors/type';
import { type ExecutionMeta } from '../remote_operation/type';
import { type Contract } from './type';

export function createContractApplier<Params, Raw, Data extends Raw>(
contract: Contract<Raw, Data>
Expand Down

0 comments on commit 24cb348

Please sign in to comment.