Repository: igorkamyshev/farfetched
Pull request #312 (2023-04-18 11:08:44)

Next 0.9
Check before release:
- [x] migration guides
- [x] updated docs
- [x] revise roadmap


Patch:
diff --git a/.changeset/bright-laws-relax.md b/.changeset/bright-laws-relax.md
new file mode 100644
index 0000000..d9951bf
--- /dev/null
+++ b/.changeset/bright-laws-relax.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Allow passing array of codes to `isHttpErrorCode`
diff --git a/.changeset/cuddly-walls-rule.md b/.changeset/cuddly-walls-rule.md
new file mode 100644
index 0000000..b7a781b
--- /dev/null
+++ b/.changeset/cuddly-walls-rule.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Add _Event_ `.started` to _Query_ and _Mutation_
diff --git a/.changeset/eighty-numbers-promise.md b/.changeset/eighty-numbers-promise.md
new file mode 100644
index 0000000..dbee48a
--- /dev/null
+++ b/.changeset/eighty-numbers-promise.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Support custom serialization in `localStorageCache` and `sessionStorageCache`
diff --git a/.changeset/hot-carrots-whisper.md b/.changeset/hot-carrots-whisper.md
new file mode 100644
index 0000000..5d95366
--- /dev/null
+++ b/.changeset/hot-carrots-whisper.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Add _Event_ `aborted` to _Query_
diff --git a/.changeset/poor-ligers-kick.md b/.changeset/poor-ligers-kick.md
new file mode 100644
index 0000000..3865824
--- /dev/null
+++ b/.changeset/poor-ligers-kick.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': patch
+---
+
+Rework internal structure of sourced fields to fix race-condition in `attachOperation`
diff --git a/.changeset/shy-avocados-love.md b/.changeset/shy-avocados-love.md
new file mode 100644
index 0000000..c60484d
--- /dev/null
+++ b/.changeset/shy-avocados-love.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Mark read-only _Events_ and _Stores_ with `readonly`
diff --git a/.changeset/six-peas-grab.md b/.changeset/six-peas-grab.md
new file mode 100644
index 0000000..07764a8
--- /dev/null
+++ b/.changeset/six-peas-grab.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Add _Store_ `.$finished` to _Remote Operation_
diff --git a/.changeset/spicy-kiwis-wonder.md b/.changeset/spicy-kiwis-wonder.md
new file mode 100644
index 0000000..28b0511
--- /dev/null
+++ b/.changeset/spicy-kiwis-wonder.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Delete `externalCache`
diff --git a/.changeset/spotty-poems-play.md b/.changeset/spotty-poems-play.md
new file mode 100644
index 0000000..00acf86
--- /dev/null
+++ b/.changeset/spotty-poems-play.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Add option `supressIntermediateErrors` to `retry` operator
diff --git a/.changeset/twelve-days-move.md b/.changeset/twelve-days-move.md
new file mode 100644
index 0000000..0622f24
--- /dev/null
+++ b/.changeset/twelve-days-move.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': minor
+---
+
+Add info about _Query_ status and corresponding data to `.finished.finally` _Event_
diff --git a/.changeset/wicked-baboons-burn.md b/.changeset/wicked-baboons-burn.md
new file mode 100644
index 0000000..523d822
--- /dev/null
+++ b/.changeset/wicked-baboons-burn.md
@@ -0,0 +1,5 @@
+---
+'@farfetched/core': patch
+---
+
+Add `ExecutionMeta` to `otherwise` _Event_ in `retry` operator
diff --git a/apps/website/docs/.vitepress/config.js b/apps/website/docs/.vitepress/config.js
index a78a5b6..d3090f2 100644
--- a/apps/website/docs/.vitepress/config.js
+++ b/apps/website/docs/.vitepress/config.js
@@ -364,6 +364,7 @@ export default withMermaid(
           {
             text: 'Releases',
             items: [
+              { text: 'v0.9', link: '/releases/0-9' },
               { text: 'v0.8 Saphan Hin', link: '/releases/0-8' },
               { text: 'v0.7 Nam Phu Chet Si', link: '/releases/0-7' },
               { text: 'v0.6 Huai Nam Dang', link: '/releases/0-6' },
diff --git a/apps/website/docs/api/operators/cache.md b/apps/website/docs/api/operators/cache.md
index 34a8ba7..d0cbce6 100644
--- a/apps/website/docs/api/operators/cache.md
+++ b/apps/website/docs/api/operators/cache.md
@@ -1,3 +1,7 @@
+---
+outline: [2, 3]
+---
+
 # `cache` <Badge type="tip" text="since v0.3.0" />
 
 Saves result of the [_Query_](/api/primitives/query) to some storage and allows to restore it back.
@@ -103,3 +107,28 @@ function myCustomAdapter({
 ```
 
 However, we still need to trigger `itemEvicted` and `itemExpired` events in our adapter.
+
+### Custom serialization <Badge type="tip" text="since v0.9.0" />
+
+Adapters that use `localStorage` and `sessionStorage` as a storage for cached results use `JSON.stringify` and `JSON.parse` to serialize and deserialize data. If you need to use custom serialization, you can use `serialize` field in the adapter config:
+
+```ts
+import { cache, localStorageCache } from '@farfetched/core';
+
+cache(query, {
+  adapter: localStorageCache({
+    serialize: {
+      read: (data) => {
+        // Do your custom deserialization here
+
+        return parsedData;
+      },
+      write: (data) => {
+        // Do your custom serialization here
+
+        return serializedData;
+      },
+    },
+  }),
+});
+```
diff --git a/apps/website/docs/api/operators/retry.md b/apps/website/docs/api/operators/retry.md
index 2b9243b..7936f62 100644
--- a/apps/website/docs/api/operators/retry.md
+++ b/apps/website/docs/api/operators/retry.md
@@ -17,6 +17,7 @@ Config fields:
   - `(params, { attempt }) => mapped`
   - `{ source: Store, fn: (params, { attempt }, source) => mapped }`
 - `otherwise?`: [_Event_](https://effector.dev/docs/api/effector/event) or [_Effect_](https://effector.dev/docs/api/effector/effect), that will be called after the last attempt if the [_Query_](/api/primitives/query) is still failed
+- `supressIntermediateErrors?`: <Badge type="tip" text="since v0.9.0" /> _boolean_ whether to suppress intermediate errors or not, defaults to `false`. If `true`, then the [_Query_](/api/primitives/query) will not be marked as failed until the last attempt is failed.
 
 ## Build-in delays
 
diff --git a/apps/website/docs/api/primitives/mutation.md b/apps/website/docs/api/primitives/mutation.md
index f22d93d..96c02d6 100644
--- a/apps/website/docs/api/primitives/mutation.md
+++ b/apps/website/docs/api/primitives/mutation.md
@@ -1,30 +1,79 @@
+---
+outline: [2, 3]
+---
+
 # Mutation <Badge type="tip" text="since v0.2.0" />
 
 Representation of a mutation of remote data.
 
-## API reference
+## Commands
+
+This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to perform actions on the _Mutation_. Commands should be called in application code.
+
+### `start`
+
+Unconditionally starts the _Mutation_ with the given parameters.
+
+## Stores
+
+This section describes the [_Stores_](https://effector.dev/docs/api/effector/store) that can be used to read the _Mutation_ state.
+
+### `$status`
+
+[_Store_](https://effector.dev/docs/api/effector/store) with the current status of the _Mutation_. It must not be changed directly. Can be one of the following values: `"initial"`, `"pending"`, `"done"`, `"fail"`.
+
+For convenience, there are also the following [_Stores_](https://effector.dev/docs/api/effector/store):
+
+- `$idle` <Badge type="tip" text="since v0.8.0" /> — `true` if the _Mutation_ is in the `"initial"` state, `false` otherwise.
+- `$pending` — `true` if the _Mutation_ is in the `"pending"` state, `false` otherwise.
+- `$failed` — `true` if the _Mutation_ is in the `"fail"` state, `false` otherwise.
+- `$succeeded` — `true` if the _Mutation_ is in the `"done"` state, `false` otherwise.
+- `$finished` <Badge type="tip" text="since v0.9.0" /> — `true` if the _Mutation_ is in the `"done"` or `"fail"` state, `false` otherwise.
+
+### `$enabled`
+
+[_Store_](https://effector.dev/docs/api/effector/store) with the current enabled state of the _Mutation_. Disabled _Mutations_ will not be executed, instead, they will be treated as skipped. It must not be changed directly. Can be `true` or `false`.
+
+## Events
+
+This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to listen to the _Mutation_ state changes. Events must not be called in application code.
+
+### `finished.success`
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is finished with success. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Mutation_
+- `result` with the result of the _Mutation_
+- `meta` with the execution metadata
+
+### `finished.failure`
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is finished with failure. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Mutation_
+- `error` with the error of the _Mutation_
+- `meta` with the execution metadata
+
+### `finished.skip`
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is skipped. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Mutation_
+- `meta` with the execution metadata
 
-```ts
-const mutation: Mutation<Params, Data, Error>;
+### `finished.finally`
 
-// Stores
-mutation.$status; // Store<'initial' | 'pending' | 'done' | 'fail'>
-mutation.$idle; // Store<boolean>, since v0.8.0
-mutation.$pending; // Store<boolean>
-mutation.$failed; // Store<boolean>
-mutation.$succeeded; // Store<boolean>
-mutation.$enabled; // Store<boolean>
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is finished with success, failure or skip. Payload will contain the object with the following fields:
 
-// Commands
-mutation.start; // Event<Params>;
+- `params` with the parameters that were used to start the _Mutation_
+- `meta` with the execution metadata
+- `status` <Badge type="tip" text="since v0.9.0" /> with a string `"done"`, `"fail"` or `"skip"` for success, failure or skip respectively
+- `result` <Badge type="tip" text="since v0.9.0" /> if the `status` is `"done"` with the result of the _Mutation_
+- `error` <Badge type="tip" text="since v0.9.0" /> if the `status` is `"fail"` with the error of the _Mutation_
 
-// Events
-mutation.finished.success; // Event<Data>;
-mutation.finished.failure; // Event<Error>;
-mutation.finished.skip; // Event<void>;
-mutation.finished.finally; // Event<void>;
+## `started` <Badge type="tip" text="since v0.9.0" />
 
-// Note: Store and Event are imported from 'effector' package
-```
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Mutation_ is started. Payload will contain the object with the following fields:
 
-More information about API can be found in [the source code](https://github.com/igorkamyshev/farfetched/blob/master/packages/core/src/mutation/type.ts).
+- `params` with the parameters that were used to start the _Mutation_
+- `meta` with the execution metadata
diff --git a/apps/website/docs/api/primitives/query.md b/apps/website/docs/api/primitives/query.md
index 6a1559a..a828c7c 100644
--- a/apps/website/docs/api/primitives/query.md
+++ b/apps/website/docs/api/primitives/query.md
@@ -1,36 +1,110 @@
+---
+outline: [2, 3]
+---
+
 # Query
 
 Representation of a piece of remote data.
 
-## API reference
-
-```ts
-const query: Query<Params, Data, Error>;
-const query: Query<Params, Data, Error, InitialData>; // InitialData is allowed since v0.3.0
-
-// Stores
-query.$data; // Store<Data | InitialData>
-query.$error; // Store<Error | null>
-query.$status; // Store<'initial' | 'pending' | 'done' | 'fail'>
-query.$idle; // Store<boolean>, since v0.8.0
-query.$pending; // Store<boolean>
-query.$failed; // Store<boolean>, since v0.2.0
-query.$succeeded; // Store<boolean>, since v0.2.0
-query.$enabled; // Store<boolean>
-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 }>
-query.finished.failure; // Event<{ error: Error, params: Params }>
-query.finished.skip; // Event<{ params: Params }>
-query.finished.finally; // Event<{ params: Params }>
-
-// Note: Store and Event are imported from 'effector' package
-```
-
-More information about API can be found in [the source code](https://github.com/igorkamyshev/farfetched/blob/master/packages/core/src/query/type.ts).
+## Commands
+
+This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to perform actions on the _Query_. Commands should be called in application code.
+
+### `start`
+
+Unconditionally starts the _Query_ with the given parameters.
+
+### `refresh` <Badge type="tip" text="since v0.8.0" />
+
+Starts the _Query_ with the given parameters if it is `$stale`. Otherwise, it will be treated as skipped.
+
+### `reset` <Badge type="tip" text="since v0.2.0" />
+
+Resets the _Query_ to the initial state.
+
+## Stores
+
+This section describes the [_Stores_](https://effector.dev/docs/api/effector/store) that can be used to read the _Query_ state.
+
+### `$data`
+
+[_Store_](https://effector.dev/docs/api/effector/store) with the latest data. It must not be changed directly. In case of error, it will contain the initial data.
+
+### `$error`
+
+[_Store_](https://effector.dev/docs/api/effector/store) with the latest error. It must not be changed directly. In case of success, it will contain `null`.
+
+### `$status`
+
+[_Store_](https://effector.dev/docs/api/effector/store) with the current status of the _Query_. It must not be changed directly. Can be one of the following values: `"initial"`, `"pending"`, `"done"`, `"fail"`.
+
+For convenience, there are also the following [_Stores_](https://effector.dev/docs/api/effector/store):
+
+- `$idle` <Badge type="tip" text="since v0.8.0" /> — `true` if the _Query_ is in the `"initial"` state, `false` otherwise.
+- `$pending` — `true` if the _Query_ is in the `"pending"` state, `false` otherwise.
+- `$failed` <Badge type="tip" text="since v0.2.0" /> — `true` if the _Query_ is in the `"fail"` state, `false` otherwise.
+- `$succeeded` <Badge type="tip" text="since v0.2.0" /> — `true` if the _Query_ is in the `"done"` state, `false` otherwise.
+- `$finished` <Badge type="tip" text="since v0.9.0" /> — `true` if the _Query_ is in the `"done"` or `"fail"` state, `false` otherwise.
+
+### `$enabled`
+
+[_Store_](https://effector.dev/docs/api/effector/store) with the current enabled state of the _Query_. Disabled queries will not be executed, instead, they will be treated as skipped. It must not be changed directly. Can be `true` or `false`.
+
+### `$stale`
+
+[_Store_](https://effector.dev/docs/api/effector/store) with the current stale state of the _Query_. Stale queries will be executed on the next call to `refresh` [_Event_](https://effector.dev/docs/api/effector/event). It must not be changed directly. Can be `true` or `false`.
+
+## Events
+
+This section describes the [_Event_](https://effector.dev/docs/api/effector/event) that can be used to listen to the _Query_ state changes. Events must not be called in application code.
+
+### `finished.success`
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is finished with success. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Query_
+- `result` with the result of the _Query_
+- `meta` with the execution metadata
+
+### `finished.failure`
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is finished with failure. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Query_
+- `error` with the error of the _Query_
+- `meta` with the execution metadata
+
+### `finished.skip`
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is skipped. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Query_
+- `meta` with the execution metadata
+
+### `finished.finally`
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is finished with success, failure or skip. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Query_
+- `meta` with the execution metadata
+- `status` <Badge type="tip" text="since v0.9.0" /> with a string `"done"`, `"fail"` or `"skip"` for success, failure or skip respectively
+- `result` <Badge type="tip" text="since v0.9.0" /> if the `status` is `"done"` with the result of the _Query_
+- `error` <Badge type="tip" text="since v0.9.0" /> if the `status` is `"fail"` with the error of the _Query_
+
+## `aborted` <Badge type="tip" text="since v0.9.0" />
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is aborted. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Query_
+- `meta` with the execution metadata
+
+::: tip
+Now, aborted _Queries_ are treated as failed. This means that `.finished.failure` will be triggered as well as `.aborted`. In v0.10 we will change this behavior and `.aborted` will not trigger `.finished.failure` and _Query_ will not be moved to the `"fail"` state.
+:::
+
+## `started` <Badge type="tip" text="since v0.9.0" />
+
+[_Event_](https://effector.dev/docs/api/effector/event) that will be triggered when the _Query_ is started. Payload will contain the object with the following fields:
+
+- `params` with the parameters that were used to start the _Query_
+- `meta` with the execution metadata
diff --git a/apps/website/docs/api/utils/error_guards.md b/apps/website/docs/api/utils/error_guards.md
index 5bcd028..9833af1 100644
--- a/apps/website/docs/api/utils/error_guards.md
+++ b/apps/website/docs/api/utils/error_guards.md
@@ -71,7 +71,11 @@ const httpError = sample({
 
 ### `isHttpErrorCode`
 
-This function is a more specific version of `isHttpError`. It takes a number as an argument and returns a function that checks if the error is a `HttpError` with the given status code.
+This function is a more specific version of `isHttpError`.
+
+#### `isHttpErrorCode(statusCode: number)`
+
+It takes a number as an argument and returns a function that checks if the error is a `HttpError` with the given status code.
 
 ```ts
 import { isHttpErrorCode } from '@farfetched/core';
@@ -82,6 +86,19 @@ const notFound = sample({
 });
 ```
 
+#### `isHttpErrorCode(statusCodes: number[])` <Badge type="tip" text="since v0.9.0" />
+
+It takes an array of numbers as an argument and returns a function that checks if the error is a `HttpError` with one of the given status codes.
+
+```ts
+import { isHttpErrorCode } from '@farfetched/core';
+
+const notFoundOrForbidden = sample({
+  clock: query.finished.failure,
+  filter: isHttpErrorCode([404, 403]),
+});
+```
+
 ## `isNetworkError`
 
 `NetworkError` is thrown when the query fails because of network problems.
diff --git a/apps/website/docs/releases/0-9.md b/apps/website/docs/releases/0-9.md
new file mode 100644
index 0000000..a982660
--- /dev/null
+++ b/apps/website/docs/releases/0-9.md
@@ -0,0 +1,59 @@
+---
+description: Nothing new, just a cleanup
+outline: [2, 3]
+---
+
+# v0.9 Cleanup
+
+This release is technical. It includes only deletions of deprecated features and general refactoring of the API. This release intentionally does not include any new features, so you can upgrade your codebase without any pressure.
+
+## Migration guide
+
+There are a bunch of breaking changes, but they are all minor and should not affect your code much. Furthermore, they are the [soundness as promised](/statements/releases). Please read the migration guide carefully before upgrading.
+
+::: tip Q: Why breaking changes is necessary now?
+
+A: The best APIs are created on top of real cases, so we've released [v0.1 Samet Nangshe](/releases/0-1) almost a year ago to get feedback from the community and collect real use cases. We've got a lot of feedback, and now we are ready to make the API more consistent and predictable.
+
+:::
+
+### Do not use `externalCache` adapter
+
+`externalCache` adapter was deprecated in [0.8](/releases/0-8), write your own adapter instead [by recipe](/recipes/server_cache).
+
+### Read-only [_Stores_](https://effector.dev/docs/api/effector/store) and [_Events_](https://effector.dev/docs/api/effector/event)
+
+[_Events_](https://effector.dev/docs/api/effector/event) `finished.*` have never been supposed to be called in application code. Now they are read-only. In case you call them, you will get a warning in console in Effector 22 and exception in Effector 23.
+
+[_Stores_](https://effector.dev/docs/api/effector/store) `$data`, `$error`, `$status`, `$idle`, `$pending`, `$succeeded`, `$failed`, `$enabled` have never been supposed to be changed in application code directly. Now they are read-only. In case you change them, you will get a warning in console in Effector 22 and exception in Effector 23.
+
+### Separate [_Event_](https://effector.dev/docs/api/effector/event) for [_Query_](/api/primitives/query) cancelation
+
+Cancelled [_Queries_](/api/primitives/query) were treated as failed before this release. We have added a separate [_Event_](https://effector.dev/docs/api/effector/event) `.aborted` to [_Query_](/api/primitives/query) to distinguish between cancelation and failure. It is recommended to use `.aborted` instead of `.finished.failure` to handle cancelation.
+
+:::warning
+In the next release v0.10 cancelation will not be treated as failure anymore, so you will have to handle it explicitly.
+:::
+
+### `supressIntermediateErrors` in `retry` operator
+
+Before this release, [`retry`](/api/operators/retry) operator was marking [_Query_](/api/primitives/query) as failed on every failed attempt. Since v0.9 it accepts options `supressIntermediateErrors` to overwrite this behavior. If `true`, then the [_Query_](/api/primitives/query) will not be marked as failed until the last attempt is failed.
+
+:::warning
+
+In the next release v0.10 `supressIntermediateErrors` will be true `true` by default. To restore the previous behavior, you will have to set it to `false` explicitly.
+
+:::
+
+### Re-worked [_Sourced_](/api/primitives/sourced) internal implementation
+
+Before [_Sourced_](/api/primitives/sourced) fields internally were represented as [_Stores_](https://effector.dev/docs/api/effector/store) with a final value of a field, which value changes fast right before read. This scheme was based on [Effector's computation priority](https://effector.dev/docs/explanation/computation-priority) and served well for a long time. But it had a few drawbacks:
+
+- It could be easily broken by minor changes in Effector itself. And we plan such changes in the future major releases of Effector (e.g. [experimental phasing](https://github.com/effector/effector/pull/958), [batching improvements](https://github.com/effector/effector/pull/922), [`combine` improvements](https://github.com/effector/effector/pull/916), etc.);
+- It led to race-conditions in some cases (e.g. [simultaneously started _Query_ created by `attachOperation`](https://github.com/igorkamyshev/farfetched/issues/327)).
+
+After v0.9 [_Sourced_](/api/primitives/sourced) fields internally is represented as [_Stores_](https://effector.dev/docs/api/effector/store) with a function return final value of a field. It is more stable and less error-prone.
+
+If you use `normalizeSourced` or `combineSourced` in your code, you may need to update it to the new API. Examples can be found in [the PR](https://github.com/igorkamyshev/farfetched/pull/354). Since `normalizeSourced` and `combineSourced` are low-level undocumented functions, we do not provide a migration guide for them.
+
+<!--@include: ./0-9.changelog.md-->
diff --git a/apps/website/docs/roadmap.md b/apps/website/docs/roadmap.md
index 81e59f7..4279d20 100644
--- a/apps/website/docs/roadmap.md
+++ b/apps/website/docs/roadmap.md
@@ -4,17 +4,18 @@ Farfetched is under development now, before the first stable release we are goin
 
 ## The next release
 
-### v.0.9
+### v.0.10
 
 - DevTools interface
+- Barrier API
 
 ## Future releases
 
-### v.0.10
+### v.0.11
 
 - Pagination and infinite scroll
 
-### v.0.11
+### v.0.12
 
 - GraphQL add-on — `@farfetched/graphql`
   - `createGraphQLQuery`
@@ -23,13 +24,13 @@ Farfetched is under development now, before the first stable release we are goin
   - import, parsing, and usage of schema
   - automatic optimistic update for Query based on Mutation and schema
 
-### v.0.12
+### v.0.13
 
 - WebSocket
 - Server Sent Events
 - HTTP Polling
 
-### v.0.13
+### v.0.14
 
 - Add-on `@farfetched/rest`
   - `createREST` — return set of Queries and Mutations for typical REST API
diff --git a/apps/website/docs/tutorial/retries.md b/apps/website/docs/tutorial/retries.md
index 9e5c19b..8ac1c1f 100644
--- a/apps/website/docs/tutorial/retries.md
+++ b/apps/website/docs/tutorial/retries.md
@@ -145,6 +145,22 @@ retry(characterQuery, {
 
 `mapParams` accepts a function that takes current parameters, occurred error and attempt number and returns new parameters. In this example, we just add `attempt` parameter to the current parameters.
 
+## Intermediate errors
+
+By default, `retry` does not supress errors, so if the operation failed, it will fail as well. But sometimes, we want to suppress errors and check for status only after all retries are failed. To do this, we can use the `supressIntermediateErrors` option of the `retry` operator.
+
+```ts
+retry(characterQuery, {
+  times: 5,
+  delay: 500,
+  supressIntermediateErrors: true,
+});
+```
+
+::: tip
+In the next release v0.10 `supressIntermediateErrors` will be true `true` by default. To restore the previous behavior, you will have to set it to `false` explicitly. If you want to be ready for this change, you can set it to `false` already.
+:::
+
 ## API reference
 
 You can find the full API reference for the `retry` operator in the [API reference](/api/operators/retry).
diff --git a/packages/core/index.ts b/packages/core/index.ts
index df573e1..a02ff64 100644
--- a/packages/core/index.ts
+++ b/packages/core/index.ts
@@ -31,7 +31,6 @@ export {
   type CacheAdapter,
   type CacheAdapterOptions,
 } from './src/cache/adapters/type';
-export { externalCache } from './src/cache/adapters/external';
 export { inMemoryCache } from './src/cache/adapters/in_memory';
 export { localStorageCache } from './src/cache/adapters/local_storage';
 export { sessionStorageCache } from './src/cache/adapters/session_storage';
diff --git a/packages/core/project.json b/packages/core/project.json
index a5b405a..8b8525f 100644
--- a/packages/core/project.json
+++ b/packages/core/project.json
@@ -71,7 +71,7 @@
     "size": {
       "executor": "./tools/executors/size-limit:size-limit",
       "options": {
-        "limit": "19 kB",
+        "limit": "20 kB",
         "outputPath": "dist/packages/core"
       },
       "dependsOn": [
diff --git a/packages/core/src/cache/adapters/browser_storage.ts b/packages/core/src/cache/adapters/browser_storage.ts
index faba464..a0942b4 100644
--- a/packages/core/src/cache/adapters/browser_storage.ts
+++ b/packages/core/src/cache/adapters/browser_storage.ts
@@ -1,20 +1,35 @@
-import { createEffect, createEvent, createStore, sample } from 'effector';
+import {
+  createEffect,
+  createEvent,
+  createStore,
+  type Json,
+  sample,
+} from 'effector';
 
 import { delay } from '../../libs/patronus';
 import { parseTime } from '../../libs/date-nfs';
 import { createCacheAdapter } from './instance';
 import { attachObservability } from './observability';
-import { CacheAdapter, CacheAdapterOptions } from './type';
+import { type CacheAdapter, type CacheAdapterOptions } from './type';
 import { get } from '../../libs/lohyphen';
 
 export const META_KEY = '__farfetched_meta__';
 
+export type SerializeConfig = {
+  serialize?: {
+    read: (value: Json) => unknown;
+    write: (value: unknown) => Json;
+  };
+};
+
 export function browserStorageCache(
   config: {
     storage: () => Storage;
-  } & CacheAdapterOptions
+  } & CacheAdapterOptions &
+    SerializeConfig
 ): CacheAdapter {
-  const { storage, observability, maxAge, maxEntries } = config;
+  const { storage, observability, maxAge, maxEntries, serialize } = config;
+
   // -- adapter
   function storageCache(): CacheAdapter {
     const getSavedItemFx = createEffect(async (key: string) => {
@@ -25,7 +40,10 @@ export function browserStorageCache(
       try {
         const parsed = JSON.parse(item);
 
-        return parsed as SavedItem;
+        return {
+          ...parsed,
+          value: serialize?.read ? serialize.read(parsed.value) : parsed.value,
+        } as SavedItem;
       } catch {
         return null;
       }
@@ -33,7 +51,10 @@ export function browserStorageCache(
 
     const setSavedItemFx = createEffect(
       async ({ key, value }: { key: string; value: unknown }) => {
-        const item = JSON.stringify({ value, timestamp: Date.now() });
+        const item = JSON.stringify({
+          value: serialize?.write ? serialize.write(value) : value,
+          timestamp: Date.now(),
+        });
 
         metaStorage.addKey({ key });
 
diff --git a/packages/core/src/cache/adapters/external.ts b/packages/core/src/cache/adapters/external.ts
deleted file mode 100644
index 8060446..0000000
--- a/packages/core/src/cache/adapters/external.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { createEffect, createEvent, Event, sample } from 'effector';
-
-import { createCacheAdapter } from './instance';
-import { attachObservability } from './observability';
-import { CacheAdapter } from './type';
-
-interface NarrowObservability {
-  hit?: Event<{ key: string }>;
-  miss?: Event<{ key: string }>;
-}
-
-/**
- * @deprecated Write your own adapter instead by recipe — https://farfetched.pages.dev/recipes/server_cache.html
- */
-export function externalCache(config: {
-  get: (opts: {
-    key: string;
-  }) =>
-    | Promise<{ value: unknown; cachedAt: number } | null>
-    | { value: unknown; cachedAt: number }
-    | null;
-  set: (opts: { key: string; value: unknown }) => Promise<void> | void;
-  unset: (opts: { key: string }) => Promise<void> | void;
-  purge: () => Promise<void> | void;
-  observability?: NarrowObservability;
-}): CacheAdapter {
-  const purge = createEvent();
-  const purgeFx = createEffect(config.purge);
-  sample({ clock: purge, target: purgeFx });
-
-  const adapter = {
-    get: createEffect(config.get),
-    set: createEffect(config.set),
-    unset: createEffect(config.unset),
-    purge,
-  };
-
-  attachObservability({ adapter, options: config.observability });
-
-  return createCacheAdapter(adapter);
-}
diff --git a/packages/core/src/cache/adapters/local_storage.ts b/packages/core/src/cache/adapters/local_storage.ts
index 3eda6a6..29b3205 100644
--- a/packages/core/src/cache/adapters/local_storage.ts
+++ b/packages/core/src/cache/adapters/local_storage.ts
@@ -1,6 +1,8 @@
-import { browserStorageCache } from './browser_storage';
+import { browserStorageCache, SerializeConfig } from './browser_storage';
 import { CacheAdapter, CacheAdapterOptions } from './type';
 
-export function localStorageCache(config?: CacheAdapterOptions): CacheAdapter {
+export function localStorageCache(
+  config?: CacheAdapterOptions & SerializeConfig
+): CacheAdapter {
   return browserStorageCache({ storage: () => localStorage, ...config });
 }
diff --git a/packages/core/src/cache/adapters/session_storage.ts b/packages/core/src/cache/adapters/session_storage.ts
index dfabc6b..3165f38 100644
--- a/packages/core/src/cache/adapters/session_storage.ts
+++ b/packages/core/src/cache/adapters/session_storage.ts
@@ -1,8 +1,8 @@
-import { browserStorageCache } from './browser_storage';
+import { browserStorageCache, SerializeConfig } from './browser_storage';
 import { CacheAdapter, CacheAdapterOptions } from './type';
 
 export function sessionStorageCache(
-  config?: CacheAdapterOptions
+  config?: CacheAdapterOptions & SerializeConfig
 ): CacheAdapter {
   return browserStorageCache({
     storage: () => sessionStorage,
diff --git a/packages/core/src/cache/key/key.ts b/packages/core/src/cache/key/key.ts
index 6ae36e0..615650d 100644
--- a/packages/core/src/cache/key/key.ts
+++ b/packages/core/src/cache/key/key.ts
@@ -39,13 +39,7 @@ export function queryUniqId(query: Query<any, any, any>) {
 }
 
 function querySid(query: Query<any, any, any>): string | null {
-  const sid = query.$data.sid;
-
-  if (!sid?.includes('|')) {
-    return null;
-  }
-
-  return sid;
+  return query.__.meta.sid ?? null;
 }
 
 const prevNames = new Set<string>();
diff --git a/packages/core/src/errors/guards.ts b/packages/core/src/errors/guards.ts
index 5b74bfa..7022c97 100644
--- a/packages/core/src/errors/guards.ts
+++ b/packages/core/src/errors/guards.ts
@@ -1,9 +1,9 @@
 import {
   ABORT,
-  AbortError,
+  type AbortError,
   HTTP,
-  HttpError,
-  InvalidDataError,
+  type HttpError,
+  type InvalidDataError,
   INVALID_DATA,
   NETWORK,
   NetworkError,
@@ -41,7 +41,7 @@ export function isHttpError(args: WithError): args is WithError<HttpError> {
   return args.error?.errorType === HTTP;
 }
 
-export function isHttpErrorCode<Code extends number>(code: Code) {
+export function isHttpErrorCode<Code extends number>(code: Code | Code[]) {
   return function isExactHttpError(
     args: WithError
   ): args is WithError<HttpError<Code>> {
@@ -49,7 +49,9 @@ export function isHttpErrorCode<Code extends number>(code: Code) {
       return false;
     }
 
-    return args.error.status === code;
+    const codes = Array.isArray(code) ? code : [code];
+
+    return codes.includes(args.error.status as any);
   };
 }
 
diff --git a/packages/core/src/libs/patronus/index.ts b/packages/core/src/libs/patronus/index.ts
index dc9b46f..94102b3 100644
--- a/packages/core/src/libs/patronus/index.ts
+++ b/packages/core/src/libs/patronus/index.ts
@@ -18,4 +18,5 @@ export {
 export { type FetchingStatus } from './status';
 export { time } from './time';
 export { and } from './and';
+export { readonly } from './readonly';
 export { syncBatch } from './sync_batch';
diff --git a/packages/core/src/libs/patronus/readonly.ts b/packages/core/src/libs/patronus/readonly.ts
new file mode 100644
index 0000000..26efce6
--- /dev/null
+++ b/packages/core/src/libs/patronus/readonly.ts
@@ -0,0 +1,10 @@
+import { Event, Store } from 'effector';
+
+export function readonly<T>(store: Store<T>): Store<T>;
+export function readonly<T>(event: Event<T>): Event<T>;
+
+export function readonly<T>(
+  storeOrEvent: Store<T> | Event<T>
+): Store<T> | Event<T> {
+  return storeOrEvent.map((v) => v);
+}
diff --git a/packages/core/src/libs/patronus/sourced.ts b/packages/core/src/libs/patronus/sourced.ts
index fc4ab44..4455414 100644
--- a/packages/core/src/libs/patronus/sourced.ts
+++ b/packages/core/src/libs/patronus/sourced.ts
@@ -1,13 +1,12 @@
 import {
+  is,
   attach,
   combine,
   createEffect,
   createStore,
-  Effect,
-  Event,
-  is,
-  sample,
-  Store,
+  type Effect,
+  type Event,
+  type Store,
 } from 'effector';
 
 // -- Main case --
@@ -31,73 +30,55 @@ type SourcedField<Data, Result, Source> =
 
 function normalizeSourced<Data, Result, Source>(config: {
   field: SourcedField<Data, Result, Source>;
-  clock: Event<Data>;
-}): Store<Result>;
+}): Store<(params: Data) => Result>;
 
 function normalizeSourced<Data, Result, Source>(config: {
   field: SourcedField<Data, Result, Source>;
-  source: Store<Data>;
-}): Store<Result>;
+}): Store<(params: Data) => Result>;
 
 function normalizeSourced<Data, Result, Source>({
   field,
-  clock,
-  source,
 }: {
   field: any;
-  clock?: Event<Data>;
-  source?: Store<Data>;
-}): Store<Result | null> {
-  let $target = createStore<any>(null, { serialize: 'ignore' });
-
-  if (clock) {
-    if (field === undefined) {
-      // do nothing
-    } else if (is.store(field)) {
-      const $storeField = field as Store<Result>;
-
-      sample({ clock, source: $storeField, target: $target });
-    } else if (field?.source && field?.fn) {
-      const callbackField = field as CallbackWithSource<Data, Result, Source>;
-
-      sample({
-        clock,
-        source: callbackField.source,
-        fn: (source, params) => callbackField.fn(params, source),
-        target: $target,
-      });
-    } else if (typeof field === 'function') {
-      const callbackField = field as Callback<Data, Result>;
-
-      sample({ clock, fn: (data) => callbackField(data), target: $target });
-    } else {
-      const valueField = field as Result;
+}): Store<(params: Data) => Result | null> {
+  let $target: Store<(params: Data) => Result | null>;
 
-      sample({ clock, fn: () => valueField, target: $target });
-    }
-  }
-
-  if (source) {
-    const $source = source as Store<Data>;
-    if (field === undefined) {
-      // do nothing
-    } else if (is.store(field)) {
-      const $storeField = field as Store<Result>;
+  if (field === undefined) {
+    // do nothing
+    $target = createStore((_: Data) => null as any, { serialize: 'ignore' });
+  } else if (is.store(field)) {
+    const $storeField = field as Store<Result>;
 
-      $target = $storeField;
-    } else if (field?.source && field?.fn) {
-      const callbackField = field as CallbackWithSource<Data, Result, Source>;
+    $target = combine(
+      $storeField,
+      (fieldValue) =>
+        (_: Data): Result | null =>
+          fieldValue ?? null
+    );
+  } else if (field?.source && field?.fn) {
+    const callbackField = field as CallbackWithSource<Data, Result, Source>;
 
-      $target = combine($source, callbackField.source, callbackField.fn);
-    } else if (typeof field === 'function') {
-      const callbackField = field as Callback<Data, Result>;
+    $target = combine(
+      callbackField.source,
+      (source) =>
+        (params: Data): Result | null =>
+          callbackField.fn(params, source) ?? null
+    );
+  } else if (typeof field === 'function') {
+    const callbackField = field as Callback<Data, Result>;
 
-      $target = $source.map(callbackField);
-    } else {
-      const valueField = field as Result;
+    $target = createStore(
+      (params: Data): Result | null => callbackField(params) ?? null,
+      {
+        serialize: 'ignore',
+      }
+    );
+  } else {
+    const valueField = field as Result;
 
-      $target = createStore(valueField, { serialize: 'ignore' });
-    }
+    $target = createStore((_: Data): Result | null => valueField ?? null, {
+      serialize: 'ignore',
+    });
   }
 
   return $target;
@@ -248,10 +229,20 @@ function extractSource<S>(sourced: SourcedField<any, any, S>): Store<S> | null {
 
 // -- Combine sourced
 
-// TODO: type it https://github.com/igorkamyshev/farfetched/issues/281
-function combineSourced(config: any, mapper?: (v: any) => any) {
-  const megaStore: any = {};
-  const megaFns: any = {};
+// TODO: improve typings of it https://github.com/igorkamyshev/farfetched/issues/281
+function combineSourced<
+  R extends Record<string, SourcedField<any, any, any>>,
+  S
+>(
+  config: R,
+  mapper?: (v: {
+    [Key in keyof R]: R[Key] extends SourcedField<any, infer D, any>
+      ? D
+      : never;
+  }) => S
+): CallbackWithSource<unknown, S, unknown> {
+  const megaStore: Record<string, unknown> = {};
+  const megaFns: Record<string, (...params: unknown[]) => unknown> = {};
 
   for (const [key, value] of Object.entries(config) as any) {
     if (is.store(value)) {
@@ -267,7 +258,7 @@ function combineSourced(config: any, mapper?: (v: any) => any) {
     }
   }
 
-  const $megaSource = combine(megaStore);
+  const $megaSource = combine(megaStore) as Store<unknown>;
 
   return {
     source: $megaSource,
@@ -288,7 +279,7 @@ function combineSourced(config: any, mapper?: (v: any) => any) {
         return result;
       }
     },
-  } as any;
+  };
 }
 
 // -- Exports --
diff --git a/packages/core/src/mutation/create_headless_mutation.ts b/packages/core/src/mutation/create_headless_mutation.ts
index 40f689d..b5e732a 100644
--- a/packages/core/src/mutation/create_headless_mutation.ts
+++ b/packages/core/src/mutation/create_headless_mutation.ts
@@ -1,10 +1,15 @@
+import { attach, type Store } from 'effector';
+
 import { createRemoteOperation } from '../remote_operation/create_remote_operation';
-import { DynamicallySourcedField, StaticOrReactive } from '../libs/patronus';
-import { Mutation, MutationSymbol } from './type';
-import { Contract } from '../contract/type';
-import { InvalidDataError } from '../errors/type';
-import { Validator } from '../validation/type';
-import { attach, Store } from 'effector';
+import {
+  readonly,
+  type DynamicallySourcedField,
+  type StaticOrReactive,
+} from '../libs/patronus';
+import { type Mutation, MutationSymbol } from './type';
+import { type Contract } from '../contract/type';
+import { type InvalidDataError } from '../errors/type';
+import { type Validator } from '../validation/type';
 import { type ExecutionMeta } from '../remote_operation/type';
 
 export interface SharedMutationFactoryConfig {
@@ -101,7 +106,21 @@ export function createHeadlessMutation<
   // -- Public API --
 
   return {
-    ...operation,
+    start: operation.start,
+    started: readonly(operation.started),
+    $status: readonly(operation.$status),
+    $idle: readonly(operation.$idle),
+    $pending: readonly(operation.$pending),
+    $succeeded: readonly(operation.$succeeded),
+    $failed: readonly(operation.$failed),
+    $finished: readonly(operation.$finished),
+    $enabled: readonly(operation.$enabled),
+    finished: {
+      success: readonly(operation.finished.success),
+      failure: readonly(operation.finished.failure),
+      finally: readonly(operation.finished.finally),
+      skip: readonly(operation.finished.skip),
+    },
     __: { ...operation.__, experimentalAPI: { attach: attachProtocol } },
     '@@unitShape': unitShapeProtocol,
   };
diff --git a/packages/core/src/mutation/create_json_mutation.ts b/packages/core/src/mutation/create_json_mutation.ts
index 708ef9c..07bb4e9 100644
--- a/packages/core/src/mutation/create_json_mutation.ts
+++ b/packages/core/src/mutation/create_json_mutation.ts
@@ -1,22 +1,22 @@
-import { attach, createEvent, Event, sample, type Json } from 'effector';
+import { attach, createEvent, sample, type Json, type Event } from 'effector';
 
-import { Contract } from '../contract/type';
+import { type Contract } from '../contract/type';
 import { unknownContract } from '../contract/unknown_contract';
-import { HttpMethod, JsonApiRequestError } from '../fetch/api';
+import { type HttpMethod, type JsonApiRequestError } from '../fetch/api';
 import { createJsonApiRequest } from '../fetch/json';
-import { FetchApiRecord } from '../fetch/lib';
-import { ParamsDeclaration } from '../remote_operation/params';
+import { type FetchApiRecord } from '../fetch/lib';
+import { type ParamsDeclaration } from '../remote_operation/params';
 import {
-  DynamicallySourcedField,
+  type DynamicallySourcedField,
+  type SourcedField,
   normalizeSourced,
-  SourcedField,
 } from '../libs/patronus';
-import { Validator } from '../validation/type';
+import { type Validator } from '../validation/type';
 import {
   createHeadlessMutation,
-  SharedMutationFactoryConfig,
+  type SharedMutationFactoryConfig,
 } from './create_headless_mutation';
-import { Mutation } from './type';
+import { type Mutation } from './type';
 
 // -- Shared --
 
@@ -219,23 +219,30 @@ export function createJsonMutation(config: any): Mutation<any, any, any> {
   headlessMutation.__.executeFx.use(
     attach({
       source: {
-        url: normalizeSourced({
+        partialUrl: normalizeSourced({
           field: config.request.url,
-          clock: internalStart,
         }),
-        body: normalizeSourced({
+        partialBody: normalizeSourced({
           field: config.request.body,
-          clock: internalStart,
         }),
-        headers: normalizeSourced({
+        partialHeaders: normalizeSourced({
           field: config.request.headers,
-          clock: internalStart,
         }),
-        query: normalizeSourced({
+        partialQuery: normalizeSourced({
           field: config.request.query,
-          clock: internalStart,
         }),
       },
+      mapParams(
+        params: any,
+        { partialUrl, partialBody, partialHeaders, partialQuery }
+      ) {
+        return {
+          url: partialUrl(params),
+          body: partialBody(params),
+          headers: partialHeaders(params),
+          query: partialQuery(params),
+        };
+      },
       effect: requestFx,
     })
   );
diff --git a/packages/core/src/query/create_headless_query.ts b/packages/core/src/query/create_headless_query.ts
index 2feedcf..1776ec7 100644
--- a/packages/core/src/query/create_headless_query.ts
+++ b/packages/core/src/query/create_headless_query.ts
@@ -1,14 +1,15 @@
 import {
+  type Store,
+  type Event,
   createStore,
   sample,
   createEvent,
-  Store,
   attach,
   split,
 } from 'effector';
 
-import { Contract } from '../contract/type';
-import { InvalidDataError } from '../errors/type';
+import { type Contract } from '../contract/type';
+import { type InvalidDataError } from '../errors/type';
 import { createRemoteOperation } from '../remote_operation/create_remote_operation';
 import {
   postpone,
@@ -16,13 +17,14 @@ import {
   type Serialize,
   type StaticOrReactive,
   type DynamicallySourcedField,
-  SourcedField,
+  type SourcedField,
 } from '../libs/patronus';
-import { Validator } from '../validation/type';
-import { Query, QueryMeta, QuerySymbol } from './type';
-import { Event } from 'effector';
-import { isEqual } from '../libs/lohyphen';
+import { type Validator } from '../validation/type';
+import { type Query, type QueryMeta, QuerySymbol } from './type';
 import { type ExecutionMeta } from '../remote_operation/type';
+import { isEqual } from '../libs/lohyphen';
+import { readonly } from '../libs/patronus';
+import { isAbortError } from '../errors/guards';
 
 export interface SharedQueryFactoryConfig<Data, Initial = Data> {
   name?: string;
@@ -88,7 +90,11 @@ export function createHeadlessQuery<
     kind: QuerySymbol,
     serialize: serializationForSideStore(serialize),
     enabled,
-    meta: { serialize, initialData },
+    meta: {
+      serialize,
+      initialData,
+      sid: querySid(createStore(null, { sid: 'dummy' })),
+    },
     contract,
     validate,
     mapData,
@@ -138,6 +144,16 @@ export function createHeadlessQuery<
     target: $stale,
   });
 
+  sample({
+    clock: operation.__.lowLevelAPI.pushData,
+    target: [$data, $error.reinit!],
+  });
+
+  sample({
+    clock: operation.__.lowLevelAPI.pushError,
+    target: [$error, $data.reinit!],
+  });
+
   // -- Trigger API
 
   const postponedRefresh: Event<Params> = postpone({
@@ -184,6 +200,16 @@ export function createHeadlessQuery<
     ],
   });
 
+  // -- Aborted --
+
+  const aborted = createEvent<{ params: Params; meta: ExecutionMeta }>();
+
+  sample({
+    clock: operation.finished.failure,
+    filter: isAbortError,
+    target: aborted,
+  });
+
   // -- Protocols --
 
   const unitShape = {
@@ -236,12 +262,27 @@ export function createHeadlessQuery<
   // -- Public API --
 
   return {
-    $data,
-    $error,
-    $stale,
     reset,
     refresh,
-    ...operation,
+    start: operation.start,
+    started: readonly(operation.started),
+    $data: readonly($data),
+    $error: readonly($error),
+    $status: readonly(operation.$status),
+    $idle: readonly(operation.$idle),
+    $pending: readonly(operation.$pending),
+    $succeeded: readonly(operation.$succeeded),
+    $failed: readonly(operation.$failed),
+    $finished: readonly(operation.$finished),
+    $enabled: readonly(operation.$enabled),
+    $stale,
+    aborted: readonly(aborted),
+    finished: {
+      success: readonly(operation.finished.success),
+      failure: readonly(operation.finished.failure),
+      finally: readonly(operation.finished.finally),
+      skip: readonly(operation.finished.skip),
+    },
     __: {
       ...operation.__,
       lowLevelAPI: { ...operation.__.lowLevelAPI, refreshSkipDueToFreshness },
@@ -250,3 +291,13 @@ export function createHeadlessQuery<
     '@@unitShape': unitShapeProtocol,
   };
 }
+
+function querySid($data: Store<any>): string | null {
+  const sid = $data.sid;
+
+  if (!sid?.includes('|')) {
+    return null;
+  }
+
+  return sid;
+}
diff --git a/packages/core/src/query/create_json_query.ts b/packages/core/src/query/create_json_query.ts
index cfeb3e4..5b00139 100644
--- a/packages/core/src/query/create_json_query.ts
+++ b/packages/core/src/query/create_json_query.ts
@@ -1,22 +1,22 @@
-import { attach, createEvent, Event, sample, type Json } from 'effector';
+import { attach, createEvent, sample, type Event, type Json } from 'effector';
 
-import { Contract } from '../contract/type';
+import { type Contract } from '../contract/type';
 import { createJsonApiRequest } from '../fetch/json';
-import { HttpMethod, JsonApiRequestError } from '../fetch/api';
+import { type HttpMethod, type JsonApiRequestError } from '../fetch/api';
 import {
   normalizeSourced,
   type SourcedField,
   type DynamicallySourcedField,
 } from '../libs/patronus';
 import { type ParamsDeclaration } from '../remote_operation/params';
-import { Query } from './type';
-import { FetchApiRecord } from '../fetch/lib';
+import { type Query } from './type';
+import { type FetchApiRecord } from '../fetch/lib';
 import {
   createHeadlessQuery,
-  SharedQueryFactoryConfig,
+  type SharedQueryFactoryConfig,
 } from './create_headless_query';
 import { unknownContract } from '../contract/unknown_contract';
-import { Validator } from '../validation/type';
+import { type Validator } from '../validation/type';
 
 // -- Shared
 
@@ -348,23 +348,30 @@ export function createJsonQuery(config: any) {
   headlessQuery.__.executeFx.use(
     attach({
       source: {
-        url: normalizeSourced({
+        partialUrl: normalizeSourced({
           field: config.request.url,
-          clock: internalStart,
         }),
-        body: normalizeSourced({
+        partialBody: normalizeSourced({
           field: config.request.body,
-          clock: internalStart,
         }),
-        headers: normalizeSourced({
+        partialHeaders: normalizeSourced({
           field: config.request.headers,
-          clock: internalStart,
         }),
-        query: normalizeSourced({
+        partialQuery: normalizeSourced({
           field: config.request.query,
-          clock: internalStart,
         }),
       },
+      mapParams(
+        params: any,
+        { partialUrl, partialBody, partialHeaders, partialQuery }
+      ) {
+        return {
+          url: partialUrl(params),
+          body: partialBody(params),
+          headers: partialHeaders(params),
+          query: partialQuery(params),
+        };
+      },
       effect: requestFx,
     })
   );
diff --git a/packages/core/src/query/type.ts b/packages/core/src/query/type.ts
index 5a09527..b97b9ae 100644
--- a/packages/core/src/query/type.ts
+++ b/packages/core/src/query/type.ts
@@ -1,6 +1,9 @@
-import { Store, Event } from 'effector';
+import { type Store, type Event } from 'effector';
 
-import { RemoteOperation } from '../remote_operation/type';
+import {
+  type ExecutionMeta,
+  type RemoteOperation,
+} from '../remote_operation/type';
 import { type Serialize } from '../libs/patronus';
 
 export const QuerySymbol = Symbol('Query');
@@ -13,6 +16,7 @@ export interface QueryMeta<Data, InitialData> {
    */
   serialize: Serialize<Data | InitialData>;
   initialData: InitialData;
+  sid: string | null;
 }
 
 export type QueryExtraLowLevelAPI = {
@@ -47,6 +51,7 @@ export interface Query<Params, Data, Error, InitialData = null>
    * Is data stale?
    */
   $stale: Store<boolean>;
+  aborted: Event<{ params: Params; meta: ExecutionMeta }>;
   /** Event to reset the whole state of the query */
   reset: Event<void>;
   '@@unitShape': () => {
diff --git a/packages/core/src/remote_operation/create_remote_operation.ts b/packages/core/src/remote_operation/create_remote_operation.ts
index c9e095b..b9888c3 100644
--- a/packages/core/src/remote_operation/create_remote_operation.ts
+++ b/packages/core/src/remote_operation/create_remote_operation.ts
@@ -8,6 +8,7 @@ import {
 
 import {
   not,
+  readonly,
   normalizeSourced,
   normalizeStaticOrReactive,
   type DynamicallySourcedField,
@@ -64,6 +65,8 @@ export function createRemoteOperation<
   paramsAreMeaningless?: boolean;
 }): RemoteOperation<Params, MappedData, Error | InvalidDataError, Meta> {
   const revalidate = createEvent<{ params: Params; refresh: boolean }>();
+  const pushData = createEvent<MappedData>();
+  const pushError = createEvent<Error | InvalidDataError>();
   const startWithMeta = createEvent<{ params: Params; meta: ExecutionMeta }>();
 
   const applyContractFx = createContractApplier<Params, Data, ContractData>(
@@ -89,7 +92,6 @@ export function createRemoteOperation<
       unknown
     >(async ({ params }) => {
       const result = await executeFx(params);
-
       return { result, stale: false };
     }),
   };
@@ -113,6 +115,8 @@ export function createRemoteOperation<
    */
   const start = createEvent<Params>();
 
+  const started = createEvent<{ params: Params; meta: ExecutionMeta }>();
+
   sample({
     clock: start,
     fn: (params) => ({
@@ -135,7 +139,16 @@ export function createRemoteOperation<
       meta: ExecutionMeta;
     }>(),
     skip: createEvent<{ params: Params; meta: ExecutionMeta }>(),
-    finally: createEvent<{ params: Params; meta: ExecutionMeta }>(),
+    finally: createEvent<
+      { params: Params; meta: ExecutionMeta } & (
+        | {
+            status: 'done';
+            result: MappedData;
+          }
+        | { status: 'fail'; error: Error | InvalidDataError }
+        | { status: 'skip' }
+      )
+    >(),
   };
 
   // -- Main stores --
@@ -156,6 +169,7 @@ export function createRemoteOperation<
   const $pending = $status.map((status) => status === 'pending');
   const $failed = $status.map((status) => status === 'fail');
   const $succeeded = $status.map((status) => status === 'done');
+  const $finished = $status.map((status) => ['fail', 'done'].includes(status));
 
   // -- Indicate status --
   sample({
@@ -213,6 +227,8 @@ export function createRemoteOperation<
     target: retrieveDataFx,
   });
 
+  sample({ clock: retrieveDataFx, target: started });
+
   sample({
     clock: retrieveDataFx.done,
     fn: ({ params, result }) => ({
@@ -226,31 +242,41 @@ export function createRemoteOperation<
 
   sample({
     clock: retrieveDataFx.fail,
-    fn: ({ error, params }) => ({
-      error: error,
+    source: $enabled,
+    filter: (enabled, { error }) => enabled && !error.stopErrorPropagation,
+    fn: (_, { error, params }) => ({
+      error: error.error as any,
       params: params.params,
-      meta: { stopErrorPropagation: false, stale: false },
+      meta: { stopErrorPropagation: error.stopErrorPropagation, stale: false },
     }),
-    filter: $enabled,
     target: finished.failure,
   });
 
   const { validDataRecieved, __: invalidDataRecieved } = split(
     sample({
       clock: applyContractFx.done,
-      source: normalizeSourced({
-        field: validate ?? validValidator,
-        clock: applyContractFx.done.map(({ result, params }) => ({
+      source: {
+        partialValidator: normalizeSourced({
+          field: validate ?? validValidator,
+        }),
+      },
+      fn: (
+        { partialValidator },
+        {
+          params: {
+            /* Extract original params, it is params of params */ params,
+            meta,
+          },
           result,
-          params: params.params, // Extract original params, it is params of params
-        })),
-      }),
-      fn: (validation, { params, result }) => ({
+        }
+      ) => ({
         result,
-        // Extract original params, it is params of params
-        params: params.params,
-        validation,
-        meta: params.meta,
+        params,
+        validation: partialValidator({
+          result,
+          params,
+        }),
+        meta,
       }),
     }),
     {
@@ -260,12 +286,13 @@ export function createRemoteOperation<
 
   sample({
     clock: validDataRecieved,
-    source: normalizeSourced({
-      field: mapData,
-      clock: validDataRecieved,
-    }),
-    fn: (result, { params, meta }) => ({
-      result,
+    source: {
+      partialMapper: normalizeSourced({
+        field: mapData,
+      }),
+    },
+    fn: ({ partialMapper }, { params, result, meta }) => ({
+      result: partialMapper({ params, result }),
       params,
       meta,
     }),
@@ -328,33 +355,61 @@ export function createRemoteOperation<
 
   // -- Send finally --
   sample({
-    clock: [finished.success, finished.failure, finished.skip],
-    fn({ params, meta }) {
-      return { params, meta };
-    },
+    clock: finished.success,
+    fn: ({ params, result, meta }) => ({
+      status: 'done' as const,
+      params,
+      result,
+      meta,
+    }),
+    target: finished.finally,
+  });
+
+  sample({
+    clock: finished.failure,
+    fn: ({ params, error, meta }) => ({
+      status: 'fail' as const,
+      params,
+      error,
+      meta,
+    }),
+    target: finished.finally,
+  });
+
+  sample({
+    clock: finished.skip,
+    fn: ({ params, meta }) => ({
+      status: 'skip' as const,
+      params,
+      meta,
+    }),
     target: finished.finally,
   });
 
   return {
     start,
     finished,
+    started,
     $status,
     $idle,
     $pending,
     $failed,
     $succeeded,
+    $finished,
     $enabled,
     __: {
       executeFx,
       meta: { ...meta, name },
       kind,
-      $latestParams,
+      $latestParams: readonly($latestParams),
       lowLevelAPI: {
         dataSources,
         dataSourceRetrieverFx: retrieveDataFx,
         sourced: sourced ?? [],
         paramsAreMeaningless: paramsAreMeaningless ?? false,
         revalidate,
+        pushError,
+        pushData,
         startWithMeta,
       },
     },
@@ -369,22 +424,32 @@ function createDataSourceHandlers<Params>(dataSources: DataSource<Params>[]) {
       meta: ExecutionMeta;
     },
     { result: unknown; stale: boolean },
-    any
+    { stopErrorPropagation: boolean; error: unknown }
   >({
     handler: async ({ params, skipStale }) => {
       for (const dataSource of dataSources) {
-        const fromSource = await dataSource.get({ params });
-
-        if (skipStale && fromSource?.stale) {
-          continue;
-        }
-
-        if (fromSource) {
-          return fromSource;
+        try {
+          const fromSource = await dataSource.get({ params });
+
+          if (skipStale && fromSource?.stale) {
+            continue;
+          }
+
+          if (fromSource) {
+            return fromSource;
+          }
+        } catch (error) {
+          throw {
+            stopErrorPropagation: false,
+            error,
+          };
         }
       }
 
-      throw new Error('No data source returned data');
+      throw {
+        stopErrorPropagation: false,
+        error: new Error('No data source returned data'),
+      };
     },
   });
 
diff --git a/packages/core/src/remote_operation/type.ts b/packages/core/src/remote_operation/type.ts
index 595dd9a..800c4ac 100644
--- a/packages/core/src/remote_operation/type.ts
+++ b/packages/core/src/remote_operation/type.ts
@@ -31,6 +31,8 @@ export interface RemoteOperation<
   $failed: Store<boolean>;
   /** Is fetching succeeded? */
   $succeeded: Store<boolean>;
+  /** Is fetching finished? */
+  $finished: Store<boolean>;
   /**
    * Is operation enabled?
    *
@@ -40,6 +42,8 @@ export interface RemoteOperation<
   $enabled: Store<boolean>;
   /** Event to trigger query */
   start: Event<Params>;
+  /** Event that trigered after operation started */
+  started: Event<{ params: Params; meta: ExecutionMeta }>;
   /** Set of events that represent end of query */
   finished: {
     /** Query was successfully ended, data will be passed as a payload */
@@ -49,7 +53,13 @@ export interface RemoteOperation<
     /** Query execution was skipped due to `enabled` field in config */
     skip: Event<{ params: Params; meta: ExecutionMeta }>;
     /** Query was ended, it merges `success`, `error` and `skip` */
-    finally: Event<{ params: Params; meta: ExecutionMeta }>;
+    finally: Event<
+      { params: Params; meta: ExecutionMeta } & (
+        | { status: 'done'; result: Data }
+        | { status: 'fail'; error: Error }
+        | { status: 'skip' }
+      )
+    >;
   };
   /**
    * DO NOT USE THIS FIELD IN PRODUCTION
@@ -97,6 +107,8 @@ export interface RemoteOperation<
       sourced: SourcedField<Params, unknown, unknown>[];
       paramsAreMeaningless: boolean;
       revalidate: Event<{ params: Params; refresh: boolean }>;
+      pushData: Event<Data>;
+      pushError: Event<Error>;
       startWithMeta: Event<{ params: Params; meta: ExecutionMeta }>;
     } & ExtraLowLevelAPI;
     experimentalAPI?: {
diff --git a/packages/core/src/retry/retry.ts b/packages/core/src/retry/retry.ts
index 748c3c3..4470750 100644
--- a/packages/core/src/retry/retry.ts
+++ b/packages/core/src/retry/retry.ts
@@ -1,10 +1,16 @@
 import {
   combine,
+  createEffect,
   createEvent,
   createStore,
   sample,
   split,
+  attach,
+  scopeBind,
   type Event,
+  type EffectError,
+  type EffectParams,
+  type EffectResult,
 } from 'effector';
 
 import {
@@ -18,6 +24,7 @@ import {
 import { type Time, parseTime } from '../libs/date-nfs';
 
 import {
+  type ExecutionMeta,
   type RemoteOperation,
   type RemoteOperationError,
   type RemoteOperationParams,
@@ -27,6 +34,7 @@ import { type RetryMeta } from './type';
 type FailInfo<Q extends RemoteOperation<any, any, any, any>> = {
   params: RemoteOperationParams<Q>;
   error: RemoteOperationError<Q>;
+  meta: ExecutionMeta;
 };
 
 type RetryConfig<
@@ -44,6 +52,7 @@ type RetryConfig<
     MapParamsSource
   >;
   otherwise?: Event<FailInfo<Q>>;
+  supressIntermediateErrors?: boolean;
 };
 
 export function retry<
@@ -58,9 +67,11 @@ export function retry<
     delay: timeout,
     filter,
     mapParams,
-    otherwise,
+    ...params
   }: RetryConfig<Q, DelaySource, FilterSource, MapParamsSource>
 ): void {
+  const supressIntermediateErrors = params.supressIntermediateErrors ?? false;
+
   const $maxAttempts = normalizeStaticOrReactive(times);
   const $attempt = createStore(1, {
     serialize: 'ignore',
@@ -70,23 +81,36 @@ export function retry<
     attempt: $attempt,
   });
 
+  const $supressError = combine(
+    $attempt,
+    $maxAttempts,
+    (attempt, maxAttempts) =>
+      supressIntermediateErrors && attempt <= maxAttempts
+  );
+
+  const failed = createEvent<{
+    params: RemoteOperationParams<Q>;
+    error: RemoteOperationError<Q>;
+    meta: ExecutionMeta;
+  }>();
+
   const newAttempt = createEvent();
 
   const { planNextAttempt, __: retriesAreOver } = split(
     sample({
-      clock: operation.finished.failure,
+      clock: failed,
       source: {
         maxAttempts: $maxAttempts,
         attempt: $attempt,
+        partialFilter: normalizeSourced({
+          field: filter ?? true,
+        }),
       },
-      filter: normalizeSourced({
-        field: (filter ?? true) as any,
-        clock: operation.finished.failure,
-      }),
-      fn: ({ attempt, maxAttempts }, { params, error }) => ({
+      filter: ({ partialFilter }, clock) => partialFilter(clock),
+      fn: ({ attempt, maxAttempts }, { params, error, meta }) => ({
         params,
         error,
-        meta: { attempt, maxAttempts },
+        meta: { ...meta, attempt, maxAttempts },
       }),
     }),
     { planNextAttempt: ({ meta }) => meta.attempt <= meta.maxAttempts }
@@ -96,15 +120,22 @@ export function retry<
     clock: delay({
       clock: sample({
         clock: planNextAttempt,
-        source: normalizeSourced({
-          field: (mapParams ?? (({ params }: any) => params)) as any,
-          clock: planNextAttempt,
-        }),
+        source: {
+          partialMapper: normalizeSourced({
+            field: (mapParams ?? (({ params }: any) => params)) as any,
+          }),
+        },
+        fn: ({ partialMapper }, clock) => partialMapper(clock),
       }),
-      timeout: normalizeSourced({
-        field: timeout,
-        source: $meta,
-      }).map(parseTime),
+      timeout: combine(
+        {
+          partialTimeout: normalizeSourced({
+            field: timeout,
+          }),
+          meta: $meta,
+        },
+        ({ partialTimeout, meta }) => parseTime(partialTimeout(meta))
+      ),
     }),
     fn: (params) => ({
       params,
@@ -117,7 +148,47 @@ export function retry<
     .on(newAttempt, (attempt) => attempt + 1)
     .reset([operation.finished.success, operation.start]);
 
-  if (otherwise) {
-    sample({ clock: retriesAreOver, target: otherwise });
+  if (params.otherwise) {
+    sample({ clock: retriesAreOver, target: params.otherwise });
   }
+
+  if (supressIntermediateErrors) {
+    const originalFx =
+      operation.__.lowLevelAPI.dataSourceRetrieverFx.use.getCurrent();
+
+    operation.__.lowLevelAPI.dataSourceRetrieverFx.use(
+      attach({
+        source: $supressError,
+        mapParams: (opts, supressError) => ({ ...opts, supressError }),
+        effect: createEffect<
+          EffectParams<
+            typeof operation.__.lowLevelAPI.dataSourceRetrieverFx
+          > & { supressError: boolean },
+          EffectResult<typeof operation.__.lowLevelAPI.dataSourceRetrieverFx>,
+          EffectError<typeof operation.__.lowLevelAPI.dataSourceRetrieverFx>
+        >(async ({ supressError, ...opts }) => {
+          const boundFailed = scopeBind(failed, { safe: true });
+          try {
+            const result = await originalFx(opts);
+
+            return result;
+          } catch (error: any) {
+            if (supressError) {
+              boundFailed({
+                params: opts.params,
+                error: error.error,
+                meta: opts.meta,
+              });
+
+              throw { error: error.error, stopErrorPropagation: true };
+            } else {
+              throw error;
+            }
+          }
+        }),
+      })
+    );
+  }
+
+  sample({ clock: operation.finished.failure, target: failed });
 }
diff --git a/packages/core/src/trigger_api/keep_fresh.ts b/packages/core/src/trigger_api/keep_fresh.ts
index 92e1dd8..7b80cbb 100644
--- a/packages/core/src/trigger_api/keep_fresh.ts
+++ b/packages/core/src/trigger_api/keep_fresh.ts
@@ -91,29 +91,36 @@ export function keepFresh<Params>(
 
     const $previousSources = createStore<any[]>([], { serialize: 'ignore' });
 
+    const $partialSources = combine(
+      query.__.lowLevelAPI.sourced.map((sourced) =>
+        normalizeSourced({ field: sourced })
+      )
+    );
+
     // @ts-expect-error I have no idea
     sample({
       clock: finalyParams,
-      source: combine(
-        query.__.lowLevelAPI.sourced.map((sourced) =>
-          normalizeSourced({ field: sourced, clock: finalyParams })
-        )
-      ),
+      source: $partialSources,
+      fn: (partialSources, clock) =>
+        partialSources.map((partialSource) => partialSource(clock)),
       filter: query.$enabled,
       target: $previousSources,
     });
 
-    const sourcesUpdated = sample({
+    const $nextSources = createStore(null, { serialize: 'ignore' });
+
+    sample({
+      // @ts-expect-error I have no idea
       clock: query.__.lowLevelAPI.sourced.map(extractSource).filter(is.store),
-      source: query.__.$latestParams,
+      source: {
+        latestParams: query.__.$latestParams,
+        partialSources: $partialSources,
+      },
       filter: not(query.$idle),
-      fn: (params): Params => params!,
+      fn: ({ latestParams, partialSources }) =>
+        partialSources.map((partialSource) => partialSource(latestParams)),
+      target: $nextSources,
     });
-    const $nextSources = combine(
-      query.__.lowLevelAPI.sourced.map((sourced) =>
-        normalizeSourced({ field: sourced, clock: sourcesUpdated })
-      )
-    );
 
     triggers.push(
       sample({
diff --git a/packages/core/src/update/update.ts b/packages/core/src/update/update.ts
index 615e056..1bf115c 100644
--- a/packages/core/src/update/update.ts
+++ b/packages/core/src/update/update.ts
@@ -1,13 +1,23 @@
-import { combine, createEvent, merge, sample, split, Store } from 'effector';
+import {
+  combine,
+  createEvent,
+  merge,
+  sample,
+  split,
+  type Store,
+} from 'effector';
 
 import { isNotEmpty } from '../libs/lohyphen';
-import { DynamicallySourcedField, normalizeSourced } from '../libs/patronus';
-import { Mutation } from '../mutation/type';
-import { Query } from '../query/type';
 import {
-  RemoteOperationError,
-  RemoteOperationParams,
-  RemoteOperationResult,
+  type DynamicallySourcedField,
+  normalizeSourced,
+} from '../libs/patronus';
+import { type Mutation } from '../mutation/type';
+import { type Query } from '../query/type';
+import {
+  type RemoteOperationError,
+  type RemoteOperationParams,
+  type RemoteOperationResult,
 } from '../remote_operation/type';
 
 type QueryState<Q extends Query<any, any, any, any>> =
@@ -87,17 +97,17 @@ export function update<
   split({
     source: sample({
       clock: mutation.finished.success,
-      source: normalizeSourced({
-        field: rules.success,
-        clock: sample({
-          clock: mutation.finished.success,
-          source: $queryState,
-          fn: (query, { result, params }) => ({
-            query,
-            mutation: { result, params: params ?? null },
-          }),
+      source: {
+        partialRule: normalizeSourced({
+          field: rules.success,
+        }),
+        queryState: $queryState,
+      },
+      fn: ({ partialRule, queryState }, { result, params }) =>
+        partialRule({
+          query: queryState,
+          mutation: { result, params: params ?? null },
         }),
-      }),
     }),
     match: {
       fillData: (payload: any) => isNotEmpty(payload.result),
@@ -109,17 +119,17 @@ export function update<
     split({
       source: sample({
         clock: mutation.finished.failure,
-        source: normalizeSourced({
-          field: rules.failure,
-          clock: sample({
-            clock: mutation.finished.failure,
-            source: $queryState,
-            fn: (query, { error, params }) => ({
-              query,
-              mutation: { error, params: params ?? null },
-            }),
+        source: {
+          partialRule: normalizeSourced({
+            field: rules.failure,
+          }),
+          queryState: $queryState,
+        },
+        fn: ({ partialRule, queryState }, { error, params }) =>
+          partialRule({
+            query: queryState,
+            mutation: { error, params: params ?? null },
           }),
-        }),
       }),
       match: {
         fillData: (payload: any) => isNotEmpty(payload.result),
@@ -131,13 +141,13 @@ export function update<
   sample({
     clock: fillQueryData,
     fn: ({ result }) => result,
-    target: [query.$data, query.$error.reinit!],
+    target: query.__.lowLevelAPI.pushData,
   });
 
   sample({
     clock: fillQueryError,
     fn: ({ error }) => error,
-    target: [query.$error, query.$data.reinit!],
+    target: query.__.lowLevelAPI.pushError,
   });
 
   // -- Refetching


Test patch:
diff --git a/packages/core/src/attach/__tests__/attach.query.test.ts b/packages/core/src/attach/__tests__/attach.query.test.ts
index 2199773..1661ce6 100644
--- a/packages/core/src/attach/__tests__/attach.query.test.ts
+++ b/packages/core/src/attach/__tests__/attach.query.test.ts
@@ -1,4 +1,4 @@
-import { allSettled, createStore, fork } from 'effector';
+import { allSettled, createEvent, createStore, fork, sample } from 'effector';
 import { describe, test, expect, vi } from 'vitest';
 import { unknownContract } from '../../contract/unknown_contract';
 import { fetchFx } from '../../fetch/fetch';
@@ -6,6 +6,7 @@ import { withFactory } from '../../libs/patronus';
 import { createJsonQuery } from '../../query/create_json_query';
 
 import { createQuery } from '../../query/create_query';
+import { declareParams } from '../../remote_operation/params';
 import { attachOperation } from '../attach';
 
 describe('attach for query', () => {
@@ -187,4 +188,41 @@ describe('attach for query', () => {
     expect(fetchMock).toBeCalledTimes(1);
     expect(await fetchMock.mock.calls[0][0].url).toBe('https://api.salo.com/');
   });
+
+  test('do not mess sourced fields, issue #327', async () => {
+    const originalHandler = vi.fn();
+    const originalQuery = createJsonQuery({
+      params: declareParams<{ id: number }>(),
+      request: {
+        method: 'GET',
+        url: (params) => `https://api.salo.com/${params.id}`,
+      },
+      response: {
+        contract: unknownContract,
+      },
+    });
+
+    const firstQuery = attachOperation(originalQuery);
+    const secondQuery = attachOperation(originalQuery);
+
+    const start = createEvent();
+
+    sample({ clock: start, fn: () => ({ id: 1 }), target: firstQuery.refresh });
+    sample({
+      clock: start,
+      fn: () => ({ id: 2 }),
+      target: secondQuery.refresh,
+    });
+
+    const scope = fork({
+      handlers: [[originalQuery.__.executeFx, originalHandler]],
+    });
+
+    await allSettled(start, { scope });
+
+    expect(originalHandler).toBeCalledTimes(2);
+
+    expect(originalHandler.mock.calls[0][0].url).toBe('https://api.salo.com/1');
+    expect(originalHandler.mock.calls[1][0].url).toBe('https://api.salo.com/2');
+  });
 });
