Skip to content

Commit

Permalink
Barrier API (#359)
Browse files Browse the repository at this point in the history
* Init release

* Abort as separate status (#357)

* Uncommect tests for changes

* Edit more test for the future

* Add one more test case

* Finalize separation of `.aborted` _Event_ in _Remote Operation_

* Fix typo

* Exclude AbortError from ApiRequestError type

* Change default value of `supressIntermediateErrors` in `retry` operator

* Allow to pass `null` and `undefined` to `FetchApiRecord`, it will be ignored

* Add docs skeleton

* Add docs skeleton

* Add API ref

* Add API ref for atomic-router

* Fix typo

* Introduce Barrier API

* Type tests for barrierChain

* Apply barrier typings

* createBarrier typings

* Implement createbarrier

* Increase size

* WIP applyBarrier

* Fix bug with mutex sharing between scopes

* Implementation of applyBarrier, remove weird API from createbarrier

* Add more tests of desired result

* Simplify mutex

* Do not start many performers

* Add TODO test

* Remove deleted overload from typetests

* Add new test for mutex

* Size limit

* Update Mutex implementation (#367)

Get rid of creation a lot of extra promises
on each waitForUnlock

* Add tests for resumeParams

* Improve auth token how-to

* Delete obsolete changes

* No resume params for now

* barrierChain

* No barrier chain for now

* Add case study

* Fix badges for barrier api version

* JSDoc

* More docs

* Fix typo

* Add more info

---------

Co-authored-by: Vladimir Ivakin <vl.ivakin@yandex.ru>
  • Loading branch information
igorkamyshev and Minhir committed Nov 25, 2023
1 parent ef2d3a5 commit c925412
Show file tree
Hide file tree
Showing 28 changed files with 1,400 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/funny-plums-report.md
@@ -0,0 +1,5 @@
---
'@farfetched/core': minor
---

Introduce Barrier API
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -69,3 +69,4 @@ Some of external libraries were inlined to Farfetched due to bundle size and cus
- 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
- https://github.com/DirtyHairy/async-mutex
30 changes: 27 additions & 3 deletions apps/website/docs/.vitepress/config.js
Expand Up @@ -132,6 +132,10 @@ export default withMermaid(
text: 'Trigger API',
link: '/tutorial/trigger_api',
},
{
text: 'Barrier API',
link: '/tutorial/barrier_api',
},
],
},
{
Expand Down Expand Up @@ -180,21 +184,31 @@ export default withMermaid(
},
],
},
{
text: 'Barrier',
items: [
{
text: 'createBarrier',
link: '/api/factories/create_barrier',
},
],
},
],
},
{
text: 'Operators',
items: [
{ text: 'connectQuery', link: '/api/operators/connect_query' },
{ text: 'update', link: '/api/operators/update' },
{ text: 'keepFresh', link: '/api/operators/keep_fresh' },
{ text: 'retry', link: '/api/operators/retry' },
{ text: 'timeout', link: '/api/operators/timeout' },
{ text: 'cache', link: '/api/operators/cache' },
{ text: 'keepFresh', link: '/api/operators/keep_fresh' },
{ text: 'applyBarrier', link: '/api/operators/apply_barrier' },
{ text: 'update', link: '/api/operators/update' },
{
text: 'attachOperation',
link: '/api/operators/attach_operation',
},
{ text: 'connectQuery', link: '/api/operators/connect_query' },
],
},
{
Expand All @@ -203,6 +217,7 @@ export default withMermaid(
items: [
{ text: 'Query', link: '/api/primitives/query' },
{ text: 'Mutation', link: '/api/primitives/mutation' },
{ text: 'Barrier', link: '/api/primitives/barrier' },
{ text: 'Contract', link: '/api/primitives/contract' },
{ text: 'Validator', link: '/api/primitives/validator' },
],
Expand Down Expand Up @@ -280,11 +295,20 @@ export default withMermaid(
text: 'Base URL for all operations',
link: '/recipes/base_url',
},
{
text: 'Auth token for operations',
link: '/recipes/auth_token',
},
{
text: 'Pause world until user action',
link: '/recipes/terms_of_use',
},
],
},
{
text: 'Case studies',
items: [
{ text: 'Auth token', link: '/recipes/auth_token' },
{ text: 'Feature flags service', link: '/recipes/feature_flags' },
{ text: 'Server side caching', link: '/recipes/server_cache' },
],
Expand Down
69 changes: 69 additions & 0 deletions apps/website/docs/api/factories/create_barrier.md
@@ -0,0 +1,69 @@
# `createBarrier` <Badge type="tip" text="since v0.11.0" />

Creates a [_Barrier_](/api/primitives/barrier) object.

## Formulae

### `createBarrier({ active, perform? })`

```ts
import { createBarrier } from '@farfetched/core';
import { combine } from 'effector';

const authBarrier = createBarrier({
active: combine(
{ token: $token, now: $now },
({ token, now }) => parseToken(token.exp) > now
),
perform: [renewTokenMutationFx],
});
```

Configuration fields:

- `active`: [_Store_](https://effector.dev/docs/api/effector/store) with boolean that indicates whether [_Barrier_](/api/primitives/barrier) is active or not.
- `perform?`: optional array of [_Performers_](#performer), that will be started in case some operation that uses this [_Barrier_](/api/primitives/barrier) is started and [_Barrier_](/api/primitives/barrier) is `$active`.

### `createBarrier({ activateOn, perform })`

```ts
import { createBarrier, isHttpError } from '@farfetched/core';
import { combine } from 'effector';

const authBarrier = createBarrier({
activateOn: {
failure: isHttpError(401),
},
perform: [renewTokenMutationFx],
});
```

Configuration fields:

- `activateOn.failure`: callback that will be called in case of failure of some operation that uses this [_Barrier_](/api/primitives/barrier). If it returns `true`, [_Barrier_](/api/primitives/barrier) will be activated.
- `perform`: array of [_Performers_](#performer), that will be started in case some operation that uses this [_Barrier_](/api/primitives/barrier) is started and [_Barrier_](/api/primitives/barrier) is `$active`.

### `createBarrier({ activateOn, deactivateOn })`

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

const authBarrier = createBarrier({
activateOn: userOpenedModal,
deactivateOn: userClosedModal,
});
```

Configuration fields:

- `activateOn`: [_Event_](https://effector.dev/docs/api/effector/event) that will activate [_Barrier_](/api/primitives/barrier) when called.
- `deactivateOn`: [_Event_](https://effector.dev/docs/api/effector/event) that will deactivate [_Barrier_](/api/primitives/barrier) when called.

## Performer

Any of the following form can be used in `perform` field:

1. [_Query_](/api/primitives/query) that does not accept any parameters
2. [_Mutation_](/api/primitives/mutation) that does not accept any parameters
3. [_Effect_](https://effector.dev/docs/api/effector/effect) that does not accept any parameters
4. Object `{ start, end }` where `start` and `end` are [_Events_](https://effector.dev/docs/api/effector/event) that do not accept any parameters. `start` [_Event_](https://effector.dev/docs/api/effector/event) will be called when the [_Barrier_](/api/primitives/barrier) is activated, `end` [_Event_](https://effector.dev/docs/api/effector/event) is expected to be called when the performer is finished.
16 changes: 16 additions & 0 deletions apps/website/docs/api/operators/apply_barrier.md
@@ -0,0 +1,16 @@
# `applyBarrier` <Badge type="tip" text="since v0.11.0" />

Applies the [_Barrier_](/api/primitives/barrier) to the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation). After operation start it checks the [_Barrier_](/api/primitives/barrier) `.$active` status and postpones the execution if the [_Barrier_](/api/primitives/barrier) is active. After the [_Barrier_](/api/primitives/barrier) is `deactivated`, it resumes the execution of the operation.

## Formulae

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

applyBarrier(operation, { barrier });
```

### Arguments

- `operation` - [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) to apply the [_Barrier_](/api/primitives/barrier) to
- `barrier` - [_Barrier_](/api/primitives/barrier) to apply
17 changes: 17 additions & 0 deletions apps/website/docs/api/primitives/barrier.md
@@ -0,0 +1,17 @@
# Barrier <Badge type="tip" text="since v0.11.0" />

Object that could be used to postpone the execution of the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation). Barrier can be created with the [`createBarrier`](/api/factories/create_barrier) function and applied to the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) with the [`applyBarrier`](/api/operators/apply_barrier) operator.

For user-land code, it is a read-only object that have the following properties:

## `$active`

[_Store_](https://effector.dev/docs/api/effector/store) with the current status of the _Barrier_. It must not be changed directly. Can be `true` or `false`. If it is `true` then the _Barrier_ is active and the execution of the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) will be postponed in case of [applying the _Barrier_ to it](/api/operators/apply_barrier).

## `activated`

[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Barrier_ is activated.

## `deactivated`

[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Barrier_ is deactivated.
100 changes: 100 additions & 0 deletions apps/website/docs/recipes/auth_token.md
@@ -0,0 +1,100 @@
---
outline: [2, 3]
---

# Auth token

There is a popular task in frontend development: to check if the users token is valid before data fetching and perform some actions if it is not. In this case study we will discuss how to implement this task with the help of `@farfetched/core` package.

<!--@include: ../shared/case.md-->

## Kick-off

Let us say we have a website with a login form. After the user submits the form, we send a request to the server and get some token in response. We store this token in the local storage or in the cookies and use it for some subsequent requests.

If something on the website tries to fetch data from the server that requires authentication, we need to check if the token is still valid. If it is not, we need to refresh the token, and continue the previous request.

In different cases, we can have different information about the token. In general, we can divide all cases into two groups:

::: details Client **do** have access to the content of the token

Sometimes the client has access to the content of the token. For example, if the token is stored in the local storage, we can get it with the help of `localStorage.getItem('SOME_KEY')`. In this case, we can check if the token is valid by decoding it and checking the expiration date.

In this case, we assume that the token is a JWT token that contains the expiration date and could be decoded without any additional information.
:::

::: details Client **do not** have access to the content of the token

Sometimes the client does not have access to the content of the token. For example, if the token is stored in the cookie that marked as `httpOnly`, we cannot get it with the help of `document.cookie`. In this case, we need to send a request to the server and check if the response is `200 OK` or `401 Unauthorized`.
:::

These two cases are very similar, so implementation will be the same for both of them with a little difference in the definition of unauthorized access.

## Implementation

Farfetched provides a [_Barrier_](/api/primitives/barrier) abstraction that allows us to implement this task in a declarative way. Its usage splits into two parts: barrier creation and barrier application. Let us start with the barrier creation since it is the most important part.

### Barrier creation

The creation of the barrier depends on the type of the token.

If the client has access to the content of the token, we can create a barrier with explicit checking of the token expiration date.

Let us assume that the token is a JWT token that contains the expiration date and could be decoded without any additional information. Such a token would be stored in a [_Store_](https://effector.dev/docs/api/effector/store) and can be accessed across the application.

```ts{6}
import { createBarrier } from '@farfetched/core';
const $authToken = createStore(/* ... */);
const authBarrier = createBarrier({
active: combine($authToken, (token) => isTokenInvalid(token)),
});
```

If the client does not have access to the content of the token, we have to rely on the response from the server on particular request. In this case, we can create a barrier with explicit checking of the response status of every request which requires authentication.

```ts{4-6}
import { createBarrier, isHttpError } from '@farfetched/core';
const authBarrier = createBarrier({
activateOn: {
failure: isHttpError(401),
},
});
```

It is only difference between these two cases. However, in both cases, we need to refresh the token if it is invalid. Let us say that refreshing the token is a [_Mutation_](/api/primitives/mutation).

```ts{7}
import { createBarrier, createMutation } from '@farfetched/core';
const renewTokenMutation = createMutation(/* ... */);
const authBarrier = createBarrier({
/* ... */
perform: [renewTokenMutation],
});
```

Now we have a [_Barrier_](/api/primitives/barrier) that will be activated if the token is invalid and will refresh the token if it is activated. It is time to apply this [_Barrier_](/api/primitives/barrier) to the requests that require authentication.

### Barrier application

This part is very simple. We just need to [`applyBarrier`](/api/operators/apply_barrier) to every [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) that requires authentication.

```ts{5}
import { createQuery, applyBarrier } from '@farfetched/core';
const someQuery = createQuery(/* ... */);
applyBarrier(someQuery, authBarrier);
```

That is it! Now every time `someQuery` is called, `authBarrier` will be checked. If the token is invalid, `authBarrier` will be activated, `someQuery` will be suspended, and `renewTokenMutation` will be called. After `renewTokenMutation` is finished, `someQuery` will be resumed in case of suspension or restarted with the latest parameters in case of failure.

## Conclusion

Barrier API is very flexible and allows us to implement different tasks in a declarative way. In this case study, we have discussed how to implement the task of checking the token validity before data fetching.

However, this is not the only task that can be implemented with the help of barriers. You can use them to implement anything that requires some actions to be performed before particular operation with suspension of this operation until the actions are finished.
53 changes: 53 additions & 0 deletions apps/website/docs/recipes/terms_of_use.md
@@ -0,0 +1,53 @@
# Pause world until user action

In web applications, we often need to pause the execution of a particular [_Query_](/api/primitives/query) until the user performs some action. For example, we want to pause the execution of the [_Query_](/api/primitives/query) that fetches the user's data until the user accepts the terms of use and privacy policy. In this case study we will discuss how to implement this task with the help of `@farfetched/core` package.

<!--@include: ../shared/case.md-->

## Kick-off

Let us say we have a website which requires the user to accept the terms of use before the user can use it. We have a bunch of [_Queries_](/api/primitives/query) and [_Mutations_](/api/primitives/mutation), and we want to prevent their execution until the user accepts the terms of use. By legal requirements, we need to show the terms of use to the user and ask the user to accept them and must not allow any network requests until the user accepts it.

Accepting the terms of use is a user action that can be represented as an [_Event_](https://effector.dev/docs/api/effector/event) for this recipe.

```ts
// terms_of_use.model.ts
import { createEvent } from 'effector';

export const termsOfUseShowed = createEvent();
export const userAcceptedTermsOfUse = createEvent();
```

[_Event_](https://effector.dev/docs/api/effector/event) `userAcceptedTermsOfUse` should be bound to the button in the terms of use dialog and [_Event_](https://effector.dev/docs/api/effector/event) `termsOfUseShowed` to the lifecycle of the app. However, it is out of scope of this recipe.

## Implementation

Farfetched provides a [_Barrier_](/api/primitives/barrier) abstraction that allows us to implement this task in a declarative way. Its usage splits into two parts: barrier creation and barrier application. Let us start with the barrier creation since it is the most important part.

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

const termsOfUseBarrier = createBarrier({
// activate barrier when termsOfUseShowed
activateOn: showTermsOfUse,
// and deactivate it when userAcceptedTermsOfUse
deactivateOn: userAcceptedTermsOfUse,
});
```

Next, we need to apply the barrier to the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) that we want to pause.

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

applyBarrier(sensitiveQuery, termsOfUseBarrier);
applyBarrier(sensitiveMutation, termsOfUseBarrier);
```

Now it is guaranteed that the `sensitiveQuery` and `sensitiveMutation` will be suspended until the `termsOfUseBarrier` is deactivated. `termsOfUseBarrier` represents the state of the terms of use acceptance.

## Conclusion

Barrier API is very flexible and allows us to implement different tasks in a declarative way. In this case study, we have discussed how to implement the task of blocking particular [_Queries_](/api/primitives/query) and [_Mutations_](/api/primitives/mutation) until the user accepts the terms of use.

However, this is not the only task that can be implemented with the help of barriers. You can use them to implement anything that requires some actions to be performed before particular operation with suspension of this operation until the actions are finished.
17 changes: 17 additions & 0 deletions apps/website/docs/tutorial/barrier_api.md
@@ -0,0 +1,17 @@
# Barrier API

:::tip You will learn

- How to postpone execution of a [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) until some conditions are met
- How to perform some actions before the execution of a [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation)

:::

In the previous chapters, we learned how to fetch data from the server and how to display it. Now let us talk about suspending the execution of [_Queries_](/api/primitives/query) and [_Mutations_](/api/primitives/mutation) based on some criteria.

This topic is quire practical, so we suggest learning it by examples:

1. [How to check if the users token is valid before data fetching and perform some actions if it is not](/recipes/auth_token)
2. [How to pause the execution of a _Query_ until the user accepts term of use](/recipes/terms_of_use)

If you want to learn more about the Barrier API, you can read the [API reference](/api/primitives/barrier).
5 changes: 5 additions & 0 deletions packages/core/index.ts
Expand Up @@ -99,3 +99,8 @@ export {

// Trigger API
export { keepFresh } from './src/trigger_api/keep_fresh';

// Barrier API
export { type Barrier } from './src/barrier_api/type';
export { applyBarrier } from './src/barrier_api/apply_barrier';
export { createBarrier } from './src/barrier_api/create_barrier';
2 changes: 1 addition & 1 deletion packages/core/project.json
Expand Up @@ -76,7 +76,7 @@
"size": {
"executor": "./tools/executors/size-limit:size-limit",
"options": {
"limit": "22 kB",
"limit": "23 kB",
"outputPath": "dist/packages/core"
},
"dependsOn": [
Expand Down

0 comments on commit c925412

Please sign in to comment.