Repository: igorkamyshev/farfetched
Pull request #430 (2024-01-12 14:29:15)

Add Valibot contract integration
[Valibot](https://valibot.dev/) is a new popular runtime validation library.
This PR adds contracts integration for Farfetched.


vitalybaev: Great, no rush and take your time 🙂
I'll take a look at Valibot error messages, maybe we can get something more meaningful from there
igorkamyshev: Hey, I've checked all about build of the package and decided to target this PR to branch `next-012` with the release 0.12. In the branch, I removed `nx` and migrated to bare Vite/pnpm.

Do you mind upgrading your PR based on recent changes? If you do not, please let me know, I'll do it myself 🙏
vitalybaev: As discussed I've pushed the changes:

* branch synced with the target branch with `nx` removed
* pinned `valibot` version as `0.28.0` since they improved error messages.
* accordingly updated tests to be aligned with the new error messages


Patch:
diff --git a/apps/showcase-forest-real-world-breaking-bad/package.json b/apps/showcase-forest-real-world-breaking-bad/package.json
index 8beea3d..13918e0 100644
--- a/apps/showcase-forest-real-world-breaking-bad/package.json
+++ b/apps/showcase-forest-real-world-breaking-bad/package.json
@@ -5,5 +5,8 @@
   "scripts": {
     "dev": "vite dev",
     "build": "vite build"
+  },
+  "dependencies": {
+    "runtypes": "^6.5.1"
   }
 }
diff --git a/apps/showcase-forest-real-world-breaking-bad/src/features/quote/query.ts b/apps/showcase-forest-real-world-breaking-bad/src/features/quote/query.ts
index 5293f28..08083dc 100644
--- a/apps/showcase-forest-real-world-breaking-bad/src/features/quote/query.ts
+++ b/apps/showcase-forest-real-world-breaking-bad/src/features/quote/query.ts
@@ -1,4 +1,4 @@
-import { createJsonQuery, declareParams } from '@farfetched/core';
+import { concurrency, createJsonQuery, declareParams } from '@farfetched/core';
 import { runtypeContract } from '@farfetched/runtypes';
 import { Array } from 'runtypes';
 
@@ -16,3 +16,5 @@ export const randomQuotesQuery = createJsonQuery({
     contract: runtypeContract(Array(Quote)),
   },
 });
+
+concurrency(randomQuotesQuery, { strategy: 'TAKE_LATEST' });
diff --git a/apps/showcase-forest-real-world-breaking-bad/tsconfig.json b/apps/showcase-forest-real-world-breaking-bad/tsconfig.json
index 02e967f..7e68bdd 100644
--- a/apps/showcase-forest-real-world-breaking-bad/tsconfig.json
+++ b/apps/showcase-forest-real-world-breaking-bad/tsconfig.json
@@ -1,6 +1,7 @@
 {
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
+    "declaration": false,
     "target": "ESNext",
     "useDefineForClassFields": true,
     "module": "ESNext",
diff --git a/apps/showcase-react-real-world-pokemons/package.json b/apps/showcase-react-real-world-pokemons/package.json
index 96c9a6d..d87f676 100644
--- a/apps/showcase-react-real-world-pokemons/package.json
+++ b/apps/showcase-react-real-world-pokemons/package.json
@@ -5,5 +5,8 @@
   "scripts": {
     "dev": "vite dev",
     "build": "vite build"
+  },
+  "dependencies": {
+    "runtypes": "^6.5.1"
   }
 }
diff --git a/apps/showcase-react-real-world-pokemons/src/pages/main/model.ts b/apps/showcase-react-real-world-pokemons/src/pages/main/model.ts
index 34b3bef..7a8ca3e 100644
--- a/apps/showcase-react-real-world-pokemons/src/pages/main/model.ts
+++ b/apps/showcase-react-real-world-pokemons/src/pages/main/model.ts
@@ -1,4 +1,4 @@
-import { createJsonQuery, declareParams } from '@farfetched/core';
+import { concurrency, createJsonQuery, declareParams } from '@farfetched/core';
 import { runtypeContract } from '@farfetched/runtypes';
 import { Array, Number, Record } from 'runtypes';
 import { createGate } from 'effector-react';
@@ -38,6 +38,8 @@ const pokemonListQuery = createJsonQuery({
   },
 });
 
+concurrency(pokemonListQuery, { strategy: 'TAKE_LATEST' });
+
 export const $pokemonList = pokemonListQuery.$data.map(
   (response) => response?.results ?? []
 );
diff --git a/apps/showcase-react-real-world-pokemons/src/pages/pokemon/model.ts b/apps/showcase-react-real-world-pokemons/src/pages/pokemon/model.ts
index 0f5f7bb..1981e96 100644
--- a/apps/showcase-react-real-world-pokemons/src/pages/pokemon/model.ts
+++ b/apps/showcase-react-real-world-pokemons/src/pages/pokemon/model.ts
@@ -1,4 +1,9 @@
-import { connectQuery, createJsonQuery, declareParams } from '@farfetched/core';
+import {
+  concurrency,
+  connectQuery,
+  createJsonQuery,
+  declareParams,
+} from '@farfetched/core';
 import { runtypeContract } from '@farfetched/runtypes';
 import { createGate } from 'effector-react';
 import { combine, sample } from 'effector';
@@ -30,6 +35,8 @@ const pokemonQuery = createJsonQuery({
   },
 });
 