diff --git a/packages/core/src/cache/adapters/__test__/browser_storage.test.ts b/packages/core/src/cache/adapters/__test__/browser_storage.test.ts
new file mode 100644
index 0000000..3dd1dae
--- /dev/null
+++ b/packages/core/src/cache/adapters/__test__/browser_storage.test.ts
@@ -0,0 +1,71 @@
+/**
+ * @vitest-environment jsdom
+ */
+
+import { fork, type Json, scopeBind } from 'effector';
+import { describe, beforeEach, test, expect, vi } from 'vitest';
+
+import { localStorageCache } from '../local_storage';
+import { sessionStorageCache } from '../session_storage';
+
+describe.each([
+  { name: 'sessionSotrage', adapter: sessionStorageCache },
+  { name: 'localStorage', adapter: localStorageCache },
+])('adapter $name', ({ adapter }) => {
+  beforeEach(() => {
+    localStorage.clear();
+    sessionStorage.clear();
+  });
+
+  describe('serialize', () => {
+    test('use custom read', async () => {
+      const read = vi.fn(() => 'custom read');
+
+      const cache = adapter({
+        serialize: {
+          read,
+          write: (value: unknown) => value as Json,
+        },
+      });
+
+      const scope = fork();
+
+      await scopeBind(cache.set, {
+        scope,
+      })({ key: 'key', value: 'myValue' });
+
+      const valueFromCache = await scopeBind(cache.get, {
+        scope,
+      })({ key: 'key' });
+
+      expect(read).toBeCalledTimes(1);
+      expect(read).toBeCalledWith('myValue');
+      expect(valueFromCache?.value).toEqual('custom read');
+    });
+  });
+
+  test('custom write', async () => {
+    const write = vi.fn(() => 'custom write');
+
+    const cache = adapter({
+      serialize: {
+        read: (value: unknown) => value as Json,
+        write,
+      },
+    });
+
+    const scope = fork();
+
+    await scopeBind(cache.set, {
+      scope,
+    })({ key: 'key', value: 'myValue' });
+
+    const valueFromCache = await scopeBind(cache.get, {
+      scope,
+    })({ key: 'key' });
+
+    expect(write).toBeCalledTimes(1);
+    expect(write).toBeCalledWith('myValue');
+    expect(valueFromCache?.value).toEqual('custom write');
+  });
+});
diff --git a/packages/core/src/cache/adapters/__test__/external.test.ts b/packages/core/src/cache/adapters/__test__/external.test.ts
deleted file mode 100644
index 4a5f3c4..0000000
--- a/packages/core/src/cache/adapters/__test__/external.test.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { allSettled, createEvent, fork } from 'effector';
-import { vi, describe, test, expect } from 'vitest';
-
-import { externalCache } from '../external';
-
-describe('external adapter', () => {
-  function externalMock() {
-    const get = vi.fn();
-    const set = vi.fn();
-    const purge = vi.fn();
-
-    return { get, set, purge };
-  }
-
-  test('set and get', async () => {
-    const mock = externalMock();
-
-    const cache = externalCache(mock);
-
-    const scope = fork();
-
-    await allSettled(cache.set, {
-      scope,
-      params: { key: 'key', value: 'myValue' },
-    });
-    expect(mock.set).toBeCalledTimes(1);
-    expect(mock.set).toBeCalledWith({ key: 'key', value: 'myValue' });
-
-    mock.get.mockImplementation(() => 'mockedValue');
-    const resultOne = await allSettled(cache.get, {
-      scope,
-      params: { key: 'key' },
-    });
-    expect(resultOne?.value).toEqual('mockedValue');
-  });
-
-  test('purge removes all values', async () => {
-    const mock = externalMock();
-    const cache = externalCache(mock);
-
-    const scope = fork();
-
-    await allSettled(cache.purge, { scope });
-
-    expect(mock.purge).toBeCalledTimes(1);
-  });
-
-  describe('observability', () => {
-    test('on key found — hit', async () => {
-      const hit = createEvent<{ key: string }>();
-
-      const listener = vi.fn();
-      hit.watch(listener);
-
-      const mock = externalMock();
-
-      mock.get.mockImplementation(() => 'Some value');
-
-      const cache = externalCache({ ...mock, observability: { hit } });
-
-      const scope = fork();
-
-      await allSettled(cache.get, {
-        params: { key: 'someKey' },
-        scope,
-      });
-
-      expect(listener).toBeCalledTimes(1);
-      expect(listener).toBeCalledWith({ key: 'someKey' });
-    });
-
-    test('on key not found — miss', async () => {
-      const miss = createEvent<{ key: string }>();
-
-      const listener = vi.fn();
-      miss.watch(listener);
-
-      const mock = externalMock();
-      mock.get.mockImplementation(() => null);
-      const cache = externalCache({ ...mock, observability: { miss } });
-
-      const scope = fork();
-
-      await allSettled(cache.get, {
-        params: { key: 'otherKey' },
-        scope,
-      });
-
-      expect(listener).toBeCalledTimes(1);
-      expect(listener).toBeCalledWith({ key: 'otherKey' });
-    });
-  });
-});
diff --git a/packages/core/src/cache/adapters/__test__/local_storage.test.ts b/packages/core/src/cache/adapters/__test__/local_storage.test.ts
index e280fd6..16258a5 100644
--- a/packages/core/src/cache/adapters/__test__/local_storage.test.ts
+++ b/packages/core/src/cache/adapters/__test__/local_storage.test.ts
@@ -2,7 +2,7 @@
  * @vitest-environment jsdom
  */
 
-import { createEvent, fork, scopeBind } from 'effector';
+import { createEvent, fork, type Json, scopeBind } from 'effector';
 import { describe, beforeEach, test, expect, vi } from 'vitest';
 
 import { META_KEY } from '../browser_storage';
diff --git a/packages/core/src/errors/__test__/guards.test.ts b/packages/core/src/errors/__test__/guards.test.ts
new file mode 100644
index 0000000..b09b70a
--- /dev/null
+++ b/packages/core/src/errors/__test__/guards.test.ts
@@ -0,0 +1,50 @@
+import { describe, test, expect } from 'vitest';
+import { httpError } from '../create_error';
+
+import { isHttpErrorCode } from '../guards';
+
+describe('isHttpErrorCode', () => {
+  test('isHttpErrorCode supports array of codes', () => {
+    const check401or403 = isHttpErrorCode([401, 403]);
+
+    expect(
+      check401or403({
+        error: httpError({
+          status: 401,
+          statusText: 'Text',
+          response: {},
+        }),
+      })
+    ).toBe(true);
+
+    expect(
+      check401or403({
+        error: httpError({
+          status: 403,
+          statusText: 'Text',
+          response: {},
+        }),
+      })
+    ).toBe(true);
+
+    expect(
+      check401or403({
+        error: httpError({
+          status: 400,
+          statusText: 'Text',
+          response: {},
+        }),
+      })
+    ).toBe(false);
+
+    expect(
+      check401or403({
+        error: httpError({
+          status: 404,
+          statusText: 'Text',
+          response: {},
+        }),
+      })
+    ).toBe(false);
+  });
+});
diff --git a/packages/core/src/libs/patronus/__test__/sourced.test.ts b/packages/core/src/libs/patronus/__test__/sourced.test.ts
index 04be235..acceab1 100644
--- a/packages/core/src/libs/patronus/__test__/sourced.test.ts
+++ b/packages/core/src/libs/patronus/__test__/sourced.test.ts
@@ -1,68 +1,68 @@
-import { allSettled, createEvent, createStore, fork } from 'effector';
-import { describe, test, expect } from 'vitest';
+import {
+  allSettled,
+  combine,
+  createEvent,
+  createStore,
+  createWatch,
+  fork,
+  sample,
+} from 'effector';
+import { describe, test, expect, vi } from 'vitest';
 
 import { combineSourced, normalizeSourced, reduceTwoArgs } from '../sourced';
 