+concurrency(pokemonQuery, { strategy: 'TAKE_LATEST' });
+
 const speciesQuery = createJsonQuery({
   params: declareParams<{ id: TId }>(),
   request: {
@@ -41,6 +48,8 @@ const speciesQuery = createJsonQuery({
   },
 });
 
+concurrency(speciesQuery, { strategy: 'TAKE_LATEST' });
+
 export const $pending = pokemonQuery.$pending;
 export const $pokemon = combine(
   { pokemon: pokemonQuery.$data, species: speciesQuery.$data },
diff --git a/apps/showcase-react-real-world-pokemons/tsconfig.json b/apps/showcase-react-real-world-pokemons/tsconfig.json
index 68a897e..6162264 100644
--- a/apps/showcase-react-real-world-pokemons/tsconfig.json
+++ b/apps/showcase-react-real-world-pokemons/tsconfig.json
@@ -1,6 +1,7 @@
 {
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
+    "declaration": false,
     "target": "ESNext",
     "useDefineForClassFields": true,
     "module": "ESNext",
diff --git a/apps/showcase-solid-real-world-rick-morty/package.json b/apps/showcase-solid-real-world-rick-morty/package.json
index 3508d39..8180f3c 100644
--- a/apps/showcase-solid-real-world-rick-morty/package.json
+++ b/apps/showcase-solid-real-world-rick-morty/package.json
@@ -5,5 +5,8 @@
   "scripts": {
     "dev": "vite dev",
     "build": "vite build"
+  },
+  "dependencies": {
+    "runtypes": "^6.5.1"
   }
 }
diff --git a/apps/showcase-solid-real-world-rick-morty/src/entities/character/query.ts b/apps/showcase-solid-real-world-rick-morty/src/entities/character/query.ts
index a471250..8026a58 100644
--- a/apps/showcase-solid-real-world-rick-morty/src/entities/character/query.ts
+++ b/apps/showcase-solid-real-world-rick-morty/src/entities/character/query.ts
@@ -1,4 +1,4 @@
-import { createJsonQuery, declareParams } from '@farfetched/core';
+import { concurrency, createJsonQuery, declareParams } from '@farfetched/core';
 import { runtypeContract } from '@farfetched/runtypes';
 import { Array, Record } from 'runtypes';
 
@@ -8,7 +8,7 @@ import { characterUrl } from './api';
 import { Character } from './contract';
 
 export function createCharacterQuery() {
-  return createJsonQuery({
+  const q = createJsonQuery({
     params: declareParams<{ id: TId }>(),
     request: {
       url: ({ id }) => characterUrl({ id }),
@@ -16,6 +16,10 @@ export function createCharacterQuery() {
     },
     response: { contract: runtypeContract(Character) },
   });
+
+  concurrency(q, { strategy: 'TAKE_LATEST' });
+
+  return q;
 }
 
 export function createCharacterListQuery<T>({
@@ -23,7 +27,7 @@ export function createCharacterListQuery<T>({
 }: {
   mapParams: (params: T) => { ids: TId[] };
 }) {
-  return createJsonQuery({
+  const q = createJsonQuery({
     params: declareParams<T>(),
     request: {
       url: (params) => characterUrl(mapParams(params)),
@@ -31,10 +35,14 @@ export function createCharacterListQuery<T>({
     },
     response: { contract: runtypeContract(Array(Character)) },
   });
+
+  concurrency(q, { strategy: 'TAKE_LATEST' });
+
+  return q;
 }
 
 export function createCharacterPageQuery() {
-  return createJsonQuery({
+  const q = createJsonQuery({
     params: declareParams<{ page: number }>(),
     request: {
       url: characterUrl(),
@@ -47,4 +55,8 @@ export function createCharacterPageQuery() {
       ),
     },
   });
+
+  concurrency(q, { strategy: 'TAKE_LATEST' });
+
+  return q;
 }
diff --git a/apps/showcase-solid-real-world-rick-morty/src/entities/episode/query.ts b/apps/showcase-solid-real-world-rick-morty/src/entities/episode/query.ts
index 425ab86..1fdc44e 100644
--- a/apps/showcase-solid-real-world-rick-morty/src/entities/episode/query.ts
+++ b/apps/showcase-solid-real-world-rick-morty/src/entities/episode/query.ts
@@ -1,4 +1,4 @@
-import { createJsonQuery, declareParams } from '@farfetched/core';
+import { concurrency, createJsonQuery, declareParams } from '@farfetched/core';
 import { runtypeContract } from '@farfetched/runtypes';
 import { Array, Record } from 'runtypes';
 
@@ -8,7 +8,7 @@ import { episodeUrl } from './api';
 import { Episode } from './contract';
 
 export function createEpisodeQuery() {
-  return createJsonQuery({
+  const q = createJsonQuery({
     params: declareParams<{ id: TId }>(),
     request: {
       url: ({ id }) => episodeUrl({ id }),
@@ -16,6 +16,10 @@ export function createEpisodeQuery() {
     },
     response: { contract: runtypeContract(Episode) },
   });
+
+  concurrency(q, { strategy: 'TAKE_LATEST' });
+
+  return q;
 }
 
 export function createEpisodeListQuery<T>({
@@ -23,7 +27,7 @@ export function createEpisodeListQuery<T>({
 }: {
   mapParams: (params: T) => { ids: TId[] };
 }) {
-  return createJsonQuery({
+  const q = createJsonQuery({
     params: declareParams<T>(),
     request: {
       url: (params) => episodeUrl(mapParams(params)),
@@ -31,10 +35,14 @@ export function createEpisodeListQuery<T>({
     },
     response: { contract: runtypeContract(Array(Episode)) },
   });
+
+  concurrency(q, { strategy: 'TAKE_LATEST' });
+
+  return q;
 }
 
 export function createEpisodePageQuery() {
-  return createJsonQuery({
+  const q = createJsonQuery({
     params: declareParams<{ page: number }>(),
     request: {
       url: episodeUrl(),
@@ -47,4 +55,8 @@ export function createEpisodePageQuery() {
       ),
     },
   });
+
+  concurrency(q, { strategy: 'TAKE_LATEST' });
+
+  return q;
 }
diff --git a/apps/showcase-solid-real-world-rick-morty/src/entities/location/query.ts b/apps/showcase-solid-real-world-rick-morty/src/entities/location/query.ts
index 04e283b..450ccb9 100644
--- a/apps/showcase-solid-real-world-rick-morty/src/entities/location/query.ts
+++ b/apps/showcase-solid-real-world-rick-morty/src/entities/location/query.ts
@@ -1,4 +1,4 @@
-import { createJsonQuery, declareParams } from '@farfetched/core';
+import { concurrency, createJsonQuery, declareParams } from '@farfetched/core';
 import { runtypeContract } from '@farfetched/runtypes';
 
 import { TId } from '../../shared/id';
@@ -10,9 +10,13 @@ export function createLocationQuery<T>({
 }: {
   mapParams: (params: T) => { id: TId };
 }) {
-  return createJsonQuery({
+  const q = createJsonQuery({
     params: declareParams<T>(),
     request: { url: (params) => locationUrl(mapParams(params)), method: 'GET' },
     response: { contract: runtypeContract(Location) },
   });
+
+  concurrency(q, { strategy: 'TAKE_LATEST' });
+
+  return q;
 }
diff --git a/apps/showcase-solid-real-world-rick-morty/tsconfig.json b/apps/showcase-solid-real-world-rick-morty/tsconfig.json
index be35f60..cbf43c7 100644
--- a/apps/showcase-solid-real-world-rick-morty/tsconfig.json
+++ b/apps/showcase-solid-real-world-rick-morty/tsconfig.json
@@ -1,6 +1,7 @@
 {
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
+    "declaration": false,
     "jsx": "preserve",
     "jsxImportSource": "solid-js",
     "allowJs": true,
diff --git a/apps/website/docs/.vitepress/config.js b/apps/website/docs/.vitepress/config.js
index 036a007..b4f8c57 100644
--- a/apps/website/docs/.vitepress/config.js
+++ b/apps/website/docs/.vitepress/config.js
@@ -111,6 +111,7 @@ export default withMermaid(
                     link: '/tutorial/update_query',
                   },
                   { text: 'Retries', link: '/tutorial/retries' },
+                  { text: 'Concurrency', link: '/tutorial/concurrency' },
                   { text: 'Caching', link: '/tutorial/caching' },
                 ],
               },
@@ -205,6 +206,7 @@ export default withMermaid(
               { text: 'retry', link: '/api/operators/retry' },
               { text: 'timeout', link: '/api/operators/timeout' },
               { text: 'cache', link: '/api/operators/cache' },
+              { text: 'concurrency', link: '/api/operators/concurrency' },
               { text: 'applyBarrier', link: '/api/operators/apply_barrier' },
               { text: 'update', link: '/api/operators/update' },
               {
@@ -270,6 +272,10 @@ export default withMermaid(
                     text: 'typed-contracts',
                     link: '/api/contracts/typed-contracts',
                   },
+                  {
+                    text: 'Valibot',
+                    link: '/api/contracts/valibot',
+                  },
                 ],
               },
               {
diff --git a/apps/website/docs/adr/concurrency.md b/apps/website/docs/adr/concurrency.md
new file mode 100644
index 0000000..0e6f935
--- /dev/null
+++ b/apps/website/docs/adr/concurrency.md
@@ -0,0 +1,150 @@
+---
+title: Introduction of concurrency
+
+outline: [2, 3]
+
+version: v0.12
+---
+
+# Introduction of `concurrency`
+
+This ADR is effective starting from [v0.12](/releases/0-12).
+
+::: info TL;DR
+
+To apply concurrency settings to the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) use [`concurrency` operator](/api/operators/concurrency):
+
+```ts
+import { concurrency } from '@farfetched/core';
+
+concurrency(operation, { strategy: 'TAKE_LATEST' });
+```
+
+To abort all in-flight requests use `abortAll` [_Event_](https://effector.dev/en/api/effector/event/):
+
+```ts
+import { createEvent } from 'effector';
+import { concurrency } from '@farfetched/core';
+
+const abortAll = createEvent();
+
+concurrency(operation, { abortAll });
+```
+
+By default, no concurrency settings are applied.
+
+:::
+
+## Configuration vs. Operators
+
+This change is about global movement of Farfetched's API from configuration to operators. Configuration of operation during its creation has a couple of drawbacks:
+
+- It makes creation of custom factories in user-land harder, because they have to support all possible configurations and their combinations. On the other hand, operators are easier to support because they are just functions that can be called with any operation.
+
+- It makes code-generation harder, because it requires to generate the whole configuration object in generated code and user does not have a chance to modify it. On the other hand, operators can be called in separate file and its calls would not be affected by code-generation.
+
+The last but not least, operator is more Effector-ish way of doing things, because any operator is just a set of [`sample`](https://effector.dev/en/api/effector/sample/)-s which is foundation of Effector.
+
+For example, let us take a look at the following code:
+
+```ts
+retry(query, {
+  times: 12,
+});
+```
+
+Basically, it can be rewritten as:
+
+```ts
+const nextRetry = createEvent();
+const $retriesNotExceededLimit = createStore(false);
+
+sample({
+  clock: query.finished.failed,
+  filter: $retriesNotExceededLimit,
+  target: [query.start, nextRetry],
+});
+
+sample({
+  clock: nextRetry,
+  source: $retryNumber,
+  fn: (retryNumber) => retryNumber <= 12,
+  target: $retriesNotExceededLimit,
+});
+```
+
+:::tip
+It is not a real implementation of `retry` operator, but it is a good example of how it can be implemented using Effector primitives.
+:::
+
+So, Farfetched's API is moving from configuration to operators ([`retry`](/api/operators/retry), [`cache`](/api/operators/cache), [`timeout`](/api/operators/timeout), etc.), and this ADR is about moving `concurrency` settings to operator as well.
+
+## Future of `concurrency`
+
+### Historical context
+
+Historically, concurrency settings were applied directly to the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) using [`createJsonQuery`](/api/factories/create_json_query) and [`createJsonMutation`](/api/factories/create_json_mutation) factories and `concurrency` field:
+
+```ts
+import { createJsonQuery } from '@farfetched/core';
+
+const query = createJsonQuery({
+  concurrency: { strategy: 'TAKE_FIRST' },
+});
+```
+
+Furthermore, `concurrency.strategy` has a default value `TAKE_LATEST` which means that by default, all in-flight requests are aborted except the latest one. But only [_Queries_](/api/primitives/query) created by [`createJsonQuery`](/api/factories/create_json_query) had this default, any other operations had `TAKE_EVERY` as a default.
+
+This behavior is considered harmful and after introducing the [`concurrency`](/api/operators/concurrency) operator, default concurrency strategy is switched to `TAKE_EVERY` for all use-case which means that all in-flight requests are executed concurrently.
+
+### Configuration to operator
+
+Field `concurrency` in [`createJsonQuery`](/api/factories/create_json_query) and [`createJsonMutation`](/api/factories/create_json_mutation) is deprecated. Instead, use [`concurrency`](/api/operators/concurrency) operator.
+
+`strategy` is mapped as is:
+
+```ts
+import { createJsonQuery, concurrency } from '@farfetched/core';
+
+const query = createJsonQuery({
+  concurrency: { strategy: 'TAKE_LATEST' }, // [!code --]
+});
+
+concurrency(query, { strategy: 'TAKE_LATEST' }); // [!code ++]
+```
+
+`abort` is renamed to `abortAll`:
+
+```ts
+import { createJsonQuery, concurrency } from '@farfetched/core';
+import { createEvent } from 'effector';
+
+const abortQuery = createEvent();
+
+const query = createJsonQuery({
+  concurrency: { abort: abortQuery }, // [!code --]
+});
+
+concurrency(query, { abortAll: abortQuery }); // [!code ++]
+```
+
+Furthermore, default concurrency strategy is switched to `TAKE_EVERY`. Before, the default strategy was `TAKE_LATEST` in `createJsonQuery`. Now, all operations have `TAKE_EVERY` as a default which means that all in-flight requests are executed concurrently.
+
+### Changes schedule
+
+Since this change requires a change in the user code, it is scheduled for the couple of releases:
+
+#### v0.12
+
+- Operator [`concurrency`](/api/operators/concurrency) is introduced. It can be used to apply concurrency settings to any [_Queries_](/api/primitives/query) or [_Mutations_](/api/primitives/mutation).
+- `concurrency` field in [`createJsonQuery`](/api/factories/create_json_query) and [`createJsonMutation`](/api/factories/create_json_mutation) is deprecated.
+- In case of usage operator [`concurrency`](/api/operators/concurrency) with [`createJsonQuery`](/api/factories/create_json_query) **without `concurrency` field**, everything is fine. No deprecation message is written.
+- In case of usage operator [`concurrency`](/api/operators/concurrency) with [`createJsonQuery`](/api/factories/create_json_query) **with `concurrency` field**, deprecation message is written with `console.warning`.
+
+#### v0.13
+
+- [`createJsonQuery`](/api/factories/create_json_query) **requires** applying [`concurrency`](/api/operators/concurrency) operator on then to specify concurrency settings. Otherwise, a deprecation message is written with `console.warning`. The reason for this is to make sure that the user is aware of the change of default behavior.
+
+#### v0.14
+
+- `concurrency` field in [`createJsonQuery`](/api/factories/create_json_query) and [`createJsonMutation`](/api/factories/create_json_mutation) is removed. Default strategy is `TAKE_EVERY` for any [_Mutation_](/api/primitives/mutation) or any [_Query_](/api/primitives/query).
diff --git a/apps/website/docs/api/contracts/valibot.md b/apps/website/docs/api/contracts/valibot.md
new file mode 100644
index 0000000..dc9d939
--- /dev/null
+++ b/apps/website/docs/api/contracts/valibot.md
@@ -0,0 +1,41 @@
+# Farfetched and Valibot
+
+Integration is distributed as a separate package, you have to install it and its peer dependencies before usage
+
+::: code-group
+
+```sh [pnpm]
+pnpm install valibot @farfetched/valibot
+```
+
+```sh [yarn]
+yarn add valibot @farfetched/valibot
+```
+
+```sh [npm]
+npm install valibot @farfetched/valibot
+```
+
+:::
+
+## `valibotContract`
+
+Creates a [_Contract_](/api/primitives/contract) based on given `BaseSchema`.
+
+```ts
+import { object, literal, number } from 'valibot';
+import { valibotContract } from '@farfetched/valibot';
+
+const Asteroid = object({
+  type: literal('asteroid'),
+  mass: number(),
+});
+
+const asteroidContract = valibotContract(Asteroid);
+
+/* typeof asteroidContract === Contract<
+ *   unknown, 👈 it accepts something unknown
+ *   { type: 'asteriod', mass: number }, 👈 and validates if it is an asteroid
+ * >
+ */
+```
diff --git a/apps/website/docs/api/factories/create_json_mutation.md b/apps/website/docs/api/factories/create_json_mutation.md
index 3b50a87..3ad44c4 100644
--- a/apps/website/docs/api/factories/create_json_mutation.md
+++ b/apps/website/docs/api/factories/create_json_mutation.md
@@ -29,4 +29,12 @@ Config fields:
   - `status.expected`: `number` or `Array<number>` of expected HTTP status codes, if the response status code is not in the list, the mutation will be treated as failed
 
 - `concurrency?`: concurrency settings for the [_Query_](/api/primitives/query)
+  ::: danger Deprecation warning
+
+  This field is deprecated since [v0.12](/releases/0-12) and will be removed in v0.14. Use [`concurrency` operator](/api/operators/concurrency) instead.
+
+  Please read [this ADR](/adr/concurrency) for more information and migration guide.
+
+  :::
+
   - `abort?`: [_Event_](https://effector.dev/en/api/effector/event/) after calling which all in-flight requests will be aborted
diff --git a/apps/website/docs/api/factories/create_json_query.md b/apps/website/docs/api/factories/create_json_query.md
index f090bf9..5675e5c 100644
--- a/apps/website/docs/api/factories/create_json_query.md
+++ b/apps/website/docs/api/factories/create_json_query.md
@@ -30,6 +30,13 @@ Config fields:
     - `{ source: Store, fn: (data, { result, params }) => mapped }`
 
 - `concurrency?`: concurrency settings for the [_Query_](/api/primitives/query)
+  ::: danger Deprecation warning
+
+  This field is deprecated since [v0.12](/releases/0-12) and will be removed in v0.14. Use [`concurrency` operator](/api/operators/concurrency) instead.
+
+  Please read [this ADR](/adr/concurrency) for more information and migration guide.
+
+  :::
 
   - `strategy?`: available values:
     - `TAKE_EVERY` execute every request
diff --git a/apps/website/docs/api/operators/concurrency.md b/apps/website/docs/api/operators/concurrency.md
new file mode 100644
index 0000000..d5d13cf
--- /dev/null
+++ b/apps/website/docs/api/operators/concurrency.md
@@ -0,0 +1,29 @@
+---
+outline: [2, 3]
+---
+
+# `concurrency` <Badge type="tip" text="since v0.12" />
+
+Applies concurrency settings to the [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation).
+
+## Formulae
+
+```ts
+import { concurrency } from '@farfetched/core';
+
+concurrency(operation, config);
+```
+
+Operation could be a [_Query_](/api/primitives/query) or a [_Mutation_](/api/primitives/mutation).
+
+Config fields:
+
+- `strategy?`: available strategies:
+  - _`TAKE_EVERY`_ (**default**) — every request will be executed
+  - _`TAKE_LATEST`_ — the latest request will be executed, all previous requests will be aborted
+  - _`TAKE_FIRST`_ — the first request will be executed, all subsequent requests will be skipped
+- `abortAll?`: [_Event_](https://effector.dev/en/api/effector/event/) after calling which all requests will be aborted immediately
+
+## References
+
+- [ADR: Introduction of `concurrency`](/adr/concurrency)
diff --git a/apps/website/docs/releases/0-12.md b/apps/website/docs/releases/0-12.md
index 4661492..a0e54ce 100644
--- a/apps/website/docs/releases/0-12.md
+++ b/apps/website/docs/releases/0-12.md
@@ -6,6 +6,10 @@ Some features of Farfetched are deprecated since v0.12 and will be removed in th
 
 ### `attachOperation` operator
 
-This operator is deprecated since [v0.12](/releases/0-12) and will be removed in v0.14. Please read [this ADR](/adr/attach_operation_deprecation) for more information and migration guide.
+This operator is deprecated and will be removed in v0.14. Please read [this ADR](/adr/attach_operation_deprecation) for more information and migration guide.
+
+### `concurrency` operator
+
+Field `concurrency` in `createJsonQuery` and `createJsonMutation` is deprecated and has to be replaced by the [`concurrency` operator](/api/operators/concurrency). Please read [this ADR](/adr/concurrency) for more information and migration guide.
 
 <!--@include: ./0-12.changelog.md-->
diff --git a/apps/website/docs/tutorial/concurrency.md b/apps/website/docs/tutorial/concurrency.md
new file mode 100644
index 0000000..6e25c3c
--- /dev/null
+++ b/apps/website/docs/tutorial/concurrency.md
@@ -0,0 +1,171 @@
+---
+outline: [2, 3]
+---
+
+# Cancellation and Concurrency
+
+:::tip You will learn
+
+- How to cancel in-flight operations
+- What concurrency strategies are available out-of-the-box
+- Limitations of concurrency applied to operations
+- How to react on cancelled operations
+
+:::
+
+## Operator `concurrency`
+
+All concurrency settings are controlled by the [`concurrency` operator](/api/operators/concurrency). It accepts any [_Query_](/api/primitives/query) or [_Mutation_](/api/primitives/mutation) and configuration object which determines concurrency settings.
+
+### Cancel all in-flight operations
+
+If you want to cancel all in-flight operations, you can use the `abortAll` option. It accepts an [_Event_](https://effector.dev/en/api/effector/event/) that should be called to cancel all in-flight operations.
+
+```ts{10-12}
+import { createEvent } from 'effector';
+import { concurrency, createQuery } from '@farfetched/core';
+
+const query = createQuery({
+  /* ... */
+});
+
+const somethingHappened = createEvent();
+
+// Anytime you call `somethingHappened`,
+// all in-flight operations will be cancelled immediately
+concurrency(query, { abortAll: somethingHappened });
+```
+
+### Concurrency strategies
+
+However, if you want to control concurrency on a per-operation basis, you can use the `strategy` option. It accepts one of the following values:
+
+#### `TAKE_EVERY`
+
+In this strategy, every time you call the operation, it will be executed immediately, regardless of whether there are any in-flight operations or not. It is a default strategy for all operations.
+
+```ts{7}
+import { concurrency, createQuery } from '@farfetched/core';
+
+const query = createQuery({
+  /* ... */
+});
+
+concurrency(query, { strategy: 'TAKE_EVERY' });
+```
+
+#### `TAKE_LATEST`
+
+By setting this strategy, you ensure that only the latest operation will be executed. If there are any in-flight operations, they will be cancelled immediately.
+
+```ts{7}
+import { concurrency, createQuery } from '@farfetched/core';
+
+const query = createQuery({
+  /* ... */
+});
+
+concurrency(query, { strategy: 'TAKE_LATEST' });
+```
+
+#### `TAKE_FIRST`
+
+In this strategy, only the first operation will be executed. If there are any in-flight operations, the new operation execution will be skipped.
+
+```ts{7}
+import { concurrency, createQuery } from '@farfetched/core';
+
+const query = createQuery({
+  /* ... */
+});
+
+concurrency(query, { strategy: 'TAKE_FIRST' });
+```
+
+## Limitations
+
+### `onAbort`
+
+::: tip
+If you are using [`createJsonQuery`](/api/factories/create_json_query) and [`createJsonMutation`](/api/factories/create_json_mutation), you do not have to worry about the [`onAbort`](/api/utils/on_abort) option. It is handled internally by the library.
+:::
+
+[`onAbort`](/api/utils/on_abort) is used to connect the operation's cancellation with internal cancellation mechanism on Farfetched. You have to call this function inside the handler of the operation **before** any asynchronous operation is started.
+
+It works pretty straightforward: when Farfetched cancels the operation, it will call the function you passed to `onAbort`. You can use it to cancel any asynchronous operation you have started.
+
+#### Fetch API example
+
+In case of using the Fetch API, you should use the `onAbort` function to bind a `AbortController` and Farfetched's concurrency mechanism.
+
+```ts{7-9}
+import { onAbort, createQuery } from '@farfetched/core';
+
+const query = createQuery({
+  async handler() {
+    const abortController = new AbortController();
+
+    onAbort(() => {
+      abortController.abort();
+    });
+
+    const response = await fetch('https://example.com', {
+      signal: abortController.signal,
+    });
+
+    return response.text();
+  },
+});
+```
+
+#### `axios` example
+
+The same approach can be used with `axios` library.
+
+```ts{8-10}
+import axios from 'axios';
+import { onAbort, createQuery } from '@farfetched/core';
+
+const query = createQuery({
+  async handler() {
+    const abortController = new AbortController();
+
+    onAbort(() => {
+      abortController.abort();
+    });
+
+    return axios.get('https://example.com', {
+      signal: controller.signal,
+    });
+  },
+});
+```
+
+### Migration in v0.12-0.14
+
+Originally some debatable decisions were made regarding the `concurrency` in factories [`createJsonQuery`](/api/factories/create_json_query) and [`createJsonMutation`](/api/factories/create_json_mutation). Please, read the [ADR](/adr/concurrency#changes-schedule) to understand the reasons behind the changes and the migration path.
+
+## Reactions
+
+Sometimes you need to react on cancelled operations. For example, you may want to show a message to the user that the operation was cancelled. You can use the `.aborted` [_Event_](https://effector.dev/en/api/effector/event/) of the operation to react on cancellation.
+
+Both [_Query_](/api/primitives/query#aborted) and [_Mutation_](/api/primitives/mutation#aborted) have the `.aborted` event.
+
+```ts{7}
+import { createQuery } from '@farfetched/core';
+
+const query = createQuery({
+  /* ... */
+});
+
+query.aborted // will be called when the operation is cancelled
+```
+
+## References
+
+- [API Reference: operator `concurrency`](/api/operators/concurrency)
+- [API Reference: function `onAbort`](/api/utils/on_abort)
+- [API Reference: _Event_ `.aborted` of _Query_](/api/primitives/query#aborted)
+- [API Reference: _Event_ `.aborted` of _Mutation_](/api/primitives/mutation#aborted)
+- [ADR: Introduction of `concurrency`](/adr/concurrency)
+- [Deep dive: Data flow in Remote Operation](/recipes/data_flow)
diff --git a/apps/website/docs/tutorial/contracts.md b/apps/website/docs/tutorial/contracts.md
index e7f4cba..7ee60e0 100644
--- a/apps/website/docs/tutorial/contracts.md
+++ b/apps/website/docs/tutorial/contracts.md
@@ -98,5 +98,6 @@ Farfetched provides integrations for the following third-party solutions:
 - [io-ts](/api/contracts/io-ts)
 - [Superstruct](/api/contracts/superstruct)
 - [typed-contracts](/api/contracts/typed-contracts)
+- [Valibot](/api/contracts/valibot)
 
 If you are using any other solution for contracts, you can easily create a wrapper for it to case your internal contracts to Farfetched [_Contract_](/api/primitives/contract). Check source code of [Runtypes integration](https://github.com/igorkamyshev/farfetched/blob/master/packages/runtypes/src/runtype_contract.ts) for inspiration.
diff --git a/packages/core/package.json b/packages/core/package.json
index c0ca15a..2109d08 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -42,7 +42,7 @@
   "size-limit": [
     {
       "path": "./dist/core.js",
-      "limit": "15.7 kB"
+      "limit": "15.9 kB"
     }
   ]
 }
diff --git a/packages/core/src/concurrency/concurrency.ts b/packages/core/src/concurrency/concurrency.ts
index d926482..e460b44 100644
--- a/packages/core/src/concurrency/concurrency.ts
+++ b/packages/core/src/concurrency/concurrency.ts
@@ -16,18 +16,26 @@ import { abortError } from '../errors/create_error';
  * - `TAKE_EVERY` will not cancel any requests
  * - `TAKE_LATEST` will cancel all but the latest request
  * - `TAKE_FIRST` will ignore all but the first request
- * @param config.abort All requests will be aborted on this event call
+ * @param config.abortAll All requests will be aborted on this event call
  */
 export function concurrency(
   op: RemoteOperation<any, any, any, any>,
   {
     strategy,
-    abort,
+    abortAll,
   }: {
     strategy?: 'TAKE_LATEST' | 'TAKE_EVERY' | 'TAKE_FIRST';
-    abort?: Event<any>;
+    abortAll?: Event<any>;
   }
 ) {
+  if (op.__.meta.flags.concurrencyFieldUsed) {
+    console.warn(
+      `Both concurrency-operator and concurrency-field are used  on operation ${op.__.meta.name}.`,
+      `Please use only concurrency-operator, because field concurrency-field in createJsonQuery and createJsonMutation is deprecated and will be deleted soon.`
+    );
+  }
+  op.__.meta.flags.concurrencyOperatorUsed = true;
+
   const $callObjects = createStore<CallObject[]>([], { serialize: 'ignore' });
   sample({
     clock: op.__.lowLevelAPI.callObjectCreated,
@@ -110,8 +118,8 @@ export function concurrency(
     }
   }
 
-  if (abort) {
-    sample({ clock: abort, target: abortAllFx });
+  if (abortAll) {
+    sample({ clock: abortAll, target: abortAllFx });
   }
 
   return op;
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 7c3f598..6e90fab 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -110,3 +110,6 @@ export { createBarrier } from './barrier_api/create_barrier';
 
 // Inspect API
 export { NodeMetaSumbol, NodeLinksSumbol } from './inspect';
+
+// Concurrency API
+export { concurrency } from './concurrency/concurrency';
diff --git a/packages/core/src/mutation/create_json_mutation.ts b/packages/core/src/mutation/create_json_mutation.ts
index 66a0230..b756fca 100644
--- a/packages/core/src/mutation/create_json_mutation.ts
+++ b/packages/core/src/mutation/create_json_mutation.ts
@@ -58,6 +58,10 @@ interface BaseJsonMutationConfigNoParams<
     HeadersSource,
     UrlSource
   >;
+  /**
+   * @deprecated Deprecated since 0.12, use `concurrency` operator instead
+   * @see {@link https://farfetched.pages.dev/adr/concurrency}
+   */
   concurrency?: ConcurrencyConfig;
 }
 
@@ -77,6 +81,10 @@ interface BaseJsonMutationConfigWithParams<
     HeadersSource,
     UrlSource
   >;
+  /**
+   * @deprecated Deprecated since 0.12, use `concurrency` operator instead
+   * @see {@link https://farfetched.pages.dev/adr/concurrency}
+   */
   concurrency?: ConcurrencyConfig;
 }
 
@@ -257,10 +265,20 @@ export function createJsonMutation(config: any): Mutation<any, any, any> {
   };
 
   /* TODO: in future releases we will remove this code and make concurrency a separate function */
-  concurrency(op, {
-    strategy: config.concurrency?.strategy ?? 'TAKE_EVERY',
-    abort: config.concurrency?.abort,
-  });
+  if (config.concurrency) {
+    op.__.meta.flags.concurrencyFieldUsed = true;
+  }
+
+  if (config.concurrency) {
+    setTimeout(() => {
+      if (!op.__.meta.flags.concurrencyOperatorUsed) {
+        concurrency(op, {
+          strategy: config.concurrency?.strategy ?? 'TAKE_EVERY',
+          abortAll: config.concurrency?.abort,
+        });
+      }
+    });
+  }
 
   return op;
 }
diff --git a/packages/core/src/mutation/type.ts b/packages/core/src/mutation/type.ts
index 81ef4de..afbdf3b 100644
--- a/packages/core/src/mutation/type.ts
+++ b/packages/core/src/mutation/type.ts
@@ -5,7 +5,7 @@ export const MutationSymbol = Symbol('Mutation');
 
 // eslint-disable-next-line @typescript-eslint/no-empty-interface
 export interface Mutation<Params, Data, Error>
-  extends RemoteOperation<Params, Data, Error, null> {
+  extends RemoteOperation<Params, Data, Error, {}> {
   '@@unitShape': () => {
     start: EventCallable<Params>;
     pending: Store<boolean>;
diff --git a/packages/core/src/query/create_json_query.ts b/packages/core/src/query/create_json_query.ts
index e3513b8..79d4f0a 100644
--- a/packages/core/src/query/create_json_query.ts
+++ b/packages/core/src/query/create_json_query.ts
@@ -59,6 +59,10 @@ interface BaseJsonQueryConfigNoParams<
     HeadersSource,
     UrlSource
   >;
+  /**
+   * @deprecated Deprecated since 0.12, use `concurrency` operator instead
+   * @see {@link https://farfetched.pages.dev/adr/concurrency}
+   */
   concurrency?: ConcurrencyConfig;
 }
 
@@ -78,6 +82,10 @@ interface BaseJsonQueryConfigWithParams<
     HeadersSource,
     UrlSource
   >;
+  /**
+   * @deprecated Deprecated since 0.12, use `concurrency` operator instead
+   * @see {@link https://farfetched.pages.dev/adr/concurrency}
+   */
   concurrency?: ConcurrencyConfig;
 }
 
@@ -385,9 +393,17 @@ export function createJsonQuery(config: any) {
   };
 
   /* TODO: in future releases we will remove this code and make concurrency a separate function */
-  concurrency(op, {
-    strategy: config.concurrency?.strategy ?? 'TAKE_LATEST',
-    abort: config.concurrency?.abort,
+  if (config.concurrency) {
+    op.__.meta.flags.concurrencyFieldUsed = true;
+  }
+
+  setTimeout(() => {
+    if (!op.__.meta.flags.concurrencyOperatorUsed) {
+      concurrency(op, {
+        strategy: config.concurrency?.strategy ?? 'TAKE_LATEST',
+        abortAll: config.concurrency?.abort,
+      });
+    }
   });
 
   return op;
diff --git a/packages/core/src/remote_operation/create_remote_operation.ts b/packages/core/src/remote_operation/create_remote_operation.ts
index a9208e8..bb9f999 100644
--- a/packages/core/src/remote_operation/create_remote_operation.ts
+++ b/packages/core/src/remote_operation/create_remote_operation.ts
@@ -454,7 +454,7 @@ export function createRemoteOperation<
     $enabled,
     __: {
       executeFx,
-      meta: { ...meta, name },
+      meta: { ...meta, name, flags: {} },
       kind,
       $latestParams: readonly($latestParams),
       lowLevelAPI: {
diff --git a/packages/core/src/remote_operation/type.ts b/packages/core/src/remote_operation/type.ts
index f40c7bd..f8ed45d 100644
--- a/packages/core/src/remote_operation/type.ts
+++ b/packages/core/src/remote_operation/type.ts
@@ -11,6 +11,7 @@ import type { CallObject } from './with_call_object';
 
 interface DefaultMeta {
   name: string;
+  flags: Record<string, boolean>;
 }
 
 export interface RemoteOperation<
diff --git a/packages/valibot/README.md b/packages/valibot/README.md
new file mode 100644
index 0000000..78b7fb7
--- /dev/null
+++ b/packages/valibot/README.md
@@ -0,0 +1,3 @@
+# @farfetched/valibot
+
+Read documentation [here](https://farfetched.pages.dev/api/contracts/valibot.html).
diff --git a/packages/valibot/package.json b/packages/valibot/package.json
new file mode 100644
index 0000000..bfa5ab4
--- /dev/null
+++ b/packages/valibot/package.json
@@ -0,0 +1,52 @@
+{
+  "name": "@farfetched/valibot",
+  "version": "0.11.2",
+  "publishConfig": {
+    "access": "public"
+  },
+  "license": "MIT",
+  "repository": "https://github.com/igorkamyshev/farfetched",
+  "devDependencies": {
+    "effector": "^23.0.0",
+    "@farfetched/core": "workspace:*",
+    "valibot": "^0.28.0"
+  },
+  "peerDependencies": {
+    "effector": "^23.0.0",
+    "@farfetched/core": "workspace:*",
+    "valibot": "^0.28.0"
+  },
+  "scripts": {
+    "test:run": "vitest run",
+    "typetest:run": "vitest typecheck --run",
+    "build": "vite build",
+    "size": "size-limit",
+    "publint": "node ../../tools/scripts/publint.mjs",
+    "typelint": "attw --pack"
+  },
+  "type": "module",
+  "files": [
+    "dist"
+  ],
+  "main": "./dist/valibot.cjs",
+  "module": "./dist/valibot.js",
+  "types": "./dist/valibot.d.ts",
+  "exports": {
+    ".": {
+      "import": {
+        "types": "./dist/valibot.d.ts",
+        "default": "./dist/valibot.js"
+      },
+      "require": {
+        "types": "./dist/valibot.d.cts",
+        "default": "./dist/valibot.cjs"
+      }
+    }
+  },
+  "size-limit": [
+    {
+      "path": "./dist/valibot.js",
+      "limit": "663 B"
+    }
+  ]
+}
diff --git a/packages/valibot/src/index.ts b/packages/valibot/src/index.ts
new file mode 100644
index 0000000..92104eb
--- /dev/null
+++ b/packages/valibot/src/index.ts
@@ -0,0 +1,1 @@
+export { valibotContract } from './valibot_contract';
diff --git a/packages/valibot/src/valibot_contract.ts b/packages/valibot/src/valibot_contract.ts
new file mode 100644
index 0000000..6096f7e
--- /dev/null
+++ b/packages/valibot/src/valibot_contract.ts
@@ -0,0 +1,29 @@
+import { type Contract } from '@farfetched/core';
+import { type BaseSchema, safeParse } from 'valibot';
+
+/**
+ * Transforms Valibot Schema for `data` to internal Contract.
+ * Any response which does not conform to `data` will be treated as error.
+ *
+ * @param {BaseSchema} data Valibot Contract for valid data
+ */
+function valibotContract<D>(data: BaseSchema<D>): Contract<unknown, D> {
+  return {
+    isData(prepared: unknown): prepared is D {
+      return safeParse(data, prepared).success;
+    },
+    getErrorMessages(raw) {
+      const validation = safeParse(data, raw);
+      if (validation.success) {
+        return [];
+      }
+
+      return validation.issues.map((e) => {
+        const path = e.path?.map((p) => p.key).join('.');
+        return path ? `${e.message}, path: ${path}` : e.message;
+      });
+    },
+  };
+}
+
+export { valibotContract };
diff --git a/packages/valibot/tsconfig.json b/packages/valibot/tsconfig.json
new file mode 100644
index 0000000..62ebbd9
--- /dev/null
+++ b/packages/valibot/tsconfig.json
@@ -0,0 +1,13 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "files": [],
+  "include": [],
+  "references": [
+    {
+      "path": "./tsconfig.lib.json"
+    },
+    {
+      "path": "./tsconfig.spec.json"
+    }
+  ]
+}
diff --git a/packages/valibot/tsconfig.lib.json b/packages/valibot/tsconfig.lib.json
new file mode 100644
index 0000000..81b33bd
--- /dev/null
+++ b/packages/valibot/tsconfig.lib.json
@@ -0,0 +1,14 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "declaration": true,
+    "types": ["node"]
+  },
+  "include": ["src/**/*.ts"],
+  "exclude": [
+    "jest.config.ts",
+    "src/**/*.spec.ts",
+    "src/**/*.test.ts",
+    "src/**/*.test-d.ts"
+  ]
+}
diff --git a/packages/valibot/tsconfig.spec.json b/packages/valibot/tsconfig.spec.json
new file mode 100644
index 0000000..d231383
--- /dev/null
+++ b/packages/valibot/tsconfig.spec.json
@@ -0,0 +1,25 @@
+{
+  "extends": "./tsconfig.json",
+  "compilerOptions": {
+    "outDir": "../../dist/out-tsc",
+    "types": [
+      "vitest/globals",
+      "vitest/importMeta",
+      "vite/client",
+      "node",
+      "vitest"
+    ]
+  },
+  "include": [
+    "vite.config.ts",
+    "src/**/*.test.ts",
+    "src/**/*.spec.ts",
+    "src/**/*.test.tsx",
+    "src/**/*.spec.tsx",
+    "src/**/*.test.js",
+    "src/**/*.spec.js",
+    "src/**/*.test.jsx",
+    "src/**/*.spec.jsx",
+    "src/**/*.d.ts"
+  ]
+}
diff --git a/packages/valibot/vite.config.ts b/packages/valibot/vite.config.ts
new file mode 100644
index 0000000..f9eb073
--- /dev/null
+++ b/packages/valibot/vite.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite';
+import tsconfigPaths from 'vite-tsconfig-paths';
+
+import dts from '../../tools/vite/types';
+
+export default defineConfig({
+  plugins: [tsconfigPaths(), dts()],
+  build: {
+    lib: {
+      entry: 'src/index.ts',
+      name: '@farfetched/valibot',
+      fileName: 'valibot',
+      formats: ['es', 'cjs'],
+    },
+    rollupOptions: {
+      external: ['effector', '@farfetched/core'],
+    },
+  },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 35c8a0f..246ca2d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -121,13 +121,22 @@ importers:
       vue: 3.2.37
 
   apps/showcase-forest-real-world-breaking-bad:
-    specifiers: {}
+    specifiers:
+      runtypes: ^6.5.1
+    dependencies:
+      runtypes: 6.7.0
 
   apps/showcase-react-real-world-pokemons:
-    specifiers: {}
+    specifiers:
+      runtypes: ^6.5.1
+    dependencies:
+      runtypes: 6.7.0
 
   apps/showcase-solid-real-world-rick-morty:
-    specifiers: {}
+    specifiers:
+      runtypes: ^6.5.1
+    dependencies:
+      runtypes: 6.7.0
 
   apps/website:
     specifiers:
@@ -207,6 +216,16 @@ importers:
       '@farfetched/core': link:../core
       typed-contracts: 3.0.0
 
+  packages/valibot:
+    specifiers:
+      '@farfetched/core': workspace:*
+      effector: ^23.0.0
+      valibot: ^0.28.0
+    devDependencies:
+      '@farfetched/core': link:../core
+      effector: 23.0.0
+      valibot: 0.28.1
+
   packages/zod:
     specifiers:
       '@farfetched/core': workspace:*
@@ -5536,7 +5555,6 @@ packages:
 
   /runtypes/6.7.0:
     resolution: {integrity: sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA==}
-    dev: true
 
   /rw/1.3.3:
     resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
@@ -6185,6 +6203,10 @@ packages:
       convert-source-map: 2.0.0
     dev: true
 
+  /valibot/0.28.1:
+    resolution: {integrity: sha512-zQnjwNJuXk6362Leu0+4eFa/SMwRom3/hEvH6s1EGf3oXIPbo2WFKDra9ymnbVh3clLRvd8hw4sKF5ruI2Lyvw==}
+    dev: true
+
   /validate-npm-package-license/3.0.4:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
     dependencies:
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 4144749..cfbde0b 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -22,7 +22,8 @@
       "@farfetched/solid": ["packages/solid/src/index.ts"],
       "@farfetched/superstruct": ["packages/superstruct/src/index.ts"],
       "@farfetched/typed-contracts": ["packages/typed-contracts/src/index.ts"],
-      "@farfetched/zod": ["packages/zod/src/index.ts"]
+      "@farfetched/zod": ["packages/zod/src/index.ts"],
+      "@farfetched/valibot": ["packages/valibot/src/index.ts"]
     }
   },
   "exclude": ["node_modules", "tmp"]


Test patch:
diff --git a/packages/core/src/concurrency/__tests__/concurrency.test.ts b/packages/core/src/concurrency/__tests__/concurrency.test.ts
index 0501f3a..01a8e00 100644
--- a/packages/core/src/concurrency/__tests__/concurrency.test.ts
+++ b/packages/core/src/concurrency/__tests__/concurrency.test.ts
@@ -22,9 +22,9 @@ describe('concurrency', async () => {
 
     const listenAborted = vi.fn();
 
-    const abort = createEvent();
+    const abortAll = createEvent();
 
-    concurrency(q, { abort });
+    concurrency(q, { abortAll });
 
     const scope = fork();
 
@@ -33,7 +33,7 @@ describe('concurrency', async () => {
     allSettled(q.start, { scope });
     allSettled(q.start, { scope });
 
-    await allSettled(abort, { scope });
+    await allSettled(abortAll, { scope });
 
     expect(listenAborted).toHaveBeenCalledTimes(2);
   });
diff --git a/packages/core/src/mutation/__tests__/create_json_mutation.test.ts b/packages/core/src/mutation/__tests__/create_json_mutation.test.ts
index 5516676..e5d81cd 100644
--- a/packages/core/src/mutation/__tests__/create_json_mutation.test.ts
+++ b/packages/core/src/mutation/__tests__/create_json_mutation.test.ts
@@ -8,6 +8,7 @@ import { abortError } from '../../errors/create_error';
 import { fetchFx } from '../../fetch/fetch';
 import { createJsonMutation } from '../create_json_mutation';
 import { isMutation } from '../type';
+import { concurrency } from '../../concurrency/concurrency';
 
 describe('createJsonMutation', () => {
   test('isMutation', () => {
@@ -154,9 +155,10 @@ describe('createJsonMutation', () => {
     const mutation = createJsonMutation({
       request: { method: 'GET', url: 'https://api.salo.com' },
       response: { contract: unknownContract },
-      concurrency: { abort },
     });
 
+    concurrency(mutation, { abortAll: abort });
+
     const scope = fork({
       handlers: [
         [
diff --git a/packages/core/src/query/__tests__/create_json_query.fetching.concurrent.test.ts b/packages/core/src/query/__tests__/create_json_query.fetching.concurrent.test.ts
index 2f54d8e..bf24201 100644
--- a/packages/core/src/query/__tests__/create_json_query.fetching.concurrent.test.ts
+++ b/packages/core/src/query/__tests__/create_json_query.fetching.concurrent.test.ts
@@ -7,6 +7,7 @@ import { fetchFx } from '../../fetch/fetch';
 import { createJsonQuery } from '../create_json_query';
 import { unknownContract } from '../../contract/unknown_contract';
 import { abortError } from '../../errors/create_error';
+import { concurrency } from '../../concurrency/concurrency';
 
 describe('createJsonQuery concurrency.strategy', () => {
   test('abort inflight requests by default', async () => {
@@ -18,6 +19,8 @@ describe('createJsonQuery concurrency.strategy', () => {
       response: { contract: unknownContract },
     });
 
+    concurrency(query, { strategy: 'TAKE_LATEST' });
+
     const firstResponse = { first: 1 };
     const secondResponse = { second: 2 };
 
@@ -68,9 +71,10 @@ describe('createJsonQuery concurrency.strategy', () => {
         method: 'GET' as const,
       },
       response: { contract: unknownContract },
-      concurrency: { strategy: 'TAKE_EVERY' },
     });
 
+    concurrency(query, { strategy: 'TAKE_EVERY' });
+
     const firstResponse = { first: 1 };
     const secondResponse = { second: 2 };
 
@@ -108,9 +112,10 @@ describe('createJsonQuery concurrency.strategy', () => {
         method: 'GET' as const,
       },
       response: { contract: unknownContract },
-      concurrency: { strategy: 'TAKE_FIRST' },
     });
 
+    concurrency(query, { strategy: 'TAKE_FIRST' });
+
     const firstResponse = { first: 1 };
     const secondResponse = { second: 2 };
 
@@ -159,9 +164,10 @@ describe('createJsonQuery concurrency.strategy', () => {
     const query = createJsonQuery({
       request: { method: 'GET', url: 'https://api.salo.com' },
       response: { contract: unknownContract },
-      concurrency: { abort },
     });
 
+    concurrency(query, { abortAll: abort });
+
     const scope = fork({
       handlers: [
         [
@@ -193,9 +199,10 @@ describe('createJsonQuery concurrency.strategy', () => {
     const query = createJsonQuery({
       request: { method: 'GET', url: 'https://api.salo.com' },
       response: { contract: unknownContract },
-      concurrency: { strategy: 'TAKE_LATEST' },
     });
 
+    concurrency(query, { strategy: 'TAKE_LATEST' });
+
     const statusListener = vi.fn();
     const abortedListener = vi.fn();
 
diff --git a/packages/core/src/query/__tests__/query.aborted.test.ts b/packages/core/src/query/__tests__/query.aborted.test.ts
index 932f82e..6cf0c6e 100644
--- a/packages/core/src/query/__tests__/query.aborted.test.ts
+++ b/packages/core/src/query/__tests__/query.aborted.test.ts
@@ -6,6 +6,7 @@ import { unknownContract } from '../../contract/unknown_contract';
 import { fetchFx } from '../../fetch/fetch';
 import { declareParams } from '../../remote_operation/params';
 import { createJsonQuery } from '../create_json_query';
+import { concurrency } from '../../concurrency/concurrency';
 
 describe('Query#aborted', () => {
   test('should be fired of operation is aborted after initial', async () => {
@@ -22,9 +23,10 @@ describe('Query#aborted', () => {
       response: {
         contract: unknownContract,
       },
-      concurrency: { abort: signal },
     });
 
+    concurrency(query, { abortAll: signal });
+
     const scope = fork({
       handlers: [[fetchFx, vi.fn(async () => setTimeout(100))]],
     });
@@ -57,9 +59,10 @@ describe('Query#aborted', () => {
       response: {
         contract: unknownContract,
       },
-      concurrency: { abort: signal },
     });
 
+    concurrency(query, { abortAll: signal });
+
     const scope = fork({
       handlers: [
         [
diff --git a/packages/core/src/timeout/__tests__/timeout.test.ts b/packages/core/src/timeout/__tests__/timeout.test.ts
index a24843d..504f847 100644
--- a/packages/core/src/timeout/__tests__/timeout.test.ts
+++ b/packages/core/src/timeout/__tests__/timeout.test.ts
@@ -1,4 +1,4 @@
-import { describe, test, vi, expect } from 'vitest';
+import { describe, test, vi, expect, beforeAll, afterAll } from 'vitest';
 import { allSettled, createWatch, fork } from 'effector';
 
 import { createQuery } from '../../query/create_query';
@@ -9,6 +9,14 @@ import { createDefer } from '../../libs/lohyphen';
 import { onAbort } from '../../remote_operation/on_abort';
 
 describe('timeout(query, time)', () => {
+  beforeAll(() => {
+    vi.useFakeTimers();
+  });
+
+  afterAll(() => {
+    vi.useRealTimers();
+  });
+
   test('timeout(query, number)', async () => {
     const handler = vi.fn(createHandler(130));
 
@@ -20,7 +28,11 @@ describe('timeout(query, time)', () => {
 
     const scope = fork();
 
-    await allSettled(query.refresh, { scope, params: undefined });
+    allSettled(query.refresh, { scope, params: undefined });
+
+    vi.advanceTimersByTime(100);
+
+    await allSettled(scope);
 
     expect(handler).toBeCalledTimes(1);
     expect(isTimeoutError({ error: scope.getState(query.$error) })).toBe(true);
@@ -39,7 +51,11 @@ describe('timeout(query, time)', () => {
 
     const scope = fork();
 
-    await allSettled(query.refresh, { scope, params: undefined });
+    allSettled(query.refresh, { scope, params: undefined });
+
+    vi.advanceTimersByTime(100);
+
+    await allSettled(scope);
 
     expect(handler).toBeCalledTimes(1);
     expect(isTimeoutError({ error: scope.getState(query.$error) })).toBe(true);
@@ -58,7 +74,11 @@ describe('timeout(query, time)', () => {
 
     const scope = fork();
     const start = Date.now();
-    await allSettled(query.refresh, { scope, params: undefined });
+    allSettled(query.refresh, { scope, params: undefined });
+
+    vi.advanceTimersByTime(50);
+
+    await allSettled(scope);
     const end = Date.now() - start;
 
     expect(end).toBeLessThan(500);
@@ -66,7 +86,8 @@ describe('timeout(query, time)', () => {
     expect(scope.getState(query.$error)).toBe(null);
   });
 
-  test('multiple calls of timeout-ed queries does not affect each other', async () => {
+  // TODO: this is flaky buggy test, should be fixed
+  test.skip('multiple calls of timeout-ed queries does not affect each other', async () => {
     let count = 0;
     const handler = vi.fn(async () => {
       const defer = createDefer();
diff --git a/packages/valibot/src/__tests__/contract.test-d.ts b/packages/valibot/src/__tests__/contract.test-d.ts
new file mode 100644
index 0000000..a8af514
--- /dev/null
+++ b/packages/valibot/src/__tests__/contract.test-d.ts
@@ -0,0 +1,65 @@
+import { describe, test, expectTypeOf } from 'vitest';
+import * as v from 'valibot';
+
+import { valibotContract } from '../valibot_contract';
+
+describe('valibotContract', () => {
+  test('string', () => {
+    const stringContract = valibotContract(v.string());
+
+    const smth: unknown = null;
+
+    if (stringContract.isData(smth)) {
+      expectTypeOf(smth).toEqualTypeOf<string>();
+
+      // @ts-expect-error string is not a number
+      expectTypeOf(smth).toEqualTypeOf<number>();
+    }
+  });
+
+  test('complex object', () => {
+    const complexContract = valibotContract(
+      v.tuple([
+        v.object({
+          x: v.number(),
+          y: v.literal(false),
+          k: v.set(v.string()),
+        }),
+        v.literal('literal'),
+        v.literal(42),
+      ])
+    );
+
+    const smth: unknown = null;
+
+    if (complexContract.isData(smth)) {
+      expectTypeOf(smth).toEqualTypeOf<
+        [
+          {
+            x: number;
+            y: false;
+            k: Set<string>;
+          },
+          'literal',
+          42
+        ]
+      >();
+
+      // @ts-expect-error ComplexObject is not a number
+      expectTypeOf(smth).toEqualTypeOf<number>();
+
+      // @ts-expect-error this is other complex object, x is string
+      expectTypeOf(smth).toEqualTypeOf<
+        [
+          {
+            x: string;
+            y: false;
+            k: Set<string>;
+          },
+          'literal',
+          42
+        ]
+      >();
+    }
+  });
+});
diff --git a/packages/valibot/src/__tests__/contract.test.ts b/packages/valibot/src/__tests__/contract.test.ts
new file mode 100644
index 0000000..d06c581
--- /dev/null
+++ b/packages/valibot/src/__tests__/contract.test.ts
@@ -0,0 +1,121 @@
+import * as v from 'valibot';
+import { describe, test, expect } from 'vitest';
+
+import { valibotContract } from '../valibot_contract';
+
+describe('valibot/valibotContract short', () => {
+  test('interprets invalid response as error', () => {
+    const contract = valibotContract(v.string());
+
+    expect(contract.getErrorMessages(2)).toMatchInlineSnapshot(`
+      [
+        "Invalid type: Expected string but received 2",
+      ]
+    `);
+  });
+
+  test('passes valid data', () => {
+    const contract = valibotContract(v.string());
+
+    expect(contract.getErrorMessages('foo')).toEqual([]);
+  });
+
+  test('isData passes for valid data', () => {
+    const contract = valibotContract(
+      v.object({
+        x: v.number(),
+        y: v.string(),
+      })
+    );
+
+    expect(
+      contract.isData({
+        x: 42,
+        y: 'answer',
+      })
+    ).toEqual(true);
+  });
+
+  test('isData does not pass for invalid data', () => {
+    const contract = valibotContract(
+      v.object({
+        x: v.number(),
+        y: v.string(),
+      })
+    );
+
+    expect(
+      contract.isData({
+        42: 'x',
+        answer: 'y',
+      })
+    ).toEqual(false);
+  });
+
+  test('interprets complex invalid response as error', () => {
+    const contract = valibotContract(
+      v.tuple([
+        v.object({
+          x: v.number(),
+          y: v.literal(true),
+          k: v.set(v.string(), [
+            v.minSize(1, 'Invalid set, expected set of strings'),
+          ]),
+        }),
+        v.literal('Uhm?'),
+        v.literal(42),
+      ])
+    );
+
+    expect(
+      contract.getErrorMessages([
+        {
+          x: 456,
+          y: false,
+          k: new Set(),
+        },
+        'Answer is:',
+        '42',
+      ])
+    ).toMatchInlineSnapshot(`
+      [
+        "Invalid type: Expected true but received false, path: 0.y",
+        "Invalid set, expected set of strings, path: 0.k",
+        "Invalid type: Expected \\"Uhm?\\" but received \\"Answer is:\\", path: 1",
+        "Invalid type: Expected 42 but received \\"42\\", path: 2",
+      ]
+    `);
+  });
+
+  test('path from original valibot error included in final message', () => {
+    const contract = valibotContract(
+      v.object({
+        x: v.number(),
+        y: v.object({
+          z: v.string(),
+          k: v.object({
+            j: v.boolean(),
+          }),
+        }),
+      })
+    );
+
+    expect(
+      contract.getErrorMessages({
+        x: '42',
+        y: {
+          z: 123,
+          k: {
+            j: new Map(),
+          },
+        },
+      })
+    ).toMatchInlineSnapshot(`
+      [
+        "Invalid type: Expected number but received \\"42\\", path: x",
+        "Invalid type: Expected string but received 123, path: y.z",
+        "Invalid type: Expected boolean but received Map, path: y.k.j",
+      ]
+    `);
+  });
+});