-describe('normalizeSourced (with clock)', () => {
+describe('normalizeSourced', () => {
   test('handle simple value', async () => {
-    const clock = createEvent();
+    const $normalized = normalizeSourced({ field: 'static string' });
 
-    const $normalized = normalizeSourced({ field: 'static string', clock });
+    const $one = combine($normalized, (normalized) => normalized('one'));
+    const $two = combine($normalized, (normalized) => normalized('two'));
 
     const scope = fork();
 
-    await allSettled(clock, { scope });
-
-    expect(scope.getState($normalized)).toBe('static string');
-
-    await allSettled(clock, { scope });
-
-    expect(scope.getState($normalized)).toBe('static string');
+    expect(scope.getState($one)).toBe('static string');
+    expect(scope.getState($two)).toBe('static string');
   });
 
   test('handle simple nullish value', async () => {
-    const clock = createEvent();
-
-    const $normalized = normalizeSourced({ field: false, clock });
+    const $normalized = normalizeSourced({ field: false });
+    const $one = combine($normalized, (normalized) => normalized('one'));
+    const $two = combine($normalized, (normalized) => normalized('two'));
 
     const scope = fork();
 
-    await allSettled(clock, { scope });
-
-    expect(scope.getState($normalized)).toBe(false);
+    expect(scope.getState($one)).toBe(false);
+    expect(scope.getState($two)).toBe(false);
   });
 
   test('handle null value', async () => {
-    const clock = createEvent();
-
-    const $normalized = normalizeSourced({ field: null, clock });
+    const $normalized = normalizeSourced({ field: null });
+    const $one = combine($normalized, (normalized) => normalized('one'));
+    const $two = combine($normalized, (normalized) => normalized('two'));
 
     const scope = fork();
 
-    await allSettled(clock, { scope });
-
-    expect(scope.getState($normalized)).toBe(null);
+    expect(scope.getState($one)).toBe(null);
+    expect(scope.getState($two)).toBe(null);
   });
 
   test('handle store value', async () => {
-    const clock = createEvent();
-
     const $field = createStore('first value');
 
-    const $normalized = normalizeSourced({ field: $field, clock });
+    const $normalized = normalizeSourced({ field: $field });
 
-    const scope = fork();
+    const $one = combine($normalized, (normalized) => normalized('one'));
+    const $two = combine($normalized, (normalized) => normalized('two'));
 
-    await allSettled(clock, { scope });
+    const scope = fork();
 
-    expect(scope.getState($normalized)).toBe('first value');
+    expect(scope.getState($one)).toBe('first value');
+    expect(scope.getState($two)).toBe('first value');
 
     await allSettled($field, { scope, params: 'Second value' });
-    expect(scope.getState($normalized)).toBe('first value');
-
-    await allSettled(clock, { scope });
 
-    expect(scope.getState($normalized)).toBe('Second value');
+    expect(scope.getState($one)).toBe('Second value');
+    expect(scope.getState($two)).toBe('Second value');
   });
 
   test('handle callback with source', async () => {
@@ -72,23 +72,31 @@ describe('normalizeSourced (with clock)', () => {
 
     const $normalized = normalizeSourced({
       field: { source: $source, fn: (params, source) => `${params}_${source}` },
+    });
+
+    const result = sample({
       clock,
+      source: $normalized,
+      fn: (normalized, params) => normalized(params),
     });
 
+    const resultWather = vi.fn();
+
     const scope = fork();
 
-    await allSettled(clock, { scope, params: 'first' });
+    createWatch({ unit: result, fn: resultWather, scope });
 
-    expect(scope.getState($normalized)).toBe('first_1');
+    await allSettled(clock, { scope, params: 'first' });
+    expect(resultWather).toBeCalledWith('first_1');
 
     await allSettled(clock, { scope, params: 'second' });
-    expect(scope.getState($normalized)).toBe('second_1');
+    expect(resultWather).toBeCalledWith('second_1');
 
     await allSettled($source, { scope, params: '2' });
-    expect(scope.getState($normalized)).toBe('second_1');
+    expect(resultWather).toBeCalledWith('second_1');
 
     await allSettled(clock, { scope, params: 'third' });
-    expect(scope.getState($normalized)).toBe('third_2');
+    expect(resultWather).toBeCalledWith('third_2');
   });
 
   test('handle callback', async () => {
@@ -96,123 +104,25 @@ describe('normalizeSourced (with clock)', () => {
 
     const $normalized = normalizeSourced({
       field: (params) => `call_${params}`,
-      clock,
     });
 
-    const scope = fork();
-
-    await allSettled(clock, { scope, params: 'first' });
-    expect(scope.getState($normalized)).toBe('call_first');
-
-    await allSettled(clock, { scope, params: 'second' });
-    expect(scope.getState($normalized)).toBe('call_second');
-  });
-});
-
-describe('normalizeSourced (with source)', () => {
-  test('handle simple value', async () => {
-    const $source = createStore<string>('fdsfd');
-
-    const $normalized = normalizeSourced({
-      field: 'static string',
-      source: $source,
-    });
-
-    const scope = fork();
-
-    expect(scope.getState($normalized)).toBe('static string');
-
-    await allSettled($source, { scope, params: 'tretre' });
-
-    expect(scope.getState($normalized)).toBe('static string');
-  });
-
-  test('handle simple nullish value', async () => {
-    const $source = createStore<string>('fdsfd');
-
-    const $normalized = normalizeSourced({
-      field: false,
-      source: $source,
+    const result = sample({
+      clock,
+      source: $normalized,
+      fn: (normalized, params) => normalized(params),
     });
 
-    const scope = fork();
-
-    expect(scope.getState($normalized)).toBe(false);
-
-    await allSettled($source, { scope, params: 'tretre' });
-
-    expect(scope.getState($normalized)).toBe(false);
-  });
-
-  test('handle null value', async () => {
-    const source = createStore(null);
-
-    const $normalized = normalizeSourced({ field: null, source });
-
-    const scope = fork();
-
-    await allSettled(source, { scope, params: 12 });
-
-    expect(scope.getState($normalized)).toBe(null);
-  });
-
-  test('handle store value', async () => {
-    const $source = createStore('');
-
-    const $field = createStore('first value');
-
-    const $normalized = normalizeSourced({ field: $field, source: $source });
-
-    const scope = fork();
-
-    expect(scope.getState($normalized)).toBe('first value');
-
-    await allSettled($source, { scope, params: 'fdsfgsdgds' });
-
-    await allSettled($field, { scope, params: 'Second value' });
-  });
-
-  test('handle callback with source', async () => {
-    const $source = createStore('first');
-
-    const $externalSource = createStore('1');
-
-    const $normalized = normalizeSourced({
-      field: {
-        source: $externalSource,
-        fn: (params, source) => `${params}_${source}`,
-      },
-      source: $source,
-    });
+    const resultWather = vi.fn();
 
     const scope = fork();
 
-    expect(scope.getState($normalized)).toBe('first_1');
-
-    await allSettled($source, { scope, params: 'second' });
-    expect(scope.getState($normalized)).toBe('second_1');
-
-    await allSettled($externalSource, { scope, params: '2' });
-    expect(scope.getState($normalized)).toBe('second_2');
-
-    await allSettled($source, { scope, params: 'third' });
-    expect(scope.getState($normalized)).toBe('third_2');
-  });
-
-  test('handle callback', async () => {
-    const $source = createStore('first');
-
-    const $normalized = normalizeSourced({
-      field: (params) => `call_${params}`,
-      source: $source,
-    });
-
-    const scope = fork();
+    createWatch({ unit: result, fn: resultWather, scope });
 
-    expect(scope.getState($normalized)).toBe('call_first');
+    await allSettled(clock, { scope, params: 'first' });
+    expect(resultWather).toBeCalledWith('call_first');
 
-    await allSettled($source, { scope, params: 'second' });
-    expect(scope.getState($normalized)).toBe('call_second');
+    await allSettled(clock, { scope, params: 'second' });
+    expect(resultWather).toBeCalledWith('call_second');
   });
 });
 
@@ -227,14 +137,23 @@ describe('reduceTwoArgs', () => {
       })
     );
 
+    const result = sample({
+      clock,
+      source: $normalized,
+      fn: (normalized, params) => normalized(params),
+    });
+
+    const resultWather = vi.fn();
+
     const scope = fork();
 
-    expect(scope.getState($normalized)).toBeNull();
+    createWatch({ unit: result, fn: resultWather, scope });
+
     await allSettled(clock, { scope, params: ['FIRST', 'params'] });
-    expect(scope.getState($normalized)).toBe('FIRSTparams');
+    expect(resultWather).toBeCalledWith('FIRSTparams');
 
     await allSettled(clock, { scope, params: ['SECOND', 'other'] });
-    expect(scope.getState($normalized)).toBe('SECONDother');
+    expect(resultWather).toBeCalledWith('SECONDother');
   });
 
   test('handle callback with source', async () => {
@@ -252,14 +171,24 @@ describe('reduceTwoArgs', () => {
       })
     );
 
+    const result = sample({
+      clock,
+      source: $normalized,
+      fn: (normalized, params) => normalized(params),
+    });
+
+    const resultWather = vi.fn();
+
     const scope = fork();
 
+    createWatch({ unit: result, fn: resultWather, scope });
+
     await allSettled(clock, { scope, params: ['FIRST', 'params'] });
-    expect(scope.getState($normalized)).toBe('FIRSTparamssource');
+    expect(resultWather).toBeCalledWith('FIRSTparamssource');
 
     await allSettled($source, { scope, params: 'new source' });
     await allSettled(clock, { scope, params: ['SECOND', 'other'] });
-    expect(scope.getState($normalized)).toBe('SECONDothernew source');
+    expect(resultWather).toBeCalledWith('SECONDothernew source');
   });
 });
 
@@ -270,13 +199,12 @@ describe('combienSourced', () => {
       name: 'John',
     });
 
-    const clock = createEvent();
-    const $result = normalizeSourced({ field: result, clock });
+    const $result = normalizeSourced({ field: result });
+    const $value = combine($result, (result) => result('DO NOT MATTER'));
 
     const scope = fork();
 
-    await allSettled(clock, { scope });
-    expect(scope.getState($result)).toEqual({ count: 1, name: 'John' });
+    expect(scope.getState($value)).toEqual({ count: 1, name: 'John' });
   });
 
   test('supports stores', async () => {
@@ -285,81 +213,113 @@ describe('combienSourced', () => {
       name: createStore('John'),
     });
 
-    const clock = createEvent();
-    const $result = normalizeSourced({ field: result, clock });
+    const $result = normalizeSourced({ field: result });
+    const $value = combine($result, (result) => result('DO NOT MATTER'));
 
     const scope = fork();
 
-    await allSettled(clock, { scope });
-    expect(scope.getState($result)).toEqual({ count: 1, name: 'John' });
+    expect(scope.getState($value)).toEqual({ count: 1, name: 'John' });
   });
 
   test('supports functions by arg', async () => {
-    const result = combineSourced({
-      count: (val: string) => Number(val),
-      name: createStore('John'),
+    const clock = createEvent<string>();
+
+    const $result = normalizeSourced({
+      field: combineSourced({
+        count: (val: string) => Number(val),
+        name: createStore('John'),
+      }),
     });
 
-    const clock = createEvent<string>();
-    const $result = normalizeSourced({ field: result, clock });
+    const result = sample({
+      clock,
+      source: $result,
+      fn: (normalized, params) => normalized(params),
+    });
+
+    const resultWather = vi.fn();
 
     const scope = fork();
 
+    createWatch({ unit: result, fn: resultWather, scope });
+
     await allSettled(clock, { scope, params: '20' });
-    expect(scope.getState($result)).toEqual({ count: 20, name: 'John' });
+    expect(resultWather).toBeCalledWith({ count: 20, name: 'John' });
 
     await allSettled(clock, { scope, params: '30' });
-    expect(scope.getState($result)).toEqual({ count: 30, name: 'John' });
+    expect(resultWather).toBeCalledWith({ count: 30, name: 'John' });
   });
 
   test('supports function with source by arg', async () => {
     const $source = createStore(1);
 
-    const result = combineSourced({
-      count: {
-        source: $source,
-        fn: (val: string, source: number) => Number(val) + source,
-      },
-      name: createStore('John'),
+    const clock = createEvent<string>();
+
+    const $result = normalizeSourced({
+      field: combineSourced({
+        count: {
+          source: $source,
+          fn: (val: string, source: number) => Number(val) + source,
+        },
+        name: createStore('John'),
+      }),
     });
 
-    const clock = createEvent<string>();
-    const $result = normalizeSourced({ field: result, clock });
+    const result = sample({
+      clock,
+      source: $result,
+      fn: (normalized, params) => normalized(params),
+    });
+
+    const resultWather = vi.fn();
 
     const scope = fork();
 
+    createWatch({ unit: result, fn: resultWather, scope });
+
     await allSettled(clock, { scope, params: '20' });
-    expect(scope.getState($result)).toEqual({ count: 21, name: 'John' });
+    expect(resultWather).toBeCalledWith({ count: 21, name: 'John' });
 
     await allSettled($source, { scope, params: 2 });
     await allSettled(clock, { scope, params: '30' });
-    expect(scope.getState($result)).toEqual({ count: 32, name: 'John' });
+    expect(resultWather).toBeCalledWith({ count: 32, name: 'John' });
   });
 
   test('supports optional mapper', async () => {
     const $source = createStore(1);
 
-    const result = combineSourced(
-      {
-        count: {
-          source: $source,
-          fn: (val: string, source: number) => Number(val) + source,
+    const clock = createEvent<string>();
+
+    const $result = normalizeSourced({
+      field: combineSourced(
+        {
+          count: {
+            source: $source,
+            fn: (val: string, source: number) => Number(val) + source,
+          },
+          name: createStore('John'),
         },
-        name: createStore('John'),
-      },
-      ({ count, name }) => ({ newCount: count, newName: name })
-    );
+        ({ count, name }) => ({ newCount: count, newName: name })
+      ),
+    });
 
-    const clock = createEvent<string>();
-    const $result = normalizeSourced({ field: result, clock });
+    const result = sample({
+      clock,
+      source: $result,
+      fn: (normalized, params) => normalized(params),
+    });
+
+    const resultWather = vi.fn();
 
     const scope = fork();
 
+    createWatch({ unit: result, fn: resultWather, scope });
+
     await allSettled(clock, { scope, params: '20' });
-    expect(scope.getState($result)).toEqual({ newCount: 21, newName: 'John' });
+    expect(resultWather).toBeCalledWith({ newCount: 21, newName: 'John' });
 
     await allSettled($source, { scope, params: 2 });
     await allSettled(clock, { scope, params: '30' });
-    expect(scope.getState($result)).toEqual({ newCount: 32, newName: 'John' });
+    expect(resultWather).toBeCalledWith({ newCount: 32, newName: 'John' });
   });
 });
diff --git a/packages/core/src/mutation/__tests__/create_headless_mutation.test.ts b/packages/core/src/mutation/__tests__/create_headless_mutation.test.ts
index db3afcd..7572d1f 100644
--- a/packages/core/src/mutation/__tests__/create_headless_mutation.test.ts
+++ b/packages/core/src/mutation/__tests__/create_headless_mutation.test.ts
@@ -46,7 +46,7 @@ describe('createHeadlessMutation', () => {
 
     expect(listeners.onFinally).toHaveBeenCalledTimes(1);
     expect(listeners.onFinally).toHaveBeenCalledWith(
-      expect.objectContaining({ params: 42 })
+      expect.objectContaining({ params: 42, result: 42, status: 'done' })
     );
   });
 
diff --git a/packages/core/src/mutation/__tests__/mutation.started.test.ts b/packages/core/src/mutation/__tests__/mutation.started.test.ts
new file mode 100644
index 0000000..0c54a22
--- /dev/null
+++ b/packages/core/src/mutation/__tests__/mutation.started.test.ts
@@ -0,0 +1,34 @@
+import { allSettled, createWatch, fork } from 'effector';
+import { describe, test, expect, vi } from 'vitest';
+
+import { unknownContract } from '../../contract/unknown_contract';
+import { fetchFx } from '../../fetch/fetch';
+import { createJsonMutation } from '../create_json_mutation';
+
+describe('Mutation#started', () => {
+  test('should be fired after start', async () => {
+    const startedListener = vi.fn();
+
+    const mutation = createJsonMutation({
+      request: {
+        method: 'GET',
+        url: '/api/aborted',
+      },
+      response: {
+        contract: unknownContract,
+      },
+    });
+
+    const scope = fork({
+      handlers: [[fetchFx, vi.fn()]],
+    });
+
+    createWatch({ unit: mutation.started, fn: startedListener, scope });
+
+    await allSettled(mutation.start, { scope });
+    expect(startedListener).toBeCalledTimes(1);
+
+    await allSettled(mutation.start, { scope });
+    expect(startedListener).toBeCalledTimes(2);
+  });
+});
diff --git a/packages/core/src/query/__tests__/create_headless_query.test.ts b/packages/core/src/query/__tests__/create_headless_query.test.ts
index 51bbbe5..4338634 100644
--- a/packages/core/src/query/__tests__/create_headless_query.test.ts
+++ b/packages/core/src/query/__tests__/create_headless_query.test.ts
@@ -43,7 +43,7 @@ describe('core/createHeadlessQuery without contract', () => {
 
     expect(listeners.onFinally).toHaveBeenCalledTimes(1);
     expect(listeners.onFinally).toHaveBeenCalledWith(
-      expect.objectContaining({ params: 42 })
+      expect.objectContaining({ params: 42, result: 42, status: 'done' })
     );
   });
 
@@ -79,7 +79,11 @@ describe('core/createHeadlessQuery without contract', () => {
 
     expect(listeners.onFinally).toHaveBeenCalledTimes(1);
     expect(listeners.onFinally).toHaveBeenCalledWith(
-      expect.objectContaining({ params: 42 })
+      expect.objectContaining({
+        params: 42,
+        error: new Error('from mock'),
+        status: 'fail',
+      })
     );
   });
 
diff --git a/packages/core/src/query/__tests__/query.aborted.test.ts b/packages/core/src/query/__tests__/query.aborted.test.ts
new file mode 100644
index 0000000..9bfa1d1
--- /dev/null
+++ b/packages/core/src/query/__tests__/query.aborted.test.ts
@@ -0,0 +1,44 @@
+import { allSettled, createEvent, createWatch, fork } from 'effector';
+import { setTimeout } from 'timers/promises';
+import { describe, test, expect, vi } from 'vitest';
+
+import { unknownContract } from '../../contract/unknown_contract';
+import { fetchFx } from '../../fetch/fetch';
+import { createJsonQuery } from '../create_json_query';
+
+describe('Query#aborted', () => {
+  test('should be fired of operation is aborted', async () => {
+    const abortedListener = vi.fn();
+    const failureListener = vi.fn();
+
+    const signal = createEvent();
+
+    const query = createJsonQuery({
+      request: {
+        method: 'GET',
+        url: '/api/aborted',
+      },
+      response: {
+        contract: unknownContract,
+      },
+      concurrency: { abort: signal },
+    });
+
+    const scope = fork({
+      handlers: [[fetchFx, vi.fn(async () => setTimeout(100))]],
+    });
+
+    createWatch({ unit: query.aborted, fn: abortedListener, scope });
+    createWatch({ unit: query.finished.failure, fn: failureListener, scope });
+
+    allSettled(query.refresh, { scope });
+    allSettled(signal, { scope });
+
+    await allSettled(scope);
+
+    expect(abortedListener).toBeCalledTimes(1);
+    // TODO: enable it in 0.10
+    // expect(failureListener).not.toBeCalledTimes(1);
+    // expect(scope.getState(query.$status)).toBe('idle');
+  });
+});
diff --git a/packages/core/src/query/__tests__/query.started.test.ts b/packages/core/src/query/__tests__/query.started.test.ts
new file mode 100644
index 0000000..840354d
--- /dev/null
+++ b/packages/core/src/query/__tests__/query.started.test.ts
@@ -0,0 +1,61 @@
+import { allSettled, createWatch, fork } from 'effector';
+import { describe, test, expect, vi } from 'vitest';
+import { attachOperation } from '../../attach/attach';
+
+import { unknownContract } from '../../contract/unknown_contract';
+import { fetchFx } from '../../fetch/fetch';
+import { createJsonQuery } from '../create_json_query';
+import { createQuery } from '../create_query';
+
+describe('Query#started', () => {
+  test('should be fired after start', async () => {
+    const startedListener = vi.fn();
+
+    const query = createJsonQuery({
+      request: {
+        method: 'GET',
+        url: '/api/aborted',
+      },
+      response: {
+        contract: unknownContract,
+      },
+    });
+
+    const scope = fork({
+      handlers: [[fetchFx, vi.fn()]],
+    });
+
+    createWatch({ unit: query.started, fn: startedListener, scope });
+
+    await allSettled(query.refresh, { scope });
+    expect(startedListener).toBeCalledTimes(1);
+
+    // Should not start Query again
+    await allSettled(query.refresh, { scope });
+    expect(startedListener).toBeCalledTimes(1);
+  });
+
+  test('support attachOperation, issue #305', async () => {
+    const original = createQuery({ handler: vi.fn() });
+    const attached = attachOperation(original);
+
+    const originalStartedListener = vi.fn();
+
+    const scope = fork();
+
+    createWatch({ unit: original.started, fn: originalStartedListener, scope });
+
+    await allSettled(attached.start, { scope, params: 100 });
+
+    expect(originalStartedListener).toBeCalledTimes(1);
+    expect(originalStartedListener).toBeCalledWith(
+      expect.objectContaining({
+        params: 100,
+        meta: {
+          stale: true,
+          stopErrorPropagation: false,
+        },
+      })
+    );
+  });
+});
diff --git a/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts b/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts
index 6b48dad..99ee146 100644
--- a/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts
+++ b/packages/core/src/remote_operation/__test__/create_remote_operation.test.ts
@@ -98,6 +98,7 @@ describe('createRemoteOperation, disable in-flight', () => {
     expect(scope.getState(operation.$pending)).toBeFalsy();
     expect(scope.getState(operation.$failed)).toBeFalsy();
     expect(scope.getState(operation.$succeeded)).toBeFalsy();
+    expect(scope.getState(operation.$finished)).toBeFalsy();
 
     // do not await
     allSettled(operation.start, { scope, params: 42 });
@@ -107,6 +108,7 @@ describe('createRemoteOperation, disable in-flight', () => {
     expect(scope.getState(operation.$pending)).toBeTruthy();
     expect(scope.getState(operation.$failed)).toBeFalsy();
     expect(scope.getState(operation.$succeeded)).toBeFalsy();
+    expect(scope.getState(operation.$finished)).toBeFalsy();
 
     executorFirstDefer.resolve('result');
     await allSettled(scope);
@@ -116,6 +118,7 @@ describe('createRemoteOperation, disable in-flight', () => {
     expect(scope.getState(operation.$pending)).toBeFalsy();
     expect(scope.getState(operation.$failed)).toBeFalsy();
     expect(scope.getState(operation.$succeeded)).toBeTruthy();
+    expect(scope.getState(operation.$finished)).toBeTruthy();
 
     // do not await
     allSettled(operation.start, { scope, params: 42 });
@@ -125,6 +128,7 @@ describe('createRemoteOperation, disable in-flight', () => {
     expect(scope.getState(operation.$pending)).toBeTruthy();
     expect(scope.getState(operation.$failed)).toBeFalsy();
     expect(scope.getState(operation.$succeeded)).toBeFalsy();
+    expect(scope.getState(operation.$finished)).toBeFalsy();
 
     executorSecondDefer.reject(new Error('error'));
     await allSettled(scope);
@@ -134,5 +138,6 @@ describe('createRemoteOperation, disable in-flight', () => {
     expect(scope.getState(operation.$pending)).toBeFalsy();
     expect(scope.getState(operation.$failed)).toBeTruthy();
     expect(scope.getState(operation.$succeeded)).toBeFalsy();
+    expect(scope.getState(operation.$finished)).toBeTruthy();
   });
 });
diff --git a/packages/core/src/retry/__tests__/retry.query.test-d.ts b/packages/core/src/retry/__tests__/retry.query.test-d.ts
new file mode 100644
index 0000000..56d96d9
--- /dev/null
+++ b/packages/core/src/retry/__tests__/retry.query.test-d.ts
@@ -0,0 +1,16 @@
+import { createEvent, type UnitValue } from 'effector';
+import { describe, test } from 'vitest';
+
+import { type Query } from '../../query/type';
+import { retry } from '../retry';
+
+describe('retry', () => {
+  // Issue — https://github.com/igorkamyshev/farfetched/issues/326
+  test('type of otherwise is compatible with query.finished.failure', () => {
+    const query: Query<void, number, string> = {} as any;
+
+    const otherwise = createEvent<UnitValue<typeof query.finished.failure>>();
+
+    retry(query, { times: 1, delay: 0, otherwise });
+  });
+});
diff --git a/packages/core/src/retry/__tests__/retry.query.test.ts b/packages/core/src/retry/__tests__/retry.query.test.ts
index a16d90a..c43281e 100644
--- a/packages/core/src/retry/__tests__/retry.query.test.ts
+++ b/packages/core/src/retry/__tests__/retry.query.test.ts
@@ -131,6 +131,8 @@ describe('retry with query', () => {
             "meta": {
               "attempt": 1,
               "maxAttempts": 3,
+              "stale": false,
+              "stopErrorPropagation": false,
             },
             "params": "Initial",
           },
@@ -141,6 +143,8 @@ describe('retry with query', () => {
             "meta": {
               "attempt": 2,
               "maxAttempts": 3,
+              "stale": false,
+              "stopErrorPropagation": false,
             },
             "params": "Initial 1",
           },
@@ -151,6 +155,8 @@ describe('retry with query', () => {
             "meta": {
               "attempt": 3,
               "maxAttempts": 3,
+              "stale": false,
+              "stopErrorPropagation": false,
             },
             "params": "Initial 1 2",
           },
@@ -338,4 +344,44 @@ describe('retry with query', () => {
     // 1 for retry
     expect(listeners.onFailure).toBeCalledTimes(2);
   });
+
+  test('throw error in case of retry with supressIntermediateErrors', async () => {
+    const query = createQuery({
+      handler: vi.fn().mockImplementation(({ attempt }) => {
+        throw new Error(`Sorry, attempt ${attempt}`);
+      }),
+    });
+
+    retry(query, {
+      times: 1,
+      mapParams({ meta }) {
+        return { attempt: meta.attempt };
+      },
+      delay: 0,
+      supressIntermediateErrors: true,
+    });
+
+    const scope = fork();
+
+    const { listeners } = watchRemoteOperation(query, scope);
+
+    await allSettled(query.start, { scope, params: { attempt: 0 } });
+
+    // 1 for retry
+    expect(listeners.onFailure).toBeCalledTimes(1);
+    expect(listeners.onFailure.mock.calls[0]).toMatchInlineSnapshot(`
+      [
+        {
+          "error": [Error: Sorry, attempt 1],
+          "meta": {
+            "stale": false,
+            "stopErrorPropagation": false,
+          },
+          "params": {
+            "attempt": 1,
+          },
+        },
+      ]
+    `);
+  });
 